From 4b1567f08ba21edfffe563f02a62bec8edecc6d3 Mon Sep 17 00:00:00 2001 From: ZanzyTHEbar Date: Wed, 18 Feb 2026 23:40:41 +0000 Subject: [PATCH] feat(memory/migrations): add Goose migrations 007-010 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 007 — agent_conversations + agent_messages (conversation turns) 008 — agent_runs + agent_run_states + agent_state_transitions + agent_checkpoints + agent_tool_results (full runtime state) 009 — jobs queue with kind+dedupe_key partial unique index 010 — conversation graph: forks, links, threads, thread_messages, mentions, message_revisions All tables use BLOB PRIMARY KEY (UUIDv7), CASCADE deletes on FK, and ISO8601 DATETIME defaults via strftime('%Y-%m-%dT%H:%M:%fZ'). Down migrations drop all created tables in reverse dependency order. --- .../migrations/007_agent_conversations.go | 53 +++++++++ .../migrations/008_agent_runtime_state.go | 107 +++++++++++++++++ pkg/memory/migrations/009_jobs.go | 54 +++++++++ .../migrations/010_conversation_graph.go | 111 ++++++++++++++++++ 4 files changed, 325 insertions(+) create mode 100644 pkg/memory/migrations/007_agent_conversations.go create mode 100644 pkg/memory/migrations/008_agent_runtime_state.go create mode 100644 pkg/memory/migrations/009_jobs.go create mode 100644 pkg/memory/migrations/010_conversation_graph.go diff --git a/pkg/memory/migrations/007_agent_conversations.go b/pkg/memory/migrations/007_agent_conversations.go new file mode 100644 index 000000000..1102442a8 --- /dev/null +++ b/pkg/memory/migrations/007_agent_conversations.go @@ -0,0 +1,53 @@ +package migrations + +import ( + "context" + "database/sql" + "fmt" + + "github.com/pressly/goose/v3" +) + +func init() { + goose.AddMigrationContext(up007AgentConversations, down007AgentConversations) +} + +func up007AgentConversations(_ context.Context, tx *sql.Tx) error { + stmts := []string{ + `CREATE TABLE IF NOT EXISTS agent_conversations ( + id BLOB PRIMARY KEY, + title TEXT, + created_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + updated_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) +)`, + `CREATE TABLE IF NOT EXISTS agent_messages ( + id BLOB PRIMARY KEY, + conversation_id BLOB NOT NULL REFERENCES agent_conversations(id) ON DELETE CASCADE, + role TEXT NOT NULL, + content TEXT NOT NULL, + metadata_json JSON NOT NULL DEFAULT '{}', + created_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + updated_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) +)`, + `CREATE INDEX IF NOT EXISTS idx_agent_messages_conversation_created_at ON agent_messages(conversation_id, created_at)`, + } + for _, s := range stmts { + if _, err := tx.ExecContext(context.Background(), s); err != nil { + return fmt.Errorf("007_agent_conversations up: %w\nSQL: %s", err, s) + } + } + return nil +} + +func down007AgentConversations(_ context.Context, tx *sql.Tx) error { + stmts := []string{ + `DROP TABLE IF EXISTS agent_messages`, + `DROP TABLE IF EXISTS agent_conversations`, + } + for _, s := range stmts { + if _, err := tx.ExecContext(context.Background(), s); err != nil { + return fmt.Errorf("007_agent_conversations down: %w\nSQL: %s", err, s) + } + } + return nil +} diff --git a/pkg/memory/migrations/008_agent_runtime_state.go b/pkg/memory/migrations/008_agent_runtime_state.go new file mode 100644 index 000000000..c08146227 --- /dev/null +++ b/pkg/memory/migrations/008_agent_runtime_state.go @@ -0,0 +1,107 @@ +package migrations + +import ( + "context" + "database/sql" + "fmt" + + "github.com/pressly/goose/v3" +) + +func init() { + goose.AddMigrationContext(up008AgentRuntimeState, down008AgentRuntimeState) +} + +func up008AgentRuntimeState(_ context.Context, tx *sql.Tx) error { + stmts := []string{ + `CREATE TABLE IF NOT EXISTS agent_runs ( + id BLOB PRIMARY KEY, + conversation_id BLOB NOT NULL REFERENCES agent_conversations(id) ON DELETE CASCADE, + status TEXT NOT NULL DEFAULT 'running', + metadata_json JSON NOT NULL DEFAULT '{}', + created_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + updated_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) +)`, + `CREATE INDEX IF NOT EXISTS idx_agent_runs_conversation_created_at ON agent_runs(conversation_id, created_at)`, + + `CREATE TABLE IF NOT EXISTS agent_run_states ( + id BLOB PRIMARY KEY, + run_id BLOB NOT NULL REFERENCES agent_runs(id) ON DELETE CASCADE, + step_index INTEGER NOT NULL, + state TEXT NOT NULL, + snapshot_json JSON NOT NULL DEFAULT '{}', + created_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + updated_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + UNIQUE(run_id, step_index) +)`, + `CREATE INDEX IF NOT EXISTS idx_agent_run_states_run_step ON agent_run_states(run_id, step_index)`, + + `CREATE TABLE IF NOT EXISTS agent_state_transitions ( + id BLOB PRIMARY KEY, + run_id BLOB NOT NULL REFERENCES agent_runs(id) ON DELETE CASCADE, + step_index INTEGER NOT NULL, + from_state TEXT NOT NULL, + to_state TEXT NOT NULL, + trigger TEXT NOT NULL, + at DATETIME NOT NULL, + meta_json JSON NOT NULL DEFAULT '{}', + error TEXT, + created_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + updated_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) +)`, + `CREATE INDEX IF NOT EXISTS idx_agent_state_transitions_run_step_at ON agent_state_transitions(run_id, step_index, at)`, + + `CREATE TABLE IF NOT EXISTS agent_checkpoints ( + id BLOB PRIMARY KEY, + conversation_id BLOB NOT NULL REFERENCES agent_conversations(id) ON DELETE CASCADE, + name TEXT NOT NULL, + run_state_id BLOB NOT NULL REFERENCES agent_run_states(id) ON DELETE RESTRICT, + metadata_json JSON NOT NULL DEFAULT '{}', + created_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + updated_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + UNIQUE(conversation_id, name) +)`, + `CREATE INDEX IF NOT EXISTS idx_agent_checkpoints_conversation_created_at ON agent_checkpoints(conversation_id, created_at)`, + + `CREATE TABLE IF NOT EXISTS agent_tool_results ( + id BLOB PRIMARY KEY, + conversation_id BLOB NOT NULL REFERENCES agent_conversations(id) ON DELETE CASCADE, + run_id BLOB NOT NULL REFERENCES agent_runs(id) ON DELETE CASCADE, + step_index INTEGER NOT NULL, + tool_call_id TEXT NOT NULL, + tool_name TEXT NOT NULL, + full_key TEXT NOT NULL, + preview TEXT, + chunk_count INTEGER NOT NULL DEFAULT 0, + metadata_json JSON NOT NULL DEFAULT '{}', + created_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + updated_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + UNIQUE(run_id, tool_call_id) +)`, + `CREATE INDEX IF NOT EXISTS idx_agent_tool_results_conversation_created_at ON agent_tool_results(conversation_id, created_at)`, + `CREATE INDEX IF NOT EXISTS idx_agent_tool_results_run_step ON agent_tool_results(run_id, step_index)`, + `CREATE INDEX IF NOT EXISTS idx_agent_tool_results_tool_name ON agent_tool_results(tool_name)`, + } + for _, s := range stmts { + if _, err := tx.ExecContext(context.Background(), s); err != nil { + return fmt.Errorf("008_agent_runtime_state up: %w\nSQL: %s", err, s) + } + } + return nil +} + +func down008AgentRuntimeState(_ context.Context, tx *sql.Tx) error { + stmts := []string{ + `DROP TABLE IF EXISTS agent_tool_results`, + `DROP TABLE IF EXISTS agent_checkpoints`, + `DROP TABLE IF EXISTS agent_state_transitions`, + `DROP TABLE IF EXISTS agent_run_states`, + `DROP TABLE IF EXISTS agent_runs`, + } + for _, s := range stmts { + if _, err := tx.ExecContext(context.Background(), s); err != nil { + return fmt.Errorf("008_agent_runtime_state down: %w\nSQL: %s", err, s) + } + } + return nil +} diff --git a/pkg/memory/migrations/009_jobs.go b/pkg/memory/migrations/009_jobs.go new file mode 100644 index 000000000..21674b917 --- /dev/null +++ b/pkg/memory/migrations/009_jobs.go @@ -0,0 +1,54 @@ +package migrations + +import ( + "context" + "database/sql" + "fmt" + + "github.com/pressly/goose/v3" +) + +func init() { + goose.AddMigrationContext(up009Jobs, down009Jobs) +} + +func up009Jobs(_ context.Context, tx *sql.Tx) error { + stmts := []string{ + `CREATE TABLE IF NOT EXISTS jobs ( + id BLOB PRIMARY KEY, + kind TEXT NOT NULL, + status TEXT NOT NULL, + run_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + attempts INTEGER NOT NULL DEFAULT 0, + max_attempts INTEGER NOT NULL DEFAULT 3, + locked_at DATETIME, + locked_by TEXT, + payload_json JSON NOT NULL DEFAULT '{}', + dedupe_key TEXT, + last_error TEXT, + created_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + updated_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + completed_at DATETIME +)`, + `CREATE INDEX IF NOT EXISTS idx_jobs_status_run_at ON jobs(status, run_at)`, + `CREATE UNIQUE INDEX IF NOT EXISTS idx_jobs_kind_dedupe ON jobs(kind, dedupe_key) WHERE dedupe_key IS NOT NULL`, + } + for _, s := range stmts { + if _, err := tx.ExecContext(context.Background(), s); err != nil { + return fmt.Errorf("009_jobs up: %w\nSQL: %s", err, s) + } + } + return nil +} + +func down009Jobs(_ context.Context, tx *sql.Tx) error { + stmts := []string{ + `DROP TABLE IF EXISTS jobs`, + } + for _, s := range stmts { + if _, err := tx.ExecContext(context.Background(), s); err != nil { + return fmt.Errorf("009_jobs down: %w\nSQL: %s", err, s) + } + } + return nil +} diff --git a/pkg/memory/migrations/010_conversation_graph.go b/pkg/memory/migrations/010_conversation_graph.go new file mode 100644 index 000000000..61b6d7aa6 --- /dev/null +++ b/pkg/memory/migrations/010_conversation_graph.go @@ -0,0 +1,111 @@ +package migrations + +import ( + "context" + "database/sql" + "fmt" + + "github.com/pressly/goose/v3" +) + +func init() { + goose.AddMigrationContext(up010ConversationGraph, down010ConversationGraph) +} + +func up010ConversationGraph(_ context.Context, tx *sql.Tx) error { + stmts := []string{ + `CREATE TABLE IF NOT EXISTS agent_conversation_forks ( + id BLOB PRIMARY KEY, + parent_conversation_id BLOB NOT NULL REFERENCES agent_conversations(id) ON DELETE CASCADE, + child_conversation_id BLOB NOT NULL REFERENCES agent_conversations(id) ON DELETE CASCADE, + checkpoint_id BLOB NOT NULL REFERENCES agent_checkpoints(id) ON DELETE RESTRICT, + metadata_json JSON NOT NULL DEFAULT '{}', + created_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + updated_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + UNIQUE(child_conversation_id) +)`, + `CREATE INDEX IF NOT EXISTS idx_agent_conversation_forks_parent_created_at ON agent_conversation_forks(parent_conversation_id, created_at DESC)`, + + `CREATE TABLE IF NOT EXISTS agent_conversation_links ( + id BLOB PRIMARY KEY, + conversation_id BLOB NOT NULL REFERENCES agent_conversations(id) ON DELETE CASCADE, + linked_conversation_id BLOB NOT NULL REFERENCES agent_conversations(id) ON DELETE CASCADE, + kind TEXT NOT NULL DEFAULT 'merge', + metadata_json JSON NOT NULL DEFAULT '{}', + created_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + updated_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + UNIQUE(conversation_id, linked_conversation_id, kind) +)`, + `CREATE INDEX IF NOT EXISTS idx_agent_conversation_links_conversation_id_created_at ON agent_conversation_links(conversation_id, created_at DESC)`, + + `CREATE TABLE IF NOT EXISTS agent_threads ( + id BLOB PRIMARY KEY, + conversation_id BLOB NOT NULL REFERENCES agent_conversations(id) ON DELETE CASCADE, + title TEXT, + metadata_json JSON NOT NULL DEFAULT '{}', + created_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + updated_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) +)`, + `CREATE INDEX IF NOT EXISTS idx_agent_threads_conversation_id_created_at ON agent_threads(conversation_id, created_at DESC)`, + + `CREATE TABLE IF NOT EXISTS agent_thread_messages ( + id BLOB PRIMARY KEY, + thread_id BLOB NOT NULL REFERENCES agent_threads(id) ON DELETE CASCADE, + role TEXT NOT NULL, + content TEXT NOT NULL, + metadata_json JSON NOT NULL DEFAULT '{}', + created_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + updated_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) +)`, + `CREATE INDEX IF NOT EXISTS idx_agent_thread_messages_thread_id_created_at ON agent_thread_messages(thread_id, created_at ASC)`, + + `CREATE TABLE IF NOT EXISTS agent_mentions ( + id BLOB PRIMARY KEY, + conversation_id BLOB NOT NULL REFERENCES agent_conversations(id) ON DELETE CASCADE, + message_id BLOB REFERENCES agent_messages(id) ON DELETE SET NULL, + kind TEXT NOT NULL, + target_id BLOB NOT NULL, + raw TEXT NOT NULL, + metadata_json JSON NOT NULL DEFAULT '{}', + created_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + updated_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) +)`, + `CREATE INDEX IF NOT EXISTS idx_agent_mentions_conversation_id_created_at ON agent_mentions(conversation_id, created_at DESC)`, + `CREATE INDEX IF NOT EXISTS idx_agent_mentions_kind_target_id ON agent_mentions(kind, target_id)`, + + `CREATE TABLE IF NOT EXISTS agent_message_revisions ( + id BLOB PRIMARY KEY, + message_id BLOB NOT NULL REFERENCES agent_messages(id) ON DELETE CASCADE, + editor TEXT NOT NULL, + old_content TEXT NOT NULL, + new_content TEXT NOT NULL, + metadata_json JSON NOT NULL DEFAULT '{}', + created_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + updated_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) +)`, + `CREATE INDEX IF NOT EXISTS idx_agent_message_revisions_message_id_created_at ON agent_message_revisions(message_id, created_at DESC)`, + } + for _, s := range stmts { + if _, err := tx.ExecContext(context.Background(), s); err != nil { + return fmt.Errorf("010_conversation_graph up: %w\nSQL: %s", err, s) + } + } + return nil +} + +func down010ConversationGraph(_ context.Context, tx *sql.Tx) error { + stmts := []string{ + `DROP TABLE IF EXISTS agent_message_revisions`, + `DROP TABLE IF EXISTS agent_mentions`, + `DROP TABLE IF EXISTS agent_thread_messages`, + `DROP TABLE IF EXISTS agent_threads`, + `DROP TABLE IF EXISTS agent_conversation_links`, + `DROP TABLE IF EXISTS agent_conversation_forks`, + } + for _, s := range stmts { + if _, err := tx.ExecContext(context.Background(), s); err != nil { + return fmt.Errorf("010_conversation_graph down: %w\nSQL: %s", err, s) + } + } + return nil +}