Files
tutti/packages/agent/store-sqlite/migrations_session_fork.go
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

577 lines
21 KiB
Go

package storesqlite
import (
"context"
"fmt"
)
// applyWorkspaceAgentSessionForkV1 adds the provider-backed session Fork
// operation log and an immutable, session-scoped Turn order. Existing
// sessions are verified only when every Turn has durable, immutable Message
// row-id evidence whose ranges do not overlap. One missing or interleaved
// boundary makes that whole legacy
// session fail closed; timestamps, random external IDs, and mutable versions
// are not proof.
func (s *Store) applyWorkspaceAgentSessionForkV1(ctx context.Context) error {
applied, err := s.hasMigration(ctx, schemaMigrationWorkspaceAgentSessionForkV1)
if err != nil || applied {
return err
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("begin workspace agent session fork v1: %w", err)
}
defer func() { _ = tx.Rollback() }()
if _, err := tx.ExecContext(ctx, `
CREATE TABLE workspace_agent_turn_sequences (
workspace_id TEXT NOT NULL,
agent_session_id TEXT NOT NULL,
turn_id TEXT NOT NULL,
turn_sequence INTEGER NOT NULL CHECK (turn_sequence > 0),
provenance TEXT NOT NULL CHECK (provenance IN ('verified','legacy_unverified','fork_clone_verified')),
PRIMARY KEY (workspace_id, agent_session_id, turn_id),
UNIQUE (workspace_id, agent_session_id, turn_sequence)
);
INSERT INTO workspace_agent_turn_sequences (
workspace_id, agent_session_id, turn_id, turn_sequence, provenance
)
WITH turn_message_evidence AS (
SELECT turn.workspace_id, turn.agent_session_id, turn.turn_id,
MIN(message.id) AS first_message_id,
MAX(message.id) AS last_message_id
FROM workspace_agent_turns turn
LEFT JOIN workspace_agent_messages message
ON message.workspace_id = turn.workspace_id
AND message.agent_session_id = turn.agent_session_id
AND message.turn_id = turn.turn_id
GROUP BY turn.workspace_id, turn.agent_session_id, turn.turn_id
),
ordered AS (
SELECT workspace_id, agent_session_id, turn_id,
ROW_NUMBER() OVER (
PARTITION BY workspace_id, agent_session_id
ORDER BY first_message_id, turn_id
) AS turn_sequence,
MIN(CASE WHEN first_message_id IS NULL THEN 0 ELSE 1 END) OVER (
PARTITION BY workspace_id, agent_session_id
) AS all_turns_have_message_evidence,
MAX(last_message_id) OVER (
PARTITION BY workspace_id, agent_session_id
ORDER BY first_message_id, turn_id
ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING
) AS prior_last_message_id,
first_message_id
FROM turn_message_evidence
),
verified AS (
SELECT workspace_id, agent_session_id, turn_id, turn_sequence,
MIN(CASE
WHEN first_message_id IS NULL THEN 0
WHEN prior_last_message_id IS NOT NULL
AND first_message_id <= prior_last_message_id THEN 0
ELSE 1
END) OVER (
PARTITION BY workspace_id, agent_session_id
) AS message_ranges_are_ordered,
all_turns_have_message_evidence
FROM ordered
)
SELECT workspace_id, agent_session_id, turn_id, turn_sequence,
CASE WHEN all_turns_have_message_evidence = 1
AND message_ranges_are_ordered = 1
THEN 'verified' ELSE 'legacy_unverified' END
FROM verified;
CREATE TRIGGER workspace_agent_turn_sequence_after_insert
AFTER INSERT ON workspace_agent_turns
WHEN NOT EXISTS (
SELECT 1 FROM workspace_agent_turn_sequences
WHERE workspace_id = NEW.workspace_id
AND agent_session_id = NEW.agent_session_id
AND turn_id = NEW.turn_id
)
BEGIN
INSERT INTO workspace_agent_turn_sequences (
workspace_id, agent_session_id, turn_id, turn_sequence, provenance
)
SELECT NEW.workspace_id, NEW.agent_session_id, NEW.turn_id,
COALESCE(MAX(turn_sequence), 0) + 1,
'legacy_unverified'
FROM workspace_agent_turn_sequences
WHERE workspace_id = NEW.workspace_id
AND agent_session_id = NEW.agent_session_id;
END;
CREATE TABLE workspace_agent_session_fork_operations (
operation_id TEXT PRIMARY KEY CHECK (length(operation_id) > 0),
workspace_id TEXT NOT NULL,
request_id TEXT NOT NULL CHECK (length(request_id) > 0),
request_hash TEXT NOT NULL CHECK (length(request_hash) > 0),
source_agent_session_id TEXT NOT NULL CHECK (length(source_agent_session_id) > 0),
target_agent_session_id TEXT NOT NULL CHECK (length(target_agent_session_id) > 0),
source_provider_session_id TEXT NOT NULL CHECK (length(source_provider_session_id) > 0),
source_turn_id TEXT NOT NULL CHECK (length(source_turn_id) > 0),
source_provider_turn_id TEXT NOT NULL CHECK (length(source_provider_turn_id) > 0),
driver_kind TEXT NOT NULL CHECK (length(driver_kind) > 0),
driver_version TEXT NOT NULL CHECK (length(driver_version) > 0),
status TEXT NOT NULL CHECK (status IN (
'prepared','dispatching','provider_accepted','committed','failed','unknown'
)),
target_provider_session_id TEXT,
snapshot_json TEXT NOT NULL CHECK (
json_valid(snapshot_json) AND json_type(snapshot_json) = 'object'
),
snapshot_hash TEXT NOT NULL CHECK (length(snapshot_hash) > 0),
last_error TEXT NOT NULL DEFAULT '',
created_at_unix_ms INTEGER NOT NULL CHECK (created_at_unix_ms > 0),
updated_at_unix_ms INTEGER NOT NULL CHECK (updated_at_unix_ms > 0),
dispatched_at_unix_ms INTEGER,
accepted_at_unix_ms INTEGER,
completed_at_unix_ms INTEGER,
UNIQUE (workspace_id, request_id)
);
CREATE INDEX idx_workspace_agent_session_fork_operations_recovery
ON workspace_agent_session_fork_operations(status, updated_at_unix_ms, operation_id);
CREATE UNIQUE INDEX idx_workspace_agent_session_fork_operations_active_source
ON workspace_agent_session_fork_operations(workspace_id, source_agent_session_id)
WHERE status IN ('prepared','dispatching','provider_accepted');
CREATE TABLE workspace_agent_session_fork_target_reservations (
workspace_id TEXT NOT NULL,
target_agent_session_id TEXT NOT NULL,
operation_id TEXT NOT NULL,
request_id TEXT NOT NULL,
request_hash TEXT NOT NULL,
created_at_unix_ms INTEGER NOT NULL,
PRIMARY KEY (workspace_id, target_agent_session_id),
UNIQUE (workspace_id, request_id),
FOREIGN KEY (operation_id)
REFERENCES workspace_agent_session_fork_operations(operation_id)
);
CREATE TABLE workspace_agent_session_forks (
workspace_id TEXT NOT NULL,
target_agent_session_id TEXT NOT NULL,
source_agent_session_id TEXT NOT NULL,
source_turn_id TEXT NOT NULL,
operation_id TEXT NOT NULL,
forked_at_unix_ms INTEGER NOT NULL,
PRIMARY KEY (workspace_id, target_agent_session_id),
UNIQUE (operation_id),
FOREIGN KEY (workspace_id, target_agent_session_id)
REFERENCES workspace_agent_sessions(workspace_id, agent_session_id)
ON DELETE CASCADE,
FOREIGN KEY (operation_id)
REFERENCES workspace_agent_session_fork_operations(operation_id)
);
`); err != nil {
return fmt.Errorf("create workspace agent session fork v1: %w", err)
}
if err := recordMigrationTx(ctx, tx, schemaMigrationWorkspaceAgentSessionForkV1); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("commit workspace agent session fork v1: %w", err)
}
return nil
}
// applyWorkspaceAgentSessionForkV2 persists the normalized fork point kind on
// every operation. Existing v1 operations are through-Turn operations by
// construction, so the backfill is exact rather than inferred.
func (s *Store) applyWorkspaceAgentSessionForkV2(ctx context.Context) error {
applied, err := s.hasMigration(ctx, schemaMigrationWorkspaceAgentSessionForkV2)
if err != nil || applied {
return err
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("begin workspace agent session fork v2: %w", err)
}
defer func() { _ = tx.Rollback() }()
if _, err := tx.ExecContext(ctx, `
ALTER TABLE workspace_agent_session_fork_operations
ADD COLUMN point_kind TEXT NOT NULL DEFAULT 'through_turn'
CHECK (point_kind IN ('through_turn'));
`); err != nil {
return fmt.Errorf("add workspace agent session fork point kind: %w", err)
}
if err := recordMigrationTx(ctx, tx, schemaMigrationWorkspaceAgentSessionForkV2); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("commit workspace agent session fork v2: %w", err)
}
return nil
}
// applyWorkspaceAgentSessionForkV3 adds an explicit client-observation
// handshake. A boundary barrier survives provider dispatch, unknown delivery,
// and an unobserved canonical commit. This prevents a caller that lost the
// committed response (and then restarted with new request identities) from
// creating a second provider child. Observing a committed operation releases
// only that barrier, so a later explicit user action may create another branch
// from the same Turn.
func (s *Store) applyWorkspaceAgentSessionForkV3(ctx context.Context) error {
applied, err := s.hasMigration(ctx, schemaMigrationWorkspaceAgentSessionForkV3)
if err != nil || applied {
return err
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("begin workspace agent session fork v3: %w", err)
}
defer func() { _ = tx.Rollback() }()
if _, err := tx.ExecContext(ctx, `
ALTER TABLE workspace_agent_session_fork_operations
ADD COLUMN client_observed_at_unix_ms INTEGER;
CREATE TABLE workspace_agent_session_fork_boundary_barriers (
workspace_id TEXT NOT NULL,
source_agent_session_id TEXT NOT NULL,
point_kind TEXT NOT NULL CHECK (point_kind IN ('through_turn')),
source_turn_id TEXT NOT NULL,
operation_id TEXT NOT NULL,
created_at_unix_ms INTEGER NOT NULL,
PRIMARY KEY (
workspace_id, source_agent_session_id, point_kind, source_turn_id
),
UNIQUE (operation_id),
FOREIGN KEY (operation_id)
REFERENCES workspace_agent_session_fork_operations(operation_id)
);
INSERT INTO workspace_agent_session_fork_boundary_barriers (
workspace_id, source_agent_session_id, point_kind, source_turn_id,
operation_id, created_at_unix_ms
)
SELECT workspace_id, source_agent_session_id, point_kind, source_turn_id,
operation_id, created_at_unix_ms
FROM (
SELECT operation.*,
ROW_NUMBER() OVER (
PARTITION BY workspace_id, source_agent_session_id,
point_kind, source_turn_id
ORDER BY created_at_unix_ms DESC, operation_id DESC
) AS boundary_rank
FROM workspace_agent_session_fork_operations operation
WHERE status IN (
'prepared','dispatching','provider_accepted','committed','unknown'
)
)
WHERE boundary_rank = 1;
UPDATE workspace_agent_session_fork_operations
SET client_observed_at_unix_ms = completed_at_unix_ms
WHERE status = 'committed'
AND operation_id NOT IN (
SELECT operation_id
FROM workspace_agent_session_fork_boundary_barriers
);
`); err != nil {
return fmt.Errorf("add workspace agent session fork client observation: %w", err)
}
if err := recordMigrationTx(ctx, tx, schemaMigrationWorkspaceAgentSessionForkV3); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("commit workspace agent session fork v3: %w", err)
}
return nil
}
// applyWorkspaceAgentSessionForkV4 records the canonical target Turn at the
// inclusive Fork boundary. Source Turn ids are remapped while cloning, so a
// durable target id lets timeline clients place lineage UI without duplicating
// the canonical id algorithm. Existing lineage rows are backfilled with that
// same deterministic mapping.
func (s *Store) applyWorkspaceAgentSessionForkV4(ctx context.Context) error {
applied, err := s.hasMigration(ctx, schemaMigrationWorkspaceAgentSessionForkV4)
if err != nil || applied {
return err
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("begin workspace agent session fork v4: %w", err)
}
defer func() { _ = tx.Rollback() }()
if _, err := tx.ExecContext(ctx, `
ALTER TABLE workspace_agent_session_fork_operations
ADD COLUMN target_turn_id TEXT;
ALTER TABLE workspace_agent_session_forks
ADD COLUMN target_turn_id TEXT;
`); err != nil {
return fmt.Errorf("add workspace agent session fork target turn: %w", err)
}
rows, err := tx.QueryContext(ctx, `
SELECT workspace_id, operation_id, source_agent_session_id,
target_agent_session_id, source_turn_id
FROM workspace_agent_session_fork_operations
WHERE status = 'committed'
`)
if err != nil {
return fmt.Errorf("read workspace agent session fork target turn backfill: %w", err)
}
type targetTurnBackfill struct {
workspaceID, operationID, sourceSessionID, targetSessionID, sourceTurnID string
}
var backfills []targetTurnBackfill
for rows.Next() {
var backfill targetTurnBackfill
if err := rows.Scan(
&backfill.workspaceID,
&backfill.operationID,
&backfill.sourceSessionID,
&backfill.targetSessionID,
&backfill.sourceTurnID,
); err != nil {
_ = rows.Close()
return fmt.Errorf("scan workspace agent session fork target turn backfill: %w", err)
}
backfills = append(backfills, backfill)
}
if err := rows.Close(); err != nil {
return fmt.Errorf("close workspace agent session fork target turn backfill: %w", err)
}
if err := rows.Err(); err != nil {
return fmt.Errorf("iterate workspace agent session fork target turn backfill: %w", err)
}
for _, backfill := range backfills {
operation := SessionForkOperation{
WorkspaceID: backfill.workspaceID,
OperationID: backfill.operationID,
SourceAgentSessionID: backfill.sourceSessionID,
TargetAgentSessionID: backfill.targetSessionID,
}
targetTurnID := deterministicSessionForkCanonicalID(
operation,
"turn",
backfill.sourceTurnID,
)
if _, err := tx.ExecContext(ctx, `
UPDATE workspace_agent_session_forks
SET target_turn_id = ?
WHERE workspace_id = ? AND operation_id = ?
`, targetTurnID, backfill.workspaceID, backfill.operationID); err != nil {
return fmt.Errorf("backfill workspace agent session fork target turn: %w", err)
}
if _, err := tx.ExecContext(ctx, `
UPDATE workspace_agent_session_fork_operations
SET target_turn_id = ?
WHERE workspace_id = ? AND operation_id = ? AND status = 'committed'
`, targetTurnID, backfill.workspaceID, backfill.operationID); err != nil {
return fmt.Errorf("backfill workspace agent session fork operation target turn: %w", err)
}
}
if err := recordMigrationTx(ctx, tx, schemaMigrationWorkspaceAgentSessionForkV4); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("commit workspace agent session fork v4: %w", err)
}
return nil
}
// applyWorkspaceAgentSessionForkV5 persists the provider identities created by
// a native fork. Claude's official SDK remaps every transcript UUID, so the
// canonical child must not retain source provider-turn identities.
func (s *Store) applyWorkspaceAgentSessionForkV5(ctx context.Context) error {
applied, err := s.hasMigration(ctx, schemaMigrationWorkspaceAgentSessionForkV5)
if err != nil || applied {
return err
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("begin workspace agent session fork v5: %w", err)
}
defer func() { _ = tx.Rollback() }()
if _, err := tx.ExecContext(ctx, `
ALTER TABLE workspace_agent_session_fork_operations
ADD COLUMN target_title TEXT NOT NULL DEFAULT '';
ALTER TABLE workspace_agent_session_fork_operations
ADD COLUMN target_provider_turn_ids_json TEXT NOT NULL DEFAULT '[]'
CHECK (
json_valid(target_provider_turn_ids_json)
AND json_type(target_provider_turn_ids_json) = 'array'
);
ALTER TABLE workspace_agent_session_fork_operations
ADD COLUMN provider_state_binding_mode TEXT NOT NULL DEFAULT '';
ALTER TABLE workspace_agent_session_fork_operations
ADD COLUMN provider_state_binding_receipt TEXT NOT NULL DEFAULT '';
UPDATE workspace_agent_session_fork_operations
SET provider_state_binding_mode = 'host_copy'
WHERE TRIM(COALESCE(target_provider_session_id, '')) <> '';
`); err != nil {
return fmt.Errorf("add workspace agent session fork provider mapping: %w", err)
}
if err := recordMigrationTx(ctx, tx, schemaMigrationWorkspaceAgentSessionForkV5); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("commit workspace agent session fork v5: %w", err)
}
return nil
}
// applyWorkspaceAgentSessionForkV6 is the intentional hard cutover to
// optimistic Fork. It refuses to migrate while an old saga is non-terminal,
// then replaces the source-wide constraint with an exact active-boundary
// constraint. Different Turns can Fork independently while one Turn cannot
// dispatch two provider mutations concurrently.
func (s *Store) applyWorkspaceAgentSessionForkV6(ctx context.Context) error {
applied, err := s.hasMigration(ctx, schemaMigrationWorkspaceAgentSessionForkV6)
if err != nil || applied {
return err
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("begin workspace agent session fork v6: %w", err)
}
defer func() { _ = tx.Rollback() }()
var nonterminal int
if err := tx.QueryRowContext(ctx, `
SELECT COUNT(*)
FROM workspace_agent_session_fork_operations
WHERE status IN ('prepared','dispatching','provider_accepted')
`).Scan(&nonterminal); err != nil {
return fmt.Errorf("count nonterminal session forks before v6: %w", err)
}
if nonterminal != 0 {
return fmt.Errorf(
"workspace agent session fork v6 requires draining %d nonterminal operations",
nonterminal,
)
}
if _, err := tx.ExecContext(ctx, `
DROP INDEX IF EXISTS idx_workspace_agent_session_fork_operations_active_source;
CREATE UNIQUE INDEX idx_workspace_agent_session_fork_operations_active_boundary
ON workspace_agent_session_fork_operations(
workspace_id, source_agent_session_id, point_kind, source_turn_id
)
WHERE status IN ('prepared','dispatching','provider_accepted');
`); err != nil {
return fmt.Errorf("replace session fork active-source constraint: %w", err)
}
if err := recordMigrationTx(ctx, tx, schemaMigrationWorkspaceAgentSessionForkV6); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("commit workspace agent session fork v6: %w", err)
}
return nil
}
// applyWorkspaceAgentSessionForkV7 stores the complete provider-owned child
// Turn/checkpoint mapping as one ordered receipt. Existing committed
// operations retain only their historical boundary receipt so idempotent
// reads remain possible; their already-materialized child Turns are not
// backfilled.
func (s *Store) applyWorkspaceAgentSessionForkV7(ctx context.Context) error {
applied, err := s.hasMigration(ctx, schemaMigrationWorkspaceAgentSessionForkV7)
if err != nil || applied {
return err
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("begin workspace agent session fork v7: %w", err)
}
defer func() { _ = tx.Rollback() }()
var nonterminal int
if err := tx.QueryRowContext(ctx, `
SELECT COUNT(*)
FROM workspace_agent_session_fork_operations
WHERE status IN ('prepared','dispatching','provider_accepted')
`).Scan(&nonterminal); err != nil {
return fmt.Errorf("count nonterminal session forks before v7: %w", err)
}
if nonterminal != 0 {
return fmt.Errorf(
"workspace agent session fork v7 requires draining %d nonterminal operations",
nonterminal,
)
}
hasSourceBindingJSON, err := hasColumnTx(
ctx,
tx,
"workspace_agent_session_fork_operations",
"source_provider_turn_binding_json",
)
if err != nil {
return err
}
if !hasSourceBindingJSON {
if _, err := tx.ExecContext(ctx, `
ALTER TABLE workspace_agent_session_fork_operations
ADD COLUMN source_provider_turn_binding_json TEXT NOT NULL DEFAULT '{}'
CHECK (
json_valid(source_provider_turn_binding_json)
AND json_type(source_provider_turn_binding_json) = 'object'
);
`); err != nil {
return fmt.Errorf(
"add session fork source provider binding json: %w",
err,
)
}
}
if _, err := tx.ExecContext(ctx, `
ALTER TABLE workspace_agent_session_fork_operations
ADD COLUMN target_provider_turn_bindings_json TEXT NOT NULL DEFAULT '[]'
CHECK (
json_valid(target_provider_turn_bindings_json)
AND json_type(target_provider_turn_bindings_json) = 'array'
);
`); err != nil {
return fmt.Errorf("add workspace agent session fork full turn bindings: %w", err)
}
hasLegacyCheckpoint, err := hasColumnTx(
ctx,
tx,
"workspace_agent_session_fork_operations",
"target_provider_checkpoint_message_id",
)
if err != nil {
return err
}
if hasLegacyCheckpoint {
if _, err := tx.ExecContext(ctx, `
UPDATE workspace_agent_session_fork_operations
SET target_provider_turn_bindings_json = json_array(
json_object(
'providerTurnId',
json_extract(target_provider_turn_ids_json, '$[#-1]'),
'providerTurnBindingJson',
json_object(
'schemaVersion', 1,
'checkpointMessageId', target_provider_checkpoint_message_id
)
)
)
WHERE provider_state_binding_mode = 'provider_owned'
AND json_array_length(target_provider_turn_ids_json) > 0
AND TRIM(COALESCE(target_provider_checkpoint_message_id, '')) <> '';
`); err != nil {
return fmt.Errorf(
"backfill legacy session fork provider binding json: %w",
err,
)
}
}
if err := recordMigrationTx(ctx, tx, schemaMigrationWorkspaceAgentSessionForkV7); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("commit workspace agent session fork v7: %w", err)
}
return nil
}