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
577 lines
21 KiB
Go
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
|
|
}
|