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

458 lines
17 KiB
Go

package storesqlite
import (
"context"
"errors"
"fmt"
"strings"
"time"
)
func (s *Store) ReportSessionState(
ctx context.Context,
input SessionStateReport,
) (StateReportResult, error) {
if s == nil || s.db == nil {
return StateReportResult{}, errors.New("workspace database is not initialized")
}
workspaceID := strings.TrimSpace(input.WorkspaceID)
agentSessionID := strings.TrimSpace(input.AgentSessionID)
if workspaceID == "" || agentSessionID == "" {
return StateReportResult{}, errors.New("workspace id and agent session id are required")
}
if err := s.ensureWorkspaceExists(ctx, workspaceID); err != nil {
return StateReportResult{}, err
}
now := unixMs(time.Now().UTC())
if input.OccurredAtUnixMS <= 0 {
input.OccurredAtUnixMS = now
}
accepted, stateApplied, lastEventUnixMS, session, err := s.upsertAgentSession(ctx, input, now)
if err != nil {
return StateReportResult{}, err
}
return StateReportResult{
TransactionID: session.CommitTransactionID,
CommitDelta: session.CommitDelta,
Accepted: accepted,
StateApplied: stateApplied,
LastEventUnixMS: lastEventUnixMS,
Session: session,
}, nil
}
// ReportActivityState commits a session report and its protocol v2 child
// entities in one transaction. This is the write boundary used by live
// runtime reports; publishing happens only after this method returns.
func (s *Store) ReportActivityState(
ctx context.Context,
input ActivityStateReport,
) (ActivityStateReportResult, error) {
if s == nil || s.db == nil {
return ActivityStateReportResult{}, errors.New("workspace database is not initialized")
}
workspaceID := strings.TrimSpace(input.Session.WorkspaceID)
agentSessionID := strings.TrimSpace(input.Session.AgentSessionID)
if workspaceID == "" || agentSessionID == "" {
return ActivityStateReportResult{}, errors.New("workspace id and agent session id are required")
}
if err := validateActivityStateChildScope(workspaceID, agentSessionID, input); err != nil {
return ActivityStateReportResult{}, err
}
if err := s.ensureWorkspaceExists(ctx, workspaceID); err != nil {
return ActivityStateReportResult{}, err
}
now := unixMs(time.Now().UTC())
if input.Session.OccurredAtUnixMS <= 0 {
input.Session.OccurredAtUnixMS = now
}
var result ActivityStateReportResult
err := retrySQLiteBusy(ctx, func(attemptCtx context.Context) error {
var err error
result, err = s.reportActivityStateOnce(attemptCtx, input, now)
return err
})
return result, err
}
func (s *Store) reportActivityStateOnce(
ctx context.Context,
input ActivityStateReport,
now int64,
) (ActivityStateReportResult, error) {
workspaceID := strings.TrimSpace(input.Session.WorkspaceID)
agentSessionID := strings.TrimSpace(input.Session.AgentSessionID)
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return ActivityStateReportResult{}, fmt.Errorf("begin workspace agent activity state report: %w", err)
}
committed := false
defer func() {
if !committed {
_ = tx.Rollback()
}
}()
goalBefore, err := readSessionGoalProjectionTx(ctx, tx, input.Session)
if err != nil {
return ActivityStateReportResult{}, err
}
accepted, stateApplied, lastEventUnixMS, session, err := s.upsertAgentSessionTx(ctx, tx, input.Session, now)
if err != nil {
return ActivityStateReportResult{}, err
}
sessionWritable, err := sessionActivityWritableTx(ctx, tx, workspaceID, agentSessionID)
if err != nil {
return ActivityStateReportResult{}, err
}
result := ActivityStateReportResult{State: StateReportResult{
Accepted: accepted,
StateApplied: stateApplied,
LastEventUnixMS: lastEventUnixMS,
Session: session,
}}
turnTerminalTransition := false
rootTurnTerminalTransition := false
result.Messages.LatestVersion = session.MessageVersion
if !accepted && len(input.Messages) > 0 {
return ActivityStateReportResult{}, errors.New("workspace agent activity session rejected atomic messages")
}
// Turn transitions have their own monotonic state machine and may be the
// first durable evidence attached to an otherwise exact-replay session
// snapshot (notably provider-initiated interactions). Apply them regardless
// of whether the enclosing session projection changed.
if input.Turn != nil && sessionWritable {
result.Turn, result.TurnAccepted, err = s.recordTurnTransitionTx(ctx, tx, *input.Turn, now)
if err != nil {
return ActivityStateReportResult{}, err
}
if !result.TurnAccepted && !turnTransitionAlreadyApplied(result.Turn, *input.Turn) {
return ActivityStateReportResult{}, errors.New("workspace agent activity turn transition was rejected")
}
turnTerminalTransition = result.TurnAccepted &&
strings.TrimSpace(input.Turn.Phase) == TurnPhaseSettled
}
// RootProviderTurn may arrive on an exact-replay session envelope after Exec
// already set CurrentPhase (Claude Code identity_resolved). Apply whenever the
// session row is addressable so Replay commit correlation still gets a
// durable turn mutation / RootProviderTurnAccepted flag.
if input.RootProviderTurn != nil && sessionWritable {
result.RootTurn, result.RootTurnAccepted, result.RootProviderTurnAccepted, err = s.applyRootProviderTurnTransitionTx(ctx, tx, *input.RootProviderTurn, now)
if err != nil {
return ActivityStateReportResult{}, err
}
rootTurnTerminalTransition = result.RootTurnAccepted &&
result.RootTurn.Phase == TurnPhaseSettled
}
if turnTerminalTransition {
rootTurn, rootAccepted, err := s.reconcileRootTurnAfterChildTerminalTx(ctx, tx, result.Turn, now)
if err != nil {
return ActivityStateReportResult{}, err
}
if rootTurn.TurnID != "" {
result.RootTurn = rootTurn
result.RootTurnAccepted = rootAccepted
rootTurnTerminalTransition = rootAccepted && rootTurn.Phase == TurnPhaseSettled
}
}
// Interaction transitions have their own monotonic identity/state machine.
// Always validate and apply them even when the enclosing session report is
// an exact replay; otherwise an immutable-identity conflict could hide
// behind a stale session timestamp.
if input.Interaction != nil && sessionWritable {
result.Interaction, result.InteractionResult, err = s.upsertInteractionTx(ctx, tx, *input.Interaction, now)
if err != nil {
return ActivityStateReportResult{}, err
}
if result.InteractionResult == InteractionTransitionConflict {
return ActivityStateReportResult{}, errors.New("workspace agent activity interaction transition conflicts with immutable identity")
}
}
if accepted {
for index, message := range input.Messages {
message.MessageID = strings.TrimSpace(message.MessageID)
message.TurnID = strings.TrimSpace(message.TurnID)
if message.MessageID == "" || message.TurnID == "" {
return ActivityStateReportResult{}, fmt.Errorf(
"workspace agent activity message %d requires message id and turn id",
index,
)
}
acceptedMessage, messageAccepted, _, messageErr := s.upsertAgentMessageTx(
ctx, tx, workspaceID, agentSessionID, message, now, false, true,
)
if messageErr != nil {
return ActivityStateReportResult{}, messageErr
}
if !messageAccepted {
return ActivityStateReportResult{}, fmt.Errorf(
"workspace agent activity message %q was rejected",
message.MessageID,
)
}
result.Messages.AcceptedCount++
if acceptedMessage.Version > result.Messages.LatestVersion {
result.Messages.LatestVersion = acceptedMessage.Version
}
result.Messages.Messages = append(result.Messages.Messages, acceptedMessage)
}
}
mutations := activityStateMutations(result, turnTerminalTransition, rootTurnTerminalTransition)
goalMutations, err := sessionGoalMutationsTx(ctx, tx, input.Session, goalBefore)
if err != nil {
return ActivityStateReportResult{}, err
}
mutations = append(mutations, goalMutations...)
delta, err := s.commitTransaction(ctx, tx, workspaceID, mutations)
if err != nil {
return ActivityStateReportResult{}, fmt.Errorf("commit workspace agent activity state report: %w", err)
}
committed = true
result.TransactionID = delta.TransactionID
result.CommitDelta = delta
result.State.TransactionID = delta.TransactionID
result.State.CommitDelta = delta
result.State.Session.CommitTransactionID = delta.TransactionID
result.State.Session.CommitDelta = delta
return result, nil
}
func activityStateMutations(
result ActivityStateReportResult,
turnTerminalTransition bool,
rootTurnTerminalTransition bool,
) []TransactionMutation {
mutations := make([]TransactionMutation, 0, 4+len(result.Messages.Messages))
if result.State.Accepted {
session := result.State.Session
mutations = append(mutations, transactionMutation(session.WorkspaceID, session.ID, MutationEntitySession, session.ID, "upsert", session.UpdatedAtUnixMS))
}
if result.TurnAccepted {
turnMutation := transactionMutation(result.Turn.WorkspaceID, result.Turn.AgentSessionID, MutationEntityTurn, result.Turn.TurnID, "upsert", result.Turn.UpdatedAtUnixMS)
if turnTerminalTransition {
turnMutation = terminalTurnMutation(result.Turn.WorkspaceID, result.Turn.AgentSessionID, result.Turn.TurnID, "upsert", result.Turn.UpdatedAtUnixMS, false)
}
mutations = append(mutations, turnMutation)
}
if result.RootTurnAccepted || result.RootProviderTurnAccepted {
rootMutation := transactionMutation(result.RootTurn.WorkspaceID, result.RootTurn.AgentSessionID, MutationEntityTurn, result.RootTurn.TurnID, "upsert", result.RootTurn.UpdatedAtUnixMS)
if rootTurnTerminalTransition {
rootMutation = terminalTurnMutation(result.RootTurn.WorkspaceID, result.RootTurn.AgentSessionID, result.RootTurn.TurnID, "upsert", result.RootTurn.UpdatedAtUnixMS, false)
}
mutations = append(mutations, rootMutation)
}
if result.InteractionResult == InteractionTransitionApplied {
mutations = append(mutations, transactionMutation(
result.Interaction.WorkspaceID, result.Interaction.AgentSessionID, MutationEntityInteraction,
interactionMutationEntityID(result.Interaction.TurnID, result.Interaction.RequestID),
"upsert", result.Interaction.UpdatedAtUnixMS,
))
}
for _, message := range result.Messages.Messages {
mutations = append(mutations, transactionMutation(
result.State.Session.WorkspaceID, message.AgentSessionID, MutationEntityMessage,
message.MessageID, "upsert", int64(message.Version),
))
}
return mutations
}
func turnTransitionAlreadyApplied(stored Turn, incoming TurnTransition) bool {
if capabilityReferencesAlreadyApplied(stored.CapabilityRefs, incoming.CapabilityRefs) {
if strings.TrimSpace(incoming.Phase) == "" {
return true
}
// A replay of the original submitted envelope may arrive after its
// capability refs were merged into a later lifecycle phase. This is the
// sole cross-phase idempotency case; refs must already be present and the
// submitted event must not be newer than the stored turn.
if strings.TrimSpace(incoming.Phase) == TurnPhaseSubmitted &&
incoming.OccurredAtUnixMS > 0 &&
incoming.OccurredAtUnixMS <= stored.UpdatedAtUnixMS {
return true
}
}
if stored.TurnID == "" || stored.Phase != strings.TrimSpace(incoming.Phase) {
return false
}
if stored.Phase != TurnPhaseSettled {
return true
}
outcome := strings.TrimSpace(incoming.Outcome)
if outcome == "" {
outcome = TurnOutcomeCompleted
}
return stored.Outcome == outcome
}
func capabilityReferencesAlreadyApplied(stored, incoming []CapabilityReference) bool {
normalizedIncoming := normalizeCapabilityReferences(incoming)
if len(normalizedIncoming) == 0 {
return false
}
storedKeys := make(map[string]struct{}, len(stored))
for _, reference := range normalizeCapabilityReferences(stored) {
storedKeys[reference.Source+"\x00"+reference.Capability] = struct{}{}
}
for _, reference := range normalizedIncoming {
if _, ok := storedKeys[reference.Source+"\x00"+reference.Capability]; !ok {
return false
}
}
return true
}
func validateActivityStateChildScope(workspaceID string, agentSessionID string, input ActivityStateReport) error {
if input.Turn != nil && (strings.TrimSpace(input.Turn.WorkspaceID) != workspaceID ||
strings.TrimSpace(input.Turn.AgentSessionID) != agentSessionID) {
return errors.New("turn workspace and agent session must match the activity state report")
}
if input.RootProviderTurn != nil && (strings.TrimSpace(input.RootProviderTurn.WorkspaceID) != workspaceID ||
strings.TrimSpace(input.RootProviderTurn.RootAgentSessionID) != agentSessionID) {
return errors.New("root provider turn workspace and root session must match the activity state report")
}
if input.Interaction != nil && (strings.TrimSpace(input.Interaction.WorkspaceID) != workspaceID ||
strings.TrimSpace(input.Interaction.AgentSessionID) != agentSessionID) {
return errors.New("interaction workspace and agent session must match the activity state report")
}
if input.Turn != nil && input.Interaction != nil &&
strings.TrimSpace(input.Turn.TurnID) != strings.TrimSpace(input.Interaction.TurnID) {
return errors.New("interaction turn must match the activity state report turn")
}
return nil
}
func (s *Store) ReportSessionMessages(
ctx context.Context,
input SessionMessageReport,
) (MessageReportResult, error) {
if s == nil || s.db == nil {
return MessageReportResult{}, errors.New("workspace database is not initialized")
}
workspaceID := strings.TrimSpace(input.WorkspaceID)
agentSessionID := strings.TrimSpace(input.AgentSessionID)
if workspaceID == "" || agentSessionID == "" || len(input.Messages) == 0 {
return MessageReportResult{}, errors.New("workspace id, agent session id, and messages are required")
}
if err := s.ensureWorkspaceExists(ctx, workspaceID); err != nil {
return MessageReportResult{}, err
}
now := unixMs(time.Now().UTC())
var result MessageReportResult
err := retrySQLiteBusy(ctx, func(attemptCtx context.Context) error {
var err error
result, err = s.reportSessionMessagesOnce(attemptCtx, input, now)
return err
})
return result, err
}
func (s *Store) reportSessionMessagesOnce(
ctx context.Context,
input SessionMessageReport,
now int64,
) (MessageReportResult, error) {
workspaceID := strings.TrimSpace(input.WorkspaceID)
agentSessionID := strings.TrimSpace(input.AgentSessionID)
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return MessageReportResult{}, fmt.Errorf("begin workspace agent message report: %w", err)
}
committed := false
defer func() {
if !committed {
_ = tx.Rollback()
}
}()
agentSessionID, err = resolveAgentMessageReportSessionIDTx(ctx, tx, workspaceID, agentSessionID, input.Provider, input.Origin)
if err != nil {
return MessageReportResult{}, err
}
accepted, _, _, _, err := s.upsertAgentSessionTx(ctx, tx, SessionStateReport{
WorkspaceID: workspaceID,
AgentSessionID: agentSessionID,
Origin: input.Origin,
Provider: input.Provider,
}, now)
if err != nil {
return MessageReportResult{}, err
}
if !accepted {
if _, err := s.commitTransaction(ctx, tx, workspaceID, nil); err != nil {
return MessageReportResult{}, fmt.Errorf("commit ignored workspace agent message report: %w", err)
}
committed = true
return MessageReportResult{}, nil
}
result := MessageReportResult{}
allowLegacyTurnless := input.HistoricalImport
historicalTurnIDs := []string(nil)
if input.HistoricalImport {
historicalTurnIDs, err = ensureHistoricalImportTurnsTx(
ctx, tx, workspaceID, agentSessionID, input.Messages, now,
)
if err != nil {
return MessageReportResult{}, err
}
}
for _, message := range input.Messages {
message.MessageID = strings.TrimSpace(message.MessageID)
if message.MessageID == "" {
continue
}
acceptedMessage, accepted, statusTransitioned, err := s.upsertAgentMessageTx(ctx, tx, workspaceID, agentSessionID, message, now, allowLegacyTurnless, false)
if err != nil {
return MessageReportResult{}, err
}
if !accepted {
if input.HistoricalImport && strings.TrimSpace(message.TurnID) != "" {
return MessageReportResult{}, fmt.Errorf(
"historical import message %q was rejected",
message.MessageID,
)
}
continue
}
result.AcceptedCount++
result.LatestVersion = acceptedMessage.Version
result.Messages = append(result.Messages, acceptedMessage)
if statusTransitioned {
result.StatusTransitionedMessageIDs = append(result.StatusTransitionedMessageIDs, acceptedMessage.MessageID)
}
}
historicalTurns := []Turn(nil)
if len(historicalTurnIDs) > 0 {
historicalTurns, err = refreshHistoricalImportTurnsTx(
ctx, tx, workspaceID, agentSessionID, historicalTurnIDs,
)
if err != nil {
return MessageReportResult{}, err
}
}
mutations := make([]TransactionMutation, 0, len(historicalTurns)+len(result.Messages))
for _, turn := range historicalTurns {
mutations = append(mutations, transactionMutation(
workspaceID, agentSessionID, MutationEntityTurn, turn.TurnID, "upsert", turn.UpdatedAtUnixMS,
))
}
for _, message := range result.Messages {
mutations = append(mutations, transactionMutation(workspaceID, agentSessionID, MutationEntityMessage, message.MessageID, "upsert", int64(message.Version)))
}
delta, err := s.commitTransaction(ctx, tx, workspaceID, mutations)
if err != nil {
return MessageReportResult{}, fmt.Errorf("commit workspace agent message report: %w", err)
}
committed = true
result.TransactionID = delta.TransactionID
result.CommitDelta = delta
return result, nil
}