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
347 lines
12 KiB
Go
347 lines
12 KiB
Go
package storesqlite
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
)
|
|
|
|
func (s *Store) applyWorkspaceAgentActivityV1(ctx context.Context) error {
|
|
applied, err := s.hasMigration(ctx, schemaMigrationWorkspaceAgentActivityV1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if applied {
|
|
return nil
|
|
}
|
|
|
|
// Unlike the pre-extraction tuttid schema, workspace_agent_sessions has
|
|
// no foreign key into a host workspaces table; hosts delete a
|
|
// workspace's rows explicitly via ClearSessions.
|
|
_, err = s.db.ExecContext(ctx, `
|
|
CREATE TABLE IF NOT EXISTS workspace_agent_sessions (
|
|
workspace_id TEXT NOT NULL,
|
|
agent_session_id TEXT NOT NULL,
|
|
origin TEXT NOT NULL DEFAULT '',
|
|
user_id TEXT NOT NULL DEFAULT '',
|
|
agent_target_id TEXT,
|
|
provider TEXT NOT NULL DEFAULT '',
|
|
provider_session_id TEXT NOT NULL DEFAULT '',
|
|
model TEXT NOT NULL DEFAULT '',
|
|
settings_json TEXT NOT NULL DEFAULT '{}',
|
|
runtime_context_json TEXT NOT NULL DEFAULT '{}',
|
|
cwd TEXT NOT NULL DEFAULT '',
|
|
title TEXT NOT NULL DEFAULT '',
|
|
status TEXT NOT NULL DEFAULT '',
|
|
current_phase TEXT NOT NULL DEFAULT '',
|
|
last_error TEXT NOT NULL DEFAULT '',
|
|
message_version INTEGER NOT NULL DEFAULT 0,
|
|
last_event_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
started_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
ended_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
deleted_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
created_at_unix_ms INTEGER NOT NULL,
|
|
updated_at_unix_ms INTEGER NOT NULL,
|
|
PRIMARY KEY (workspace_id, agent_session_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_workspace_agent_sessions_workspace_updated
|
|
ON workspace_agent_sessions(workspace_id, deleted_at_unix_ms, updated_at_unix_ms);
|
|
|
|
CREATE TABLE IF NOT EXISTS workspace_agent_messages (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
workspace_id TEXT NOT NULL,
|
|
agent_session_id TEXT NOT NULL,
|
|
message_id TEXT NOT NULL,
|
|
version INTEGER NOT NULL,
|
|
turn_id TEXT NOT NULL DEFAULT '',
|
|
role TEXT NOT NULL DEFAULT '',
|
|
kind TEXT NOT NULL DEFAULT '',
|
|
status TEXT NOT NULL DEFAULT '',
|
|
payload_json TEXT NOT NULL DEFAULT '{}',
|
|
occurred_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
started_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
completed_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
deleted_at_unix_ms INTEGER NOT NULL DEFAULT 0,
|
|
created_at_unix_ms INTEGER NOT NULL,
|
|
updated_at_unix_ms INTEGER NOT NULL,
|
|
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
|
|
);
|
|
|
|
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);
|
|
`)
|
|
if err != nil {
|
|
return fmt.Errorf("migrate workspace database agent activity v1: %w", err)
|
|
}
|
|
|
|
return s.recordMigration(ctx, schemaMigrationWorkspaceAgentActivityV1)
|
|
}
|
|
|
|
func (s *Store) applyWorkspaceAgentActivityV2(ctx context.Context) error {
|
|
applied, err := s.hasMigration(ctx, schemaMigrationWorkspaceAgentActivityV2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if applied {
|
|
return nil
|
|
}
|
|
|
|
hasSettings, err := s.hasColumn(ctx, "workspace_agent_sessions", "settings_json")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
hasRuntimeContext, err := s.hasColumn(ctx, "workspace_agent_sessions", "runtime_context_json")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !hasSettings {
|
|
if _, err := s.db.ExecContext(ctx, `ALTER TABLE workspace_agent_sessions ADD COLUMN settings_json TEXT NOT NULL DEFAULT '{}';`); err != nil {
|
|
return fmt.Errorf("migrate workspace agent activity to v2 settings: %w", err)
|
|
}
|
|
}
|
|
if !hasRuntimeContext {
|
|
if _, err := s.db.ExecContext(ctx, `ALTER TABLE workspace_agent_sessions ADD COLUMN runtime_context_json TEXT NOT NULL DEFAULT '{}';`); err != nil {
|
|
return fmt.Errorf("migrate workspace agent activity to v2 runtime context: %w", err)
|
|
}
|
|
}
|
|
return s.recordMigration(ctx, schemaMigrationWorkspaceAgentActivityV2)
|
|
}
|
|
|
|
func (s *Store) applyWorkspaceAgentActivityV3(ctx context.Context) error {
|
|
applied, err := s.hasMigration(ctx, schemaMigrationWorkspaceAgentActivityV3)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if applied {
|
|
return nil
|
|
}
|
|
|
|
hasPinnedAt, err := s.hasColumn(ctx, "workspace_agent_sessions", "pinned_at_unix_ms")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !hasPinnedAt {
|
|
if _, err := s.db.ExecContext(ctx, `ALTER TABLE workspace_agent_sessions ADD COLUMN pinned_at_unix_ms INTEGER NOT NULL DEFAULT 0;`); err != nil {
|
|
return fmt.Errorf("migrate workspace agent activity to v3 pinned state: %w", err)
|
|
}
|
|
}
|
|
return s.recordMigration(ctx, schemaMigrationWorkspaceAgentActivityV3)
|
|
}
|
|
|
|
func (s *Store) applyWorkspaceAgentActivityV4(ctx context.Context) error {
|
|
applied, err := s.hasMigration(ctx, schemaMigrationWorkspaceAgentActivityV4)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// The column is repaired even when the migration is already recorded:
|
|
// some databases carry the v4 marker without the column.
|
|
hasAgentTargetID, err := s.hasColumn(ctx, "workspace_agent_sessions", "agent_target_id")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !hasAgentTargetID {
|
|
if _, err := s.db.ExecContext(ctx, `ALTER TABLE workspace_agent_sessions ADD COLUMN agent_target_id TEXT;`); err != nil {
|
|
return fmt.Errorf("migrate workspace agent activity to v4 agent target id: %w", err)
|
|
}
|
|
}
|
|
if applied {
|
|
return nil
|
|
}
|
|
return s.recordMigration(ctx, schemaMigrationWorkspaceAgentActivityV4)
|
|
}
|
|
|
|
func (s *Store) applyWorkspaceAgentActivityV5(ctx context.Context) error {
|
|
applied, err := s.hasMigration(ctx, schemaMigrationWorkspaceAgentActivityV5)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if applied {
|
|
return nil
|
|
}
|
|
|
|
if err := s.backfillSystemAgentTargetIDs(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
return s.recordMigration(ctx, schemaMigrationWorkspaceAgentActivityV5)
|
|
}
|
|
|
|
func (s *Store) applyWorkspaceAgentActivityV6(ctx context.Context) error {
|
|
applied, err := s.hasMigration(ctx, schemaMigrationWorkspaceAgentActivityV6)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
hasUserID, err := s.hasColumn(ctx, "workspace_agent_sessions", "user_id")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !hasUserID {
|
|
if _, err := s.db.ExecContext(ctx, `ALTER TABLE workspace_agent_sessions ADD COLUMN user_id TEXT NOT NULL DEFAULT '';`); err != nil {
|
|
return fmt.Errorf("migrate workspace agent activity to v6 user id: %w", err)
|
|
}
|
|
}
|
|
if applied {
|
|
return nil
|
|
}
|
|
return s.recordMigration(ctx, schemaMigrationWorkspaceAgentActivityV6)
|
|
}
|
|
|
|
// workspaceAgentActivityV7BackfillProviders are the providers whose
|
|
// TargetIDBackfillByProvider entry shipped after the one-time v5 backfill was
|
|
// already recorded on existing databases. v5 never replays (its marker is
|
|
// claimed from the legacy ledger), so without this step legacy sessions of a
|
|
// later-added provider keep an empty agent_target_id forever — and the
|
|
// server-side paged section queries, which filter strictly by
|
|
// agent_target_id, silently exclude them from the provider's tab.
|
|
var workspaceAgentActivityV7BackfillProviders = []string{"cursor"}
|
|
|
|
func (s *Store) applyWorkspaceAgentActivityV7(ctx context.Context) error {
|
|
applied, err := s.hasMigration(ctx, schemaMigrationWorkspaceAgentActivityV7)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if applied {
|
|
return nil
|
|
}
|
|
|
|
for _, provider := range workspaceAgentActivityV7BackfillProviders {
|
|
targetID := s.opts.TargetIDBackfillByProvider[provider]
|
|
if targetID == "" {
|
|
continue
|
|
}
|
|
if err := s.backfillAgentTargetIDForProvider(ctx, provider, targetID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return s.recordMigration(ctx, schemaMigrationWorkspaceAgentActivityV7)
|
|
}
|
|
|
|
func (s *Store) applyWorkspaceAgentActivityV8(ctx context.Context) error {
|
|
applied, err := s.hasMigration(ctx, schemaMigrationWorkspaceAgentActivityV8)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if applied {
|
|
return nil
|
|
}
|
|
|
|
if _, err := s.db.ExecContext(ctx, `
|
|
CREATE INDEX IF NOT EXISTS idx_workspace_agent_sessions_pinned_page
|
|
ON workspace_agent_sessions(workspace_id, deleted_at_unix_ms, pinned_at_unix_ms DESC, agent_session_id ASC);
|
|
`); err != nil {
|
|
return fmt.Errorf("migrate workspace agent activity to v8 pinned pagination index: %w", err)
|
|
}
|
|
|
|
return s.recordMigration(ctx, schemaMigrationWorkspaceAgentActivityV8)
|
|
}
|
|
|
|
// applyWorkspaceAgentActivityV9 restores the pinned-page index after the v3
|
|
// session-entity table rebuild. It runs after that rebuild for both fresh and
|
|
// upgraded databases, including databases that already recorded activity v8.
|
|
func (s *Store) applyWorkspaceAgentActivityV9(ctx context.Context) error {
|
|
applied, err := s.hasMigration(ctx, schemaMigrationWorkspaceAgentActivityV9)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Reassert the physical index even when the marker exists. Databases opened
|
|
// by different builds can retain the marker after a table rebuild drops it.
|
|
if _, err := s.db.ExecContext(ctx, `
|
|
CREATE INDEX IF NOT EXISTS idx_workspace_agent_sessions_pinned_page
|
|
ON workspace_agent_sessions(workspace_id, deleted_at_unix_ms, pinned_at_unix_ms DESC, agent_session_id ASC);
|
|
`); err != nil {
|
|
return fmt.Errorf("migrate workspace agent activity to v9 pinned pagination index: %w", err)
|
|
}
|
|
if applied {
|
|
return nil
|
|
}
|
|
|
|
return s.recordMigration(ctx, schemaMigrationWorkspaceAgentActivityV9)
|
|
}
|
|
|
|
// applyWorkspaceAgentActivityV10 adds target-scoped rail indexes. Provider
|
|
// rail switches must narrow by target inside the index instead of scanning all
|
|
// providers in each requested section and filtering rows afterwards.
|
|
func (s *Store) applyWorkspaceAgentActivityV10(ctx context.Context) error {
|
|
applied, err := s.hasMigration(ctx, schemaMigrationWorkspaceAgentActivityV10)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Keep the target-scoped query contract self-healing for the same
|
|
// marker-without-index schema drift covered by v9.
|
|
if _, err := s.db.ExecContext(ctx, `
|
|
CREATE INDEX IF NOT EXISTS idx_workspace_agent_sessions_rail_section_target_page
|
|
ON workspace_agent_sessions(workspace_id, rail_section_key, agent_target_id, deleted_at_unix_ms, pinned_at_unix_ms, agent_session_id);
|
|
CREATE INDEX IF NOT EXISTS idx_workspace_agent_sessions_pinned_target_page
|
|
ON workspace_agent_sessions(workspace_id, agent_target_id, deleted_at_unix_ms, pinned_at_unix_ms DESC, agent_session_id ASC);
|
|
`); err != nil {
|
|
return fmt.Errorf("migrate workspace agent activity to v10 target-scoped rail indexes: %w", err)
|
|
}
|
|
if applied {
|
|
return nil
|
|
}
|
|
|
|
return s.recordMigration(ctx, schemaMigrationWorkspaceAgentActivityV10)
|
|
}
|
|
|
|
// applyWorkspaceAgentActivityV11 supports project removal, whose ownership
|
|
// query starts from a project rail section across all workspaces. Existing
|
|
// workspace-prefixed rail indexes cannot serve that access path.
|
|
func (s *Store) applyWorkspaceAgentActivityV11(ctx context.Context) error {
|
|
applied, err := s.hasMigration(ctx, schemaMigrationWorkspaceAgentActivityV11)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := s.db.ExecContext(ctx, `
|
|
CREATE INDEX IF NOT EXISTS idx_workspace_agent_sessions_project_removal
|
|
ON workspace_agent_sessions(rail_section_key, session_kind, deleted_at_unix_ms, pinned_at_unix_ms, workspace_id, agent_session_id);
|
|
`); err != nil {
|
|
return fmt.Errorf("migrate workspace agent activity to v11 project removal index: %w", err)
|
|
}
|
|
if applied {
|
|
return nil
|
|
}
|
|
return s.recordMigration(ctx, schemaMigrationWorkspaceAgentActivityV11)
|
|
}
|
|
|
|
func (s *Store) backfillSystemAgentTargetIDs(ctx context.Context) error {
|
|
providers := make([]string, 0, len(s.opts.TargetIDBackfillByProvider))
|
|
for provider := range s.opts.TargetIDBackfillByProvider {
|
|
providers = append(providers, provider)
|
|
}
|
|
sort.Strings(providers)
|
|
for _, provider := range providers {
|
|
if err := s.backfillAgentTargetIDForProvider(ctx, provider, s.opts.TargetIDBackfillByProvider[provider]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) backfillAgentTargetIDForProvider(ctx context.Context, provider string, targetID string) error {
|
|
if _, err := s.db.ExecContext(ctx, `
|
|
UPDATE workspace_agent_sessions
|
|
SET agent_target_id = ?
|
|
WHERE (agent_target_id IS NULL OR TRIM(agent_target_id) = '')
|
|
AND provider = ?
|
|
`, targetID, provider); err != nil {
|
|
return fmt.Errorf("backfill %s agent target ids: %w", provider, err)
|
|
}
|
|
return nil
|
|
}
|