feat(memory/migrations): add Goose migrations 007-010

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.
This commit is contained in:
ZanzyTHEbar 2026-02-18 23:40:41 +00:00
parent 626308ad95
commit 4b1567f08b
4 changed files with 325 additions and 0 deletions

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}