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

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
}