#!/usr/bin/env bash set -euo pipefail DB_PATH=${1:-rah_trial.db} if [ ! -f "$DB_PATH" ]; then echo "Error: Database file not found: $DB_PATH" >&2 exit 1 fi if command -v brew >/dev/null 2>&1; then SQLITE_BIN="$(brew --prefix sqlite 2>/dev/null)/bin/sqlite3" [ -x "$SQLITE_BIN" ] || SQLITE_BIN="sqlite3" else SQLITE_BIN="sqlite3" fi echo "Using sqlite: $($SQLITE_BIN --version)" has_col() { local table=$1 col=$2 "$SQLITE_BIN" "$DB_PATH" -json \ "PRAGMA table_info($table);" | \ grep -q "\"name\":\s*\"$col\"" } has_table() { local table=$1 "$SQLITE_BIN" "$DB_PATH" -json \ "SELECT name FROM sqlite_master WHERE type='table' AND name='$table';" | \ grep -q "$table" } has_view() { local view=$1 "$SQLITE_BIN" "$DB_PATH" -json \ "SELECT name FROM sqlite_master WHERE type='view' AND name='$view';" | \ grep -q "$view" } has_trigger() { local trg=$1 "$SQLITE_BIN" "$DB_PATH" -json \ "SELECT name FROM sqlite_master WHERE type='trigger' AND name='$trg';" | \ grep -q "$trg" } echo "Ensuring agents table exists and orchestrator is seeded..." # Rename legacy helpers table if present if has_table helpers && ! has_table agents; then "$SQLITE_BIN" "$DB_PATH" "ALTER TABLE helpers RENAME TO agents;" fi if ! has_table agents; then "$SQLITE_BIN" "$DB_PATH" <<'SQL' CREATE TABLE agents ( id INTEGER PRIMARY KEY AUTOINCREMENT, key TEXT UNIQUE NOT NULL, display_name TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'executor', system_prompt TEXT NOT NULL, available_tools TEXT NOT NULL, model TEXT NOT NULL, description TEXT, enabled INTEGER DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, memory TEXT, prompts TEXT ); SQL fi if has_table agents && ! has_col agents role; then "$SQLITE_BIN" "$DB_PATH" "ALTER TABLE agents ADD COLUMN role TEXT NOT NULL DEFAULT 'executor';" fi if has_table agents && ! has_col agents prompts; then "$SQLITE_BIN" "$DB_PATH" "ALTER TABLE agents ADD COLUMN prompts TEXT DEFAULT '[]';" fi if has_table agents && ! has_col agents memory; then "$SQLITE_BIN" "$DB_PATH" "ALTER TABLE agents ADD COLUMN memory TEXT;" fi COUNT_AGENTS=$("$SQLITE_BIN" -readonly "$DB_PATH" "SELECT COUNT(*) FROM agents;" 2>/dev/null || echo 0) if [ "${COUNT_AGENTS:-0}" = "0" ]; then echo " Seeding default orchestrator agent (ra-h)..." NOW=$(date -u +"%Y-%m-%dT%H:%M:%SZ") TOOLS_JSON='["queryNodes","createNode","updateNode","createEdge","queryEdge","updateEdge","searchContentEmbeddings","webSearch","think","delegateToMiniRAH"]' PROMPTS_JSON='[{"id":"p_seed_0","name":"Summary of Focus","content":"Summarize the primary focused node clearly. Include 3–5 key points and cite [NODE:id:\"title\"]."},{"id":"p_seed_1","name":"Next Steps","content":"Propose 3 concrete next actions based on the focused nodes with references to [NODE:id:\"title\"]."}]' SYSTEM_PROMPT="You are ra-h, the main orchestrator for RA-H. Coordinate work, delegate to mini ra-hs when tasks can be isolated, and keep the conversation focused on the user's goals." ESCAPED_SYSTEM_PROMPT=${SYSTEM_PROMPT//\'/''} "$SQLITE_BIN" "$DB_PATH" < logs if needed if ! has_table logs && has_table memory; then echo "Dropping view memory_v (if exists) to unlock table rename..." "$SQLITE_BIN" "$DB_PATH" "DROP VIEW IF EXISTS memory_v;" echo "Renaming memory -> logs..." "$SQLITE_BIN" "$DB_PATH" "ALTER TABLE memory RENAME TO logs;" fi # logs table if ! has_table logs; then "$SQLITE_BIN" "$DB_PATH" <<'SQL' CREATE TABLE logs ( id INTEGER PRIMARY KEY, ts TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), table_name TEXT NOT NULL, action TEXT NOT NULL, row_id INTEGER NOT NULL, summary TEXT, enriched_summary TEXT, snapshot_json TEXT ); SQL fi # Add enriched_summary column if missing if has_table logs && ! has_col logs enriched_summary; then "$SQLITE_BIN" "$DB_PATH" "ALTER TABLE logs ADD COLUMN enriched_summary TEXT;" fi # indexes on logs (cleanup legacy names first) "$SQLITE_BIN" "$DB_PATH" <<'SQL' DROP INDEX IF EXISTS idx_memory_ts; DROP INDEX IF EXISTS idx_memory_table_ts; DROP INDEX IF EXISTS idx_memory_table_row; CREATE INDEX IF NOT EXISTS idx_logs_ts ON logs(ts); CREATE INDEX IF NOT EXISTS idx_logs_table_ts ON logs(table_name, ts); CREATE INDEX IF NOT EXISTS idx_logs_table_row ON logs(table_name, row_id); SQL # triggers for nodes (drop/recreate to ensure enriched payloads) if has_table nodes; then "$SQLITE_BIN" "$DB_PATH" <<'SQL' DROP TRIGGER IF EXISTS trg_nodes_ai; DROP TRIGGER IF EXISTS trg_nodes_au; CREATE TRIGGER trg_nodes_ai AFTER INSERT ON nodes BEGIN INSERT INTO logs(table_name, action, row_id, summary, snapshot_json) VALUES('nodes', 'insert', NEW.id, printf('node created: %s', COALESCE(NEW.title,'')), json_object( 'id', NEW.id, 'title', NEW.title, 'link', NEW.link ) ); END; CREATE TRIGGER trg_nodes_au AFTER UPDATE ON nodes BEGIN INSERT INTO logs(table_name, action, row_id, summary, snapshot_json) VALUES('nodes', 'update', NEW.id, printf('node updated: %s', COALESCE(NEW.title,'')), json_object( 'id', NEW.id, 'title', NEW.title, 'link', NEW.link ) ); END; SQL fi # triggers for edges (enriched with node titles, truncated) if has_table edges; then "$SQLITE_BIN" "$DB_PATH" <<'SQL' DROP TRIGGER IF EXISTS trg_edges_ai; DROP TRIGGER IF EXISTS trg_edges_au; CREATE TRIGGER trg_edges_ai AFTER INSERT ON edges BEGIN INSERT INTO logs(table_name, action, row_id, summary, snapshot_json) VALUES('edges', 'insert', NEW.id, printf('edge %d→%d (%s)', NEW.from_node_id, NEW.to_node_id, COALESCE(NEW.source,'')), json_object( 'id', NEW.id, 'from', NEW.from_node_id, 'to', NEW.to_node_id, 'source', NEW.source, 'from_title', substr((SELECT title FROM nodes WHERE id = NEW.from_node_id), 1, 120), 'to_title', substr((SELECT title FROM nodes WHERE id = NEW.to_node_id), 1, 120) ) ); END; CREATE TRIGGER trg_edges_au AFTER UPDATE ON edges BEGIN INSERT INTO logs(table_name, action, row_id, summary, snapshot_json) VALUES('edges', 'update', NEW.id, printf('edge updated %d→%d', NEW.from_node_id, NEW.to_node_id), json_object( 'id', NEW.id, 'from', NEW.from_node_id, 'to', NEW.to_node_id, 'source', NEW.source, 'from_title', substr((SELECT title FROM nodes WHERE id = NEW.from_node_id), 1, 120), 'to_title', substr((SELECT title FROM nodes WHERE id = NEW.to_node_id), 1, 120) ) ); END; -- Add trigger to auto-update node updated_at timestamps when edges are created DROP TRIGGER IF EXISTS trg_edges_update_nodes_on_insert; CREATE TRIGGER trg_edges_update_nodes_on_insert AFTER INSERT ON edges BEGIN UPDATE nodes SET updated_at = strftime('%Y-%m-%dT%H:%M:%f', 'now') || 'Z' WHERE id = NEW.from_node_id; UPDATE nodes SET updated_at = strftime('%Y-%m-%dT%H:%M:%f', 'now') || 'Z' WHERE id = NEW.to_node_id; END; SQL fi # trigger for chats (enriched with content previews, truncated) if has_table chats; then "$SQLITE_BIN" "$DB_PATH" <<'SQL' DROP TRIGGER IF EXISTS trg_chats_ai; CREATE TRIGGER trg_chats_ai AFTER INSERT ON chats BEGIN INSERT INTO logs(table_name, action, row_id, summary, snapshot_json) VALUES('chats', 'insert', NEW.id, printf('chat: %s (%s)', COALESCE(NEW.helper_name,''), COALESCE(NEW.thread_id,'')), json_object( 'id', NEW.id, 'helper', NEW.helper_name, 'thread', NEW.thread_id, 'user_preview', substr(NEW.user_message, 1, 120), 'assistant_preview', substr(NEW.assistant_message, 1, 120) ) ); END; SQL fi # Add trigger to auto-prune logs to keep only most recent 10k entries if has_table logs; then "$SQLITE_BIN" "$DB_PATH" <<'SQL' DROP TRIGGER IF EXISTS trg_logs_prune; CREATE TRIGGER trg_logs_prune AFTER INSERT ON logs BEGIN DELETE FROM logs WHERE id < NEW.id - 10000; END; SQL fi echo "Ensuring logs_v view exists (removing legacy memory_v)..." "$SQLITE_BIN" "$DB_PATH" <<'SQL' DROP VIEW IF EXISTS logs_v; DROP VIEW IF EXISTS memory_v; CREATE VIEW logs_v AS SELECT m.id, m.ts, m.table_name, m.action, m.row_id, m.summary, m.enriched_summary, m.snapshot_json, CASE WHEN m.table_name='nodes' THEN n.title END AS node_title, CASE WHEN m.table_name='edges' THEN nf.title END AS edge_from_title, CASE WHEN m.table_name='edges' THEN nt.title END AS edge_to_title, CASE WHEN m.table_name='chats' THEN c.helper_name END AS chat_helper, CASE WHEN m.table_name='chats' THEN substr(c.user_message,1,120) END AS chat_user_preview, CASE WHEN m.table_name='chats' THEN substr(c.assistant_message,1,120) END AS chat_assistant_preview, CASE WHEN m.table_name='chats' THEN c.user_message END AS chat_user_full, CASE WHEN m.table_name='chats' THEN c.assistant_message END AS chat_assistant_full FROM logs m LEFT JOIN nodes n ON (m.table_name='nodes' AND m.row_id = n.id) LEFT JOIN edges e ON (m.table_name='edges' AND m.row_id = e.id) LEFT JOIN nodes nf ON e.from_node_id = nf.id LEFT JOIN nodes nt ON e.to_node_id = nt.id LEFT JOIN chats c ON (m.table_name='chats' AND m.row_id = c.id); SQL echo "Ensuring helpers.memory exists and is populated from fluid_context (if present)..." if has_table helpers; then if ! has_col helpers memory; then "$SQLITE_BIN" "$DB_PATH" "ALTER TABLE helpers ADD COLUMN memory TEXT;" if has_col helpers fluid_context; then "$SQLITE_BIN" "$DB_PATH" "UPDATE helpers SET memory = fluid_context WHERE fluid_context IS NOT NULL;" fi fi else echo " helpers table not present; skipping helpers.memory migration" fi echo "Updating helper available_tools to use updateHelperMemory (renaming from updateHelperFluidContext)..." if has_table helpers; then "$SQLITE_BIN" "$DB_PATH" <<'SQL' UPDATE helpers SET available_tools = REPLACE(available_tools, 'updateHelperFluidContext', 'updateHelperMemory') WHERE available_tools LIKE '%updateHelperFluidContext%'; SQL else echo " helpers table not present; skipping available_tools rename" fi echo "Dropping helpers.fluid_context column if present (post-migration cleanup)..." if has_table helpers && has_col helpers fluid_context; then "$SQLITE_BIN" "$DB_PATH" "ALTER TABLE helpers DROP COLUMN fluid_context;" || true fi echo "Running VACUUM and ANALYZE..." "$SQLITE_BIN" "$DB_PATH" "VACUUM; ANALYZE;" echo "Done. Schema is compatible."