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
490 lines
19 KiB
Go
490 lines
19 KiB
Go
package storesqlite
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
)
|
|
|
|
// applyWorkspaceAgentActivityTurnsV1 creates the protocol v2 turn and
|
|
// interaction entities and backfills historical turn records from message
|
|
// turn ids.
|
|
//
|
|
// Backfill semantics (agent-gui refactor plan, rule nine adjacent):
|
|
// - every distinct non-empty message turn_id becomes a settled turn;
|
|
// - outcome defaults to completed; the newest turn of a failed/canceled
|
|
// session inherits that session outcome (older turns of the same
|
|
// session had to complete for a newer one to exist);
|
|
// - reruns are harmless: inserts use INSERT OR IGNORE and the outcome
|
|
// repair only touches rows the insert created (settled + backfill
|
|
// marker), so live rows written after the migration are never touched.
|
|
func (s *Store) applyWorkspaceAgentActivityTurnsV1(ctx context.Context) error {
|
|
applied, err := s.hasMigration(ctx, schemaMigrationWorkspaceAgentActivityTurnsV1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if applied {
|
|
return nil
|
|
}
|
|
|
|
tx, err := s.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("begin workspace agent activity turns v1: %w", err)
|
|
}
|
|
committed := false
|
|
defer func() {
|
|
if !committed {
|
|
_ = tx.Rollback()
|
|
}
|
|
}()
|
|
|
|
if _, err := tx.ExecContext(ctx, `
|
|
CREATE TABLE IF NOT EXISTS workspace_agent_turns (
|
|
workspace_id TEXT NOT NULL,
|
|
agent_session_id TEXT NOT NULL,
|
|
turn_id TEXT NOT NULL CHECK (length(turn_id) > 0),
|
|
identity_anchor_turn_id TEXT
|
|
CHECK (identity_anchor_turn_id IS NULL OR
|
|
(length(trim(identity_anchor_turn_id)) > 0 AND identity_anchor_turn_id != turn_id)),
|
|
phase TEXT NOT NULL CHECK (phase IN ('submitted','running','waiting','settling','settled')),
|
|
outcome TEXT CHECK (outcome IS NULL OR outcome IN ('completed','failed','canceled','interrupted')),
|
|
error_json TEXT,
|
|
file_changes_json TEXT,
|
|
completed_command_json TEXT,
|
|
backfilled INTEGER NOT NULL DEFAULT 0,
|
|
turn_origin TEXT NOT NULL DEFAULT 'legacy_unknown'
|
|
CHECK (turn_origin IN ('user_prompt','goal_arm','goal_continuation','provider_initiated','legacy_unknown')),
|
|
source_goal_operation_id TEXT,
|
|
source_goal_revision INTEGER CHECK (source_goal_revision IS NULL OR source_goal_revision >= 0),
|
|
source_goal_repair_epoch INTEGER CHECK (source_goal_repair_epoch IS NULL OR source_goal_repair_epoch >= 0),
|
|
started_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
settled_at_unix_ms INTEGER,
|
|
created_at_unix_ms INTEGER NOT NULL,
|
|
updated_at_unix_ms INTEGER NOT NULL,
|
|
root_provider_turn_id TEXT,
|
|
provider_turn_binding_json TEXT NOT NULL DEFAULT '{}'
|
|
CHECK (json_valid(provider_turn_binding_json) AND json_type(provider_turn_binding_json) = 'object'),
|
|
root_provider_turn_phase TEXT CHECK (root_provider_turn_phase IS NULL OR root_provider_turn_phase IN ('running','completed')),
|
|
root_provider_turn_outcome TEXT CHECK (root_provider_turn_outcome IS NULL OR root_provider_turn_outcome IN ('completed','failed','canceled','interrupted')),
|
|
root_provider_turn_error_json TEXT,
|
|
root_provider_turn_completed_command_json TEXT,
|
|
root_provider_turn_updated_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
PRIMARY KEY (workspace_id, agent_session_id, turn_id),
|
|
FOREIGN KEY (workspace_id, agent_session_id)
|
|
REFERENCES workspace_agent_sessions(workspace_id, agent_session_id)
|
|
ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_workspace_agent_turns_session_phase
|
|
ON workspace_agent_turns(workspace_id, agent_session_id, phase);
|
|
|
|
CREATE TABLE IF NOT EXISTS workspace_agent_interactions (
|
|
workspace_id TEXT NOT NULL,
|
|
agent_session_id TEXT NOT NULL,
|
|
request_id TEXT NOT NULL CHECK (length(request_id) > 0),
|
|
turn_id TEXT NOT NULL CHECK (length(turn_id) > 0),
|
|
kind TEXT NOT NULL CHECK (kind IN ('approval','question','plan')),
|
|
status TEXT NOT NULL CHECK (status IN ('pending','answered','superseded')),
|
|
tool_name TEXT NOT NULL DEFAULT '',
|
|
input_json TEXT NOT NULL DEFAULT '{}',
|
|
output_json TEXT NOT NULL DEFAULT '{}',
|
|
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
created_at_unix_ms INTEGER NOT NULL,
|
|
updated_at_unix_ms INTEGER NOT NULL,
|
|
PRIMARY KEY (workspace_id, agent_session_id, turn_id, request_id),
|
|
FOREIGN KEY (workspace_id, agent_session_id, turn_id)
|
|
REFERENCES workspace_agent_turns(workspace_id, agent_session_id, turn_id)
|
|
ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_workspace_agent_interactions_session_status
|
|
ON workspace_agent_interactions(workspace_id, agent_session_id, status);
|
|
`); err != nil {
|
|
return fmt.Errorf("migrate workspace agent activity turns v1: %w", err)
|
|
}
|
|
|
|
hasActiveTurnID, err := hasColumnTx(ctx, tx, "workspace_agent_sessions", "active_turn_id")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !hasActiveTurnID {
|
|
if _, err := tx.ExecContext(ctx, `ALTER TABLE workspace_agent_sessions ADD COLUMN active_turn_id TEXT;`); err != nil {
|
|
return fmt.Errorf("migrate workspace agent sessions active turn id: %w", err)
|
|
}
|
|
}
|
|
|
|
if err := backfillWorkspaceAgentTurnsTx(ctx, tx); err != nil {
|
|
return err
|
|
}
|
|
if err := recordMigrationTx(ctx, tx, schemaMigrationWorkspaceAgentActivityTurnsV1); err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
return fmt.Errorf("commit workspace agent activity turns v1: %w", err)
|
|
}
|
|
committed = true
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) applyWorkspaceAgentActivityInteractionsV2(ctx context.Context) error {
|
|
applied, err := s.hasMigration(ctx, schemaMigrationWorkspaceAgentActivityInteractionsV2)
|
|
if err != nil || applied {
|
|
return err
|
|
}
|
|
tx, err := s.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("begin workspace agent interactions v2: %w", err)
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
if _, err := tx.ExecContext(ctx, `
|
|
CREATE TABLE workspace_agent_interactions_v2 (
|
|
workspace_id TEXT NOT NULL,
|
|
agent_session_id TEXT NOT NULL,
|
|
request_id TEXT NOT NULL CHECK (length(request_id) > 0),
|
|
turn_id TEXT NOT NULL CHECK (length(turn_id) > 0),
|
|
kind TEXT NOT NULL CHECK (kind IN ('approval','question','plan')),
|
|
status TEXT NOT NULL CHECK (status IN ('pending','answered','superseded')),
|
|
tool_name TEXT NOT NULL DEFAULT '',
|
|
input_json TEXT NOT NULL DEFAULT '{}',
|
|
output_json TEXT NOT NULL DEFAULT '{}',
|
|
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
created_at_unix_ms INTEGER NOT NULL,
|
|
updated_at_unix_ms INTEGER NOT NULL,
|
|
PRIMARY KEY (workspace_id, agent_session_id, turn_id, request_id),
|
|
FOREIGN KEY (workspace_id, agent_session_id, turn_id)
|
|
REFERENCES workspace_agent_turns(workspace_id, agent_session_id, turn_id)
|
|
ON DELETE CASCADE
|
|
);
|
|
INSERT INTO workspace_agent_interactions_v2 (
|
|
workspace_id, agent_session_id, request_id, turn_id, kind, status, tool_name,
|
|
input_json, output_json, metadata_json, created_at_unix_ms, updated_at_unix_ms
|
|
)
|
|
SELECT workspace_id, agent_session_id, request_id, turn_id, kind, status, tool_name,
|
|
input_json, output_json, metadata_json, created_at_unix_ms, updated_at_unix_ms
|
|
FROM workspace_agent_interactions;
|
|
DROP TABLE workspace_agent_interactions;
|
|
ALTER TABLE workspace_agent_interactions_v2 RENAME TO workspace_agent_interactions;
|
|
CREATE INDEX idx_workspace_agent_interactions_session_status
|
|
ON workspace_agent_interactions(workspace_id, agent_session_id, status);
|
|
`); err != nil {
|
|
return fmt.Errorf("migrate workspace agent interactions v2: %w", err)
|
|
}
|
|
if err := recordMigrationTx(ctx, tx, schemaMigrationWorkspaceAgentActivityInteractionsV2); err != nil {
|
|
return err
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
func backfillWorkspaceAgentTurnsTx(ctx context.Context, tx *sql.Tx) error {
|
|
if _, err := tx.ExecContext(ctx, `
|
|
INSERT OR IGNORE INTO workspace_agent_turns (
|
|
workspace_id, agent_session_id, turn_id, phase, outcome, backfilled,
|
|
started_at_unix_ms, settled_at_unix_ms, created_at_unix_ms, updated_at_unix_ms
|
|
)
|
|
SELECT
|
|
message_times.workspace_id,
|
|
message_times.agent_session_id,
|
|
message_times.turn_id,
|
|
'settled',
|
|
'completed',
|
|
1,
|
|
COALESCE(
|
|
MIN(CASE WHEN message_times.role = 'user' THEN message_times.started_at_unix_ms END),
|
|
MIN(message_times.started_at_unix_ms)
|
|
),
|
|
MAX(message_times.settled_at_unix_ms),
|
|
COALESCE(
|
|
MIN(CASE WHEN message_times.role = 'user' THEN message_times.started_at_unix_ms END),
|
|
MIN(message_times.started_at_unix_ms)
|
|
),
|
|
MAX(message_times.settled_at_unix_ms)
|
|
FROM (
|
|
SELECT
|
|
m.workspace_id,
|
|
m.agent_session_id,
|
|
TRIM(m.turn_id) AS turn_id,
|
|
m.role,
|
|
CASE
|
|
WHEN m.started_at_unix_ms > 0 THEN m.started_at_unix_ms
|
|
WHEN m.occurred_at_unix_ms > 0 THEN m.occurred_at_unix_ms
|
|
ELSE m.created_at_unix_ms
|
|
END AS started_at_unix_ms,
|
|
CASE
|
|
WHEN m.completed_at_unix_ms > 0 THEN m.completed_at_unix_ms
|
|
WHEN m.occurred_at_unix_ms > 0 THEN m.occurred_at_unix_ms
|
|
WHEN m.updated_at_unix_ms > 0 THEN m.updated_at_unix_ms
|
|
ELSE m.created_at_unix_ms
|
|
END AS settled_at_unix_ms
|
|
FROM workspace_agent_messages m
|
|
INNER JOIN workspace_agent_sessions s
|
|
ON s.workspace_id = m.workspace_id
|
|
AND s.agent_session_id = m.agent_session_id
|
|
WHERE TRIM(m.turn_id) != ''
|
|
AND m.deleted_at_unix_ms = 0
|
|
AND s.deleted_at_unix_ms = 0
|
|
) AS message_times
|
|
GROUP BY message_times.workspace_id, message_times.agent_session_id, message_times.turn_id
|
|
`); err != nil {
|
|
return fmt.Errorf("backfill workspace agent turns: %w", err)
|
|
}
|
|
|
|
// Claimed legacy databases may still carry a pre-activity sessions table
|
|
// without a status column; those sessions have no terminal status to
|
|
// inherit, so the outcome repair is skipped.
|
|
hasStatus, err := hasColumnTx(ctx, tx, "workspace_agent_sessions", "status")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !hasStatus {
|
|
return nil
|
|
}
|
|
|
|
// The newest backfilled turn of a failed/canceled session inherits the
|
|
// session outcome; ties on settled_at fall back to lexically greatest
|
|
// turn id for determinism.
|
|
if _, err := tx.ExecContext(ctx, `
|
|
UPDATE workspace_agent_turns
|
|
SET outcome = (
|
|
SELECT CASE s.status WHEN 'failed' THEN 'failed' ELSE 'canceled' END
|
|
FROM workspace_agent_sessions s
|
|
WHERE s.workspace_id = workspace_agent_turns.workspace_id
|
|
AND s.agent_session_id = workspace_agent_turns.agent_session_id
|
|
)
|
|
WHERE backfilled = 1
|
|
AND EXISTS (
|
|
SELECT 1
|
|
FROM workspace_agent_sessions s
|
|
WHERE s.workspace_id = workspace_agent_turns.workspace_id
|
|
AND s.agent_session_id = workspace_agent_turns.agent_session_id
|
|
AND s.status IN ('failed', 'canceled')
|
|
)
|
|
AND NOT EXISTS (
|
|
SELECT 1
|
|
FROM workspace_agent_turns newer
|
|
WHERE newer.workspace_id = workspace_agent_turns.workspace_id
|
|
AND newer.agent_session_id = workspace_agent_turns.agent_session_id
|
|
AND newer.backfilled = 1
|
|
AND (
|
|
newer.settled_at_unix_ms > workspace_agent_turns.settled_at_unix_ms
|
|
OR (
|
|
newer.settled_at_unix_ms = workspace_agent_turns.settled_at_unix_ms
|
|
AND newer.turn_id > workspace_agent_turns.turn_id
|
|
)
|
|
)
|
|
)
|
|
`); err != nil {
|
|
return fmt.Errorf("repair backfilled workspace agent turn outcomes: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func hasColumnTx(ctx context.Context, tx *sql.Tx, tableName string, columnName string) (bool, error) {
|
|
rows, err := tx.QueryContext(ctx, `SELECT name FROM pragma_table_info(?)`, tableName)
|
|
if err != nil {
|
|
return false, fmt.Errorf("inspect table %s columns: %w", tableName, err)
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var name string
|
|
if err := rows.Scan(&name); err != nil {
|
|
return false, fmt.Errorf("scan table %s column: %w", tableName, err)
|
|
}
|
|
if name == columnName {
|
|
return true, nil
|
|
}
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return false, fmt.Errorf("inspect table %s columns: %w", tableName, err)
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
// applyWorkspaceAgentActivityMessagesV2 rebuilds workspace_agent_messages so
|
|
// message ownership is an explicit choice (protocol v2 rule eight): turn_id
|
|
// is either a non-empty turn reference or NULL for session-level messages.
|
|
// Historical empty strings are normalized to NULL — "attribution unknown" is
|
|
// honestly expressed as session-level instead of faked.
|
|
func (s *Store) applyWorkspaceAgentActivityMessagesV2(ctx context.Context) error {
|
|
applied, err := s.hasMigration(ctx, schemaMigrationWorkspaceAgentActivityMessagesV2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if applied {
|
|
return nil
|
|
}
|
|
if err := s.ensureWorkspaceAgentMessageSemanticsColumn(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
return s.rebuildWorkspaceAgentMessagesV2(ctx)
|
|
}
|
|
|
|
// rebuildWorkspaceAgentMessagesV2 performs the table rebuild on a dedicated
|
|
// connection and releases it before returning, so the migration ledger write
|
|
// that follows can obtain a pool connection even with a single-connection
|
|
// pool.
|
|
func (s *Store) rebuildWorkspaceAgentMessagesV2(ctx context.Context) error {
|
|
conn, err := s.db.Conn(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("open connection for workspace agent messages v2: %w", err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
// The rebuild follows the documented SQLite table-rebuild procedure:
|
|
// disable FK enforcement on this connection, rebuild inside one
|
|
// transaction, re-enable enforcement.
|
|
if _, err := conn.ExecContext(ctx, `PRAGMA foreign_keys = OFF;`); err != nil {
|
|
return fmt.Errorf("disable foreign keys for workspace agent messages v2: %w", err)
|
|
}
|
|
defer func() {
|
|
_, _ = conn.ExecContext(ctx, `PRAGMA foreign_keys = ON;`)
|
|
}()
|
|
|
|
return runInConnTx(ctx, conn, func(tx *sql.Tx) error {
|
|
if _, err := tx.ExecContext(ctx, `
|
|
CREATE TABLE workspace_agent_messages_v2 (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
workspace_id TEXT NOT NULL,
|
|
agent_session_id TEXT NOT NULL,
|
|
message_id TEXT NOT NULL CHECK (length(message_id) > 0),
|
|
version INTEGER NOT NULL CHECK (version > 0),
|
|
turn_id TEXT CHECK (turn_id IS NULL OR length(turn_id) > 0),
|
|
role TEXT NOT NULL CHECK (length(role) > 0),
|
|
kind TEXT NOT NULL CHECK (length(kind) > 0),
|
|
status TEXT NOT NULL DEFAULT '',
|
|
semantics_json TEXT NOT NULL DEFAULT 'null' CHECK (json_valid(semantics_json)),
|
|
payload_json TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(payload_json) AND json_type(payload_json) = 'object'),
|
|
occurred_at_unix_ms INTEGER NOT NULL DEFAULT 0 CHECK (occurred_at_unix_ms >= 0),
|
|
started_at_unix_ms INTEGER NOT NULL DEFAULT 0 CHECK (started_at_unix_ms >= 0),
|
|
completed_at_unix_ms INTEGER NOT NULL DEFAULT 0 CHECK (completed_at_unix_ms >= 0),
|
|
deleted_at_unix_ms INTEGER NOT NULL DEFAULT 0 CHECK (deleted_at_unix_ms >= 0),
|
|
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),
|
|
UNIQUE (workspace_id, agent_session_id, message_id),
|
|
FOREIGN KEY (workspace_id, agent_session_id)
|
|
REFERENCES workspace_agent_sessions(workspace_id, agent_session_id)
|
|
ON DELETE CASCADE,
|
|
FOREIGN KEY (workspace_id, agent_session_id, turn_id)
|
|
REFERENCES workspace_agent_turns(workspace_id, agent_session_id, turn_id)
|
|
);
|
|
`); err != nil {
|
|
return fmt.Errorf("create workspace agent messages v2 table: %w", err)
|
|
}
|
|
if _, err := tx.ExecContext(ctx, `
|
|
INSERT INTO workspace_agent_messages_v2 (
|
|
id, 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,
|
|
deleted_at_unix_ms, created_at_unix_ms, updated_at_unix_ms
|
|
)
|
|
SELECT
|
|
id, workspace_id, agent_session_id, message_id, version,
|
|
CASE
|
|
WHEN NULLIF(TRIM(turn_id), '') IS NULL THEN NULL
|
|
WHEN EXISTS (
|
|
SELECT 1
|
|
FROM workspace_agent_turns turn_parent
|
|
WHERE turn_parent.workspace_id = workspace_agent_messages.workspace_id
|
|
AND turn_parent.agent_session_id = workspace_agent_messages.agent_session_id
|
|
AND turn_parent.turn_id = TRIM(workspace_agent_messages.turn_id)
|
|
) THEN TRIM(turn_id)
|
|
ELSE NULL
|
|
END,
|
|
role, kind, status,
|
|
semantics_json, payload_json, occurred_at_unix_ms, started_at_unix_ms, completed_at_unix_ms,
|
|
deleted_at_unix_ms, created_at_unix_ms, updated_at_unix_ms
|
|
FROM workspace_agent_messages
|
|
`); err != nil {
|
|
return fmt.Errorf("copy workspace agent messages into v2 table: %w", err)
|
|
}
|
|
if _, err := tx.ExecContext(ctx, `DROP TABLE workspace_agent_messages;`); err != nil {
|
|
return fmt.Errorf("drop legacy workspace agent messages table: %w", err)
|
|
}
|
|
if _, err := tx.ExecContext(ctx, `ALTER TABLE workspace_agent_messages_v2 RENAME TO workspace_agent_messages;`); err != nil {
|
|
return fmt.Errorf("rename workspace agent messages v2 table: %w", err)
|
|
}
|
|
if _, err := tx.ExecContext(ctx, `
|
|
CREATE INDEX IF NOT EXISTS idx_workspace_agent_messages_session_version
|
|
ON workspace_agent_messages(workspace_id, agent_session_id, deleted_at_unix_ms, version);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_workspace_agent_messages_session_display
|
|
ON workspace_agent_messages(workspace_id, agent_session_id, deleted_at_unix_ms, id);
|
|
`); err != nil {
|
|
return fmt.Errorf("recreate workspace agent message indexes: %w", err)
|
|
}
|
|
return recordMigrationTx(ctx, tx, schemaMigrationWorkspaceAgentActivityMessagesV2)
|
|
})
|
|
}
|
|
|
|
// applyWorkspaceAgentActivityTurnIntegrityV1 repairs child references written
|
|
// by the original messages-v2 rebuild and adds the index used by conversation
|
|
// rail latest-turn ordering.
|
|
func (s *Store) applyWorkspaceAgentActivityTurnIntegrityV1(ctx context.Context) error {
|
|
applied, err := s.hasMigration(ctx, schemaMigrationWorkspaceAgentActivityTurnIntegrityV1)
|
|
if err != nil || applied {
|
|
return err
|
|
}
|
|
tx, err := s.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("begin workspace agent activity turn integrity v1: %w", err)
|
|
}
|
|
committed := false
|
|
defer func() {
|
|
if !committed {
|
|
_ = tx.Rollback()
|
|
}
|
|
}()
|
|
if _, err := tx.ExecContext(ctx, `
|
|
UPDATE workspace_agent_messages
|
|
SET turn_id = NULL
|
|
WHERE turn_id IS NOT NULL
|
|
AND NOT EXISTS (
|
|
SELECT 1
|
|
FROM workspace_agent_turns turn_parent
|
|
WHERE turn_parent.workspace_id = workspace_agent_messages.workspace_id
|
|
AND turn_parent.agent_session_id = workspace_agent_messages.agent_session_id
|
|
AND turn_parent.turn_id = workspace_agent_messages.turn_id
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_workspace_agent_turns_session_latest
|
|
ON workspace_agent_turns(
|
|
workspace_id,
|
|
agent_session_id,
|
|
updated_at_unix_ms DESC,
|
|
created_at_unix_ms DESC,
|
|
started_at_unix_ms DESC,
|
|
turn_id DESC
|
|
);
|
|
`); err != nil {
|
|
return fmt.Errorf("apply workspace agent activity turn integrity v1: %w", err)
|
|
}
|
|
if err := recordMigrationTx(ctx, tx, schemaMigrationWorkspaceAgentActivityTurnIntegrityV1); err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
return fmt.Errorf("commit workspace agent activity turn integrity v1: %w", err)
|
|
}
|
|
committed = true
|
|
return nil
|
|
}
|
|
|
|
func runInConnTx(ctx context.Context, conn *sql.Conn, fn func(*sql.Tx) error) error {
|
|
tx, err := conn.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("begin workspace agent messages v2 rebuild: %w", err)
|
|
}
|
|
committed := false
|
|
defer func() {
|
|
if !committed {
|
|
_ = tx.Rollback()
|
|
}
|
|
}()
|
|
if err := fn(tx); err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
return fmt.Errorf("commit workspace agent messages v2 rebuild: %w", err)
|
|
}
|
|
committed = true
|
|
return nil
|
|
}
|