Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

214 lines
8.1 KiB
Go

package storesqlite
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"strings"
)
const (
TurnHistoryStateEffective = "effective"
TurnHistoryStateRetracted = "retracted"
SessionHistoryRecoveryReady = "ready"
SessionHistoryRecoveryRollbackPending = "rollback_pending"
SessionHistoryRecoveryResendPending = "resend_pending"
SessionHistoryRecoveryRequired = "recovery_required"
)
var ErrTurnSubmissionConflict = errors.New("workspace agent turn submission conflicts with persisted envelope")
type SessionHistory struct {
WorkspaceID string
AgentSessionID string
Revision uint64
RecoveryState string
OperationID string
UpdatedAtUnixMS int64
}
type TurnHistory struct {
WorkspaceID string
AgentSessionID string
TurnID string
State string
RetractedByOperationID string
ReplacementTurnID string
UpdatedAtUnixMS int64
}
// TurnSubmission is the lossless persisted request envelope used for replay.
// ContentJSON retains attachment identifiers, never hydrated image bytes.
type TurnSubmission struct {
WorkspaceID string
AgentSessionID string
TurnID string
ContentJSON string
DisplayPrompt string
CapabilityRefsJSON string
TuttiModeSnapshotJSON string
MetadataJSON string
ClientSubmitID string
CreatedAtUnixMS int64
UpdatedAtUnixMS int64
}
type TurnSubmissionReader interface {
GetTurnSubmission(context.Context, string, string, string) (TurnSubmission, bool, error)
}
func (s *Store) GetSessionHistory(ctx context.Context, workspaceID, agentSessionID string) (SessionHistory, bool, error) {
if s == nil || s.db == nil {
return SessionHistory{}, false, errors.New("workspace database is not initialized")
}
workspaceID, agentSessionID = strings.TrimSpace(workspaceID), strings.TrimSpace(agentSessionID)
if workspaceID == "" || agentSessionID == "" {
return SessionHistory{}, false, nil
}
var result SessionHistory
err := s.db.QueryRowContext(ctx, `
SELECT workspace_id, agent_session_id, history_revision, recovery_state,
operation_id, updated_at_unix_ms
FROM workspace_agent_session_history
WHERE workspace_id = ? AND agent_session_id = ?
`, workspaceID, agentSessionID).Scan(
&result.WorkspaceID, &result.AgentSessionID, &result.Revision,
&result.RecoveryState, &result.OperationID, &result.UpdatedAtUnixMS,
)
if errors.Is(err, sql.ErrNoRows) {
return SessionHistory{}, false, nil
}
if err != nil {
return SessionHistory{}, false, fmt.Errorf("get workspace agent session history: %w", err)
}
return result, true, nil
}
func (s *Store) GetTurnHistory(ctx context.Context, workspaceID, agentSessionID, turnID string) (TurnHistory, bool, error) {
if s == nil || s.db == nil {
return TurnHistory{}, false, errors.New("workspace database is not initialized")
}
workspaceID, agentSessionID, turnID = strings.TrimSpace(workspaceID), strings.TrimSpace(agentSessionID), strings.TrimSpace(turnID)
if workspaceID == "" || agentSessionID == "" || turnID == "" {
return TurnHistory{}, false, nil
}
var result TurnHistory
err := s.db.QueryRowContext(ctx, `
SELECT workspace_id, agent_session_id, turn_id, history_state,
retracted_by_operation_id, replacement_turn_id, updated_at_unix_ms
FROM workspace_agent_turn_history
WHERE workspace_id = ? AND agent_session_id = ? AND turn_id = ?
`, workspaceID, agentSessionID, turnID).Scan(
&result.WorkspaceID, &result.AgentSessionID, &result.TurnID, &result.State,
&result.RetractedByOperationID, &result.ReplacementTurnID, &result.UpdatedAtUnixMS,
)
if errors.Is(err, sql.ErrNoRows) {
return TurnHistory{}, false, nil
}
if err != nil {
return TurnHistory{}, false, fmt.Errorf("get workspace agent turn history: %w", err)
}
return result, true, nil
}
func (s *Store) RecordTurnSubmission(ctx context.Context, input TurnSubmission) (TurnSubmission, bool, error) {
if s == nil || s.db == nil {
return TurnSubmission{}, false, errors.New("workspace database is not initialized")
}
input.WorkspaceID = strings.TrimSpace(input.WorkspaceID)
input.AgentSessionID = strings.TrimSpace(input.AgentSessionID)
input.TurnID = strings.TrimSpace(input.TurnID)
input.ClientSubmitID = strings.TrimSpace(input.ClientSubmitID)
if strings.TrimSpace(input.MetadataJSON) == "" {
input.MetadataJSON = "{}"
}
var metadata map[string]any
metadataErr := json.Unmarshal([]byte(input.MetadataJSON), &metadata)
if input.WorkspaceID == "" || input.AgentSessionID == "" || input.TurnID == "" ||
!json.Valid([]byte(input.ContentJSON)) ||
!json.Valid([]byte(input.CapabilityRefsJSON)) ||
!json.Valid([]byte(input.TuttiModeSnapshotJSON)) ||
metadataErr != nil || metadata == nil {
return TurnSubmission{}, false, errors.New("record workspace agent turn submission: invalid envelope")
}
if input.CreatedAtUnixMS <= 0 {
input.CreatedAtUnixMS = input.UpdatedAtUnixMS
}
if input.UpdatedAtUnixMS <= 0 {
input.UpdatedAtUnixMS = input.CreatedAtUnixMS
}
result, err := s.db.ExecContext(ctx, `
INSERT INTO workspace_agent_turn_submissions (
workspace_id, agent_session_id, turn_id, content_json, display_prompt,
capability_refs_json, tutti_mode_snapshot_json, metadata_json, client_submit_id,
created_at_unix_ms, updated_at_unix_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(workspace_id, agent_session_id, turn_id) DO NOTHING
`, input.WorkspaceID, input.AgentSessionID, input.TurnID, input.ContentJSON, input.DisplayPrompt,
input.CapabilityRefsJSON, input.TuttiModeSnapshotJSON, input.MetadataJSON, input.ClientSubmitID,
input.CreatedAtUnixMS, input.UpdatedAtUnixMS)
if err != nil {
return TurnSubmission{}, false, fmt.Errorf("record workspace agent turn submission: %w", err)
}
insertedRows, err := result.RowsAffected()
if err != nil {
return TurnSubmission{}, false, fmt.Errorf("record workspace agent turn submission rows affected: %w", err)
}
stored, ok, err := s.GetTurnSubmission(ctx, input.WorkspaceID, input.AgentSessionID, input.TurnID)
if err != nil {
return TurnSubmission{}, false, err
}
if !ok {
return TurnSubmission{}, false, errors.New("record workspace agent turn submission: persisted envelope missing")
}
if !sameTurnSubmissionEnvelope(stored, input) {
return TurnSubmission{}, false, ErrTurnSubmissionConflict
}
return stored, insertedRows == 1, nil
}
func (s *Store) GetTurnSubmission(ctx context.Context, workspaceID, agentSessionID, turnID string) (TurnSubmission, bool, error) {
if s == nil || s.db == nil {
return TurnSubmission{}, false, errors.New("workspace database is not initialized")
}
workspaceID, agentSessionID, turnID = strings.TrimSpace(workspaceID), strings.TrimSpace(agentSessionID), strings.TrimSpace(turnID)
if workspaceID == "" || agentSessionID == "" || turnID == "" {
return TurnSubmission{}, false, nil
}
var result TurnSubmission
err := s.db.QueryRowContext(ctx, `
SELECT workspace_id, agent_session_id, turn_id, content_json, display_prompt,
capability_refs_json, tutti_mode_snapshot_json, metadata_json, client_submit_id,
created_at_unix_ms, updated_at_unix_ms
FROM workspace_agent_turn_submissions
WHERE workspace_id = ? AND agent_session_id = ? AND turn_id = ?
`, workspaceID, agentSessionID, turnID).Scan(
&result.WorkspaceID, &result.AgentSessionID, &result.TurnID,
&result.ContentJSON, &result.DisplayPrompt, &result.CapabilityRefsJSON,
&result.TuttiModeSnapshotJSON, &result.MetadataJSON, &result.ClientSubmitID,
&result.CreatedAtUnixMS, &result.UpdatedAtUnixMS,
)
if errors.Is(err, sql.ErrNoRows) {
return TurnSubmission{}, false, nil
}
if err != nil {
return TurnSubmission{}, false, fmt.Errorf("get workspace agent turn submission: %w", err)
}
return result, true, nil
}
func sameTurnSubmissionEnvelope(left, right TurnSubmission) bool {
return left.WorkspaceID == right.WorkspaceID &&
left.AgentSessionID == right.AgentSessionID &&
left.TurnID == right.TurnID &&
left.ContentJSON == right.ContentJSON &&
left.DisplayPrompt == right.DisplayPrompt &&
left.CapabilityRefsJSON == right.CapabilityRefsJSON &&
left.TuttiModeSnapshotJSON == right.TuttiModeSnapshotJSON &&
left.MetadataJSON == right.MetadataJSON &&
left.ClientSubmitID == right.ClientSubmitID
}