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

314 lines
12 KiB
Go

package storesqlite
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"reflect"
"strings"
"time"
agentactivityprojection "github.com/tutti-os/tutti/packages/agent/store-sqlite/canonical"
)
func incrementAgentSessionMessageVersion(ctx context.Context, tx *sql.Tx, workspaceID, agentSessionID string) (uint64, error) {
if _, err := tx.ExecContext(ctx, `
UPDATE workspace_agent_sessions
SET message_version = message_version + 1,
updated_at_unix_ms = ?
WHERE workspace_id = ? AND agent_session_id = ? AND deleted_at_unix_ms = 0
`, unixMs(time.Now().UTC()), workspaceID, agentSessionID); err != nil {
return 0, fmt.Errorf("increment workspace agent message version: %w", err)
}
row := tx.QueryRowContext(ctx, `
SELECT message_version
FROM workspace_agent_sessions
WHERE workspace_id = ? AND agent_session_id = ? AND deleted_at_unix_ms = 0
`, workspaceID, agentSessionID)
var version uint64
if err := row.Scan(&version); err != nil {
return 0, fmt.Errorf("select workspace agent message version: %w", err)
}
return version, nil
}
// upsertAgentMessageTx returns whether the update was accepted and, separately,
// whether it moved the message to a new status. An accepted replay of an
// already-stored snapshot reports no status transition, so observers that treat
// a terminal status as an incident do not report it again.
func (*Store) upsertAgentMessageTx(
ctx context.Context,
tx *sql.Tx,
workspaceID string,
agentSessionID string,
input MessageUpdate,
now int64,
allowLegacyTurnless bool,
protectExisting bool,
) (Message, bool, bool, error) {
if err := requireSessionForkSourceWritableTx(ctx, tx, workspaceID, agentSessionID); err != nil {
return Message{}, false, false, err
}
existing, ok, err := getAgentMessageForUpdate(ctx, tx, workspaceID, agentSessionID, input.MessageID)
if err != nil {
return Message{}, false, false, err
}
normalizedPayload, err := normalizeJSONMap(input.Payload)
if err != nil {
return Message{}, false, false, fmt.Errorf("normalize workspace agent message payload: %w", err)
}
message, accepted, err := agentactivityprojection.ProjectMessageUpdateChecked(
messageProjectionSnapshot(existing),
ok,
agentactivityprojection.MessageUpdate{
MessageID: input.MessageID,
TurnID: input.TurnID,
Role: input.Role,
Kind: input.Kind,
Status: input.Status,
ContentDelta: input.ContentDelta,
Payload: normalizedPayload,
OccurredAtUnixMS: input.OccurredAtUnixMS,
StartedAtUnixMS: input.StartedAtUnixMS,
CompletedAtUnixMS: input.CompletedAtUnixMS,
},
0,
now,
)
if err != nil {
return Message{}, false, false, fmt.Errorf("project workspace agent message: %w", err)
}
if !accepted {
return Message{}, false, false, nil
}
if ok && agentMessageProjectionAlreadyApplied(existing, message) {
return existing, true, false, nil
}
statusTransitioned := !ok || strings.TrimSpace(existing.Status) != strings.TrimSpace(message.Status)
if ok && protectExisting {
if agentMessageSubmitProvenanceReplay(existing, message, input.Semantics) {
return existing, true, false, nil
}
return Message{}, false, false, fmt.Errorf(
"workspace agent activity message %q conflicts with durable submit provenance",
input.MessageID,
)
}
messageSemantics := cloneMessageSemantics(input.Semantics)
turnID := strings.TrimSpace(message.TurnID)
kind := strings.TrimSpace(message.Kind)
if kind == "session_audit" {
if turnID != "" {
return Message{}, false, false, fmt.Errorf("workspace agent session audit must not reference turn %q", turnID)
}
} else if kind == "collaboration" {
if turnID != "" {
return Message{}, false, false, fmt.Errorf("workspace agent collaboration message must not reference turn %q", turnID)
}
} else if turnID == "" && !allowLegacyTurnless {
return Message{}, false, false, fmt.Errorf("workspace agent message %q kind %q is missing turn", message.MessageID, kind)
} else if turnID != "" {
if _, exists, err := getAgentTurnTx(ctx, tx, workspaceID, agentSessionID, turnID); err != nil {
return Message{}, false, false, err
} else if !exists {
return Message{}, false, false, fmt.Errorf("workspace agent message references unknown turn %q", turnID)
}
}
version, err := incrementAgentSessionMessageVersion(ctx, tx, workspaceID, agentSessionID)
if err != nil {
return Message{}, false, false, err
}
message.Version = version
payloadJSON, err := json.Marshal(message.Payload)
if err != nil {
return Message{}, false, false, fmt.Errorf("encode workspace agent message payload: %w", err)
}
semanticsJSON, err := json.Marshal(messageSemantics)
if err != nil {
return Message{}, false, false, fmt.Errorf("encode workspace agent message semantics: %w", err)
}
_, err = tx.ExecContext(ctx, `
INSERT INTO workspace_agent_messages (
workspace_id, agent_session_id, message_id, version, turn_id, role, kind, status,
semantics_json, payload_json, occurred_at_unix_ms, started_at_unix_ms, completed_at_unix_ms,
created_at_unix_ms, updated_at_unix_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(workspace_id, agent_session_id, message_id) DO UPDATE SET
version = excluded.version,
turn_id = excluded.turn_id,
role = excluded.role,
kind = excluded.kind,
status = excluded.status,
semantics_json = excluded.semantics_json,
payload_json = excluded.payload_json,
occurred_at_unix_ms = excluded.occurred_at_unix_ms,
started_at_unix_ms = excluded.started_at_unix_ms,
completed_at_unix_ms = excluded.completed_at_unix_ms,
deleted_at_unix_ms = 0,
updated_at_unix_ms = excluded.updated_at_unix_ms
`, workspaceID, agentSessionID, strings.TrimSpace(input.MessageID), version,
nullString(strings.TrimSpace(message.TurnID)), message.Role, message.Kind, message.Status, string(semanticsJSON), string(payloadJSON),
message.OccurredAtUnixMS, message.StartedAtUnixMS, message.CompletedAtUnixMS,
message.CreatedAtUnixMS, message.UpdatedAtUnixMS)
if err != nil {
return Message{}, false, false, fmt.Errorf("upsert workspace agent message: %w", err)
}
acceptedMessage, ok, err := getAgentMessageForUpdate(ctx, tx, workspaceID, agentSessionID, input.MessageID)
if err != nil {
return Message{}, false, false, err
}
if !ok {
return Message{}, false, false, fmt.Errorf("read accepted workspace agent message: %w", sql.ErrNoRows)
}
return acceptedMessage, true, statusTransitioned, nil
}
func agentMessageProjectionAlreadyApplied(
existing Message,
projected agentactivityprojection.MessageSnapshot,
) bool {
return strings.TrimSpace(existing.TurnID) == strings.TrimSpace(projected.TurnID) &&
strings.TrimSpace(existing.Role) == strings.TrimSpace(projected.Role) &&
strings.TrimSpace(existing.Kind) == strings.TrimSpace(projected.Kind) &&
strings.TrimSpace(existing.Status) == strings.TrimSpace(projected.Status) &&
reflect.DeepEqual(existing.Payload, projected.Payload)
}
// A runtime's initial user-message report and the Host's post-Exec provenance
// barrier can race to persist the same client submit. Their stable semantic
// envelope is identical, while transport metadata such as sequence and
// timestamps may differ between reports. Treat that as an idempotent replay
// without weakening the protected content, identity, or semantics checks.
func agentMessageSubmitProvenanceReplay(
existing Message,
projected agentactivityprojection.MessageSnapshot,
semantics *MessageSemantics,
) bool {
if strings.TrimSpace(existing.TurnID) != strings.TrimSpace(projected.TurnID) ||
strings.TrimSpace(existing.Role) != "user" ||
strings.TrimSpace(projected.Role) != "user" ||
strings.TrimSpace(existing.Kind) != "text" ||
strings.TrimSpace(projected.Kind) != "text" ||
strings.TrimSpace(existing.Status) != strings.TrimSpace(projected.Status) ||
!reflect.DeepEqual(existing.Semantics, cloneMessageSemantics(semantics)) {
return false
}
existingClientSubmitID := payloadString(existing.Payload, "clientSubmitId")
projectedClientSubmitID := payloadString(projected.Payload, "clientSubmitId")
if existingClientSubmitID == "" || existingClientSubmitID != projectedClientSubmitID {
return false
}
for _, key := range []string{"content", "contentMode", "displayPrompt", "text"} {
if !reflect.DeepEqual(existing.Payload[key], projected.Payload[key]) {
return false
}
}
return true
}
func getAgentMessageForUpdate(
ctx context.Context,
tx *sql.Tx,
workspaceID string,
agentSessionID string,
messageID string,
) (Message, bool, error) {
row := tx.QueryRowContext(ctx, `
SELECT id, agent_session_id, message_id, version, turn_id, role, kind, status,
semantics_json, payload_json, occurred_at_unix_ms, started_at_unix_ms, completed_at_unix_ms,
created_at_unix_ms, updated_at_unix_ms
FROM workspace_agent_messages
WHERE workspace_id = ? AND agent_session_id = ? AND message_id = ? AND deleted_at_unix_ms = 0
`, workspaceID, agentSessionID, messageID)
message, err := scanAgentMessage(row)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return Message{}, false, nil
}
return Message{}, false, fmt.Errorf("get workspace agent message: %w", err)
}
return message, true, nil
}
func messageProjectionSnapshot(message Message) agentactivityprojection.MessageSnapshot {
return agentactivityprojection.MessageSnapshot{
ID: message.ID,
AgentSessionID: strings.TrimSpace(message.AgentSessionID),
MessageID: strings.TrimSpace(message.MessageID),
Version: message.Version,
TurnID: strings.TrimSpace(message.TurnID),
Role: strings.TrimSpace(message.Role),
Kind: strings.TrimSpace(message.Kind),
Status: strings.TrimSpace(message.Status),
Payload: message.Payload,
OccurredAtUnixMS: message.OccurredAtUnixMS,
StartedAtUnixMS: message.StartedAtUnixMS,
CompletedAtUnixMS: message.CompletedAtUnixMS,
CreatedAtUnixMS: message.CreatedAtUnixMS,
UpdatedAtUnixMS: message.UpdatedAtUnixMS,
}
}
func scanAgentMessage(scanner rowScanner) (Message, error) {
var message Message
var payloadJSON string
var semanticsJSON string
var turnID sql.NullString
err := scanner.Scan(
&message.ID,
&message.AgentSessionID,
&message.MessageID,
&message.Version,
&turnID,
&message.Role,
&message.Kind,
&message.Status,
&semanticsJSON,
&payloadJSON,
&message.OccurredAtUnixMS,
&message.StartedAtUnixMS,
&message.CompletedAtUnixMS,
&message.CreatedAtUnixMS,
&message.UpdatedAtUnixMS,
)
if err != nil {
return Message{}, fmt.Errorf("scan workspace agent message row: %w", err)
}
// NULL turn_id (session-level message) is surfaced as an empty string in
// the Go DTO; transport projections re-encode it as null.
message.TurnID = strings.TrimSpace(turnID.String)
if strings.TrimSpace(semanticsJSON) != "" && strings.TrimSpace(semanticsJSON) != "null" && strings.TrimSpace(semanticsJSON) != "{}" {
if err := json.Unmarshal([]byte(semanticsJSON), &message.Semantics); err != nil {
return Message{}, fmt.Errorf("decode workspace agent message semantics id=%d message_id=%q: %w", message.ID, message.MessageID, err)
}
}
if strings.TrimSpace(payloadJSON) == "" {
message.Payload = map[string]any{}
return message, nil
}
if err := json.Unmarshal([]byte(payloadJSON), &message.Payload); err != nil {
return Message{}, fmt.Errorf(
"decode workspace agent message payload id=%d message_id=%q version=%d turn_id=%q role=%q kind=%q status=%q: %w",
message.ID,
message.MessageID,
message.Version,
message.TurnID,
message.Role,
message.Kind,
message.Status,
err,
)
}
return message, nil
}
func cloneMessageSemantics(value *MessageSemantics) *MessageSemantics {
if value == nil {
return nil
}
copy := *value
return &copy
}