import { Database } from 'bun:sqlite'; const ROOM_SKILL_OVERRIDES_SCHEMA = ` CREATE TABLE IF NOT EXISTS room_skill_overrides ( chat_jid TEXT NOT NULL, agent_type TEXT NOT NULL, skill_scope TEXT NOT NULL, skill_name TEXT NOT NULL, enabled INTEGER NOT NULL, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (chat_jid, agent_type, skill_scope, skill_name), FOREIGN KEY (chat_jid) REFERENCES room_settings(chat_jid) ON DELETE CASCADE, CHECK (agent_type IN ('claude-code', 'codex')), CHECK (enabled IN (0, 1)), CHECK (length(skill_scope) > 0), CHECK (length(skill_name) > 0) ); CREATE INDEX IF NOT EXISTS idx_room_skill_overrides_room ON room_skill_overrides(chat_jid, agent_type); `; export function applyBaseSchema(database: Database): void { database.exec(` CREATE TABLE IF NOT EXISTS chats ( jid TEXT PRIMARY KEY, name TEXT, last_message_time TEXT, channel TEXT, is_group INTEGER DEFAULT 0 ); CREATE TABLE IF NOT EXISTS messages ( id TEXT, chat_jid TEXT, sender TEXT, sender_name TEXT, content TEXT, timestamp TEXT, seq INTEGER, is_from_me INTEGER, is_bot_message INTEGER DEFAULT 0, message_source_kind TEXT NOT NULL DEFAULT 'human', PRIMARY KEY (id, chat_jid), FOREIGN KEY (chat_jid) REFERENCES chats(jid) ); CREATE INDEX IF NOT EXISTS idx_timestamp ON messages(timestamp); CREATE INDEX IF NOT EXISTS idx_messages_chat_timestamp ON messages(chat_jid, timestamp DESC); CREATE TABLE IF NOT EXISTS message_sequence ( id INTEGER PRIMARY KEY AUTOINCREMENT ); CREATE TABLE IF NOT EXISTS work_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, group_folder TEXT NOT NULL, chat_jid TEXT NOT NULL, agent_type TEXT NOT NULL, service_id TEXT, delivery_role TEXT, status TEXT NOT NULL DEFAULT 'produced', start_seq INTEGER, end_seq INTEGER, result_payload TEXT NOT NULL, attachment_payload TEXT, delivery_attempts INTEGER NOT NULL DEFAULT 0, delivery_message_id TEXT, last_error TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')), delivered_at TEXT, CHECK (status IN ('produced', 'delivery_retry', 'delivered')), CHECK (delivery_role IN ('owner', 'reviewer', 'arbiter') OR delivery_role IS NULL) ); CREATE INDEX IF NOT EXISTS idx_work_items_status ON work_items(status, updated_at); CREATE INDEX IF NOT EXISTS idx_work_items_group_agent ON work_items(chat_jid, agent_type, delivery_role, status); CREATE UNIQUE INDEX IF NOT EXISTS idx_work_items_open ON work_items(chat_jid, agent_type, IFNULL(delivery_role, '')) WHERE status IN ('produced', 'delivery_retry'); CREATE TABLE IF NOT EXISTS scheduled_tasks ( id TEXT PRIMARY KEY, group_folder TEXT NOT NULL, chat_jid TEXT NOT NULL, agent_type TEXT, ci_provider TEXT, ci_metadata TEXT, max_duration_ms INTEGER, status_message_id TEXT, status_started_at TEXT, prompt TEXT NOT NULL, schedule_type TEXT NOT NULL, schedule_value TEXT NOT NULL, next_run TEXT, last_run TEXT, last_result TEXT, status TEXT DEFAULT 'active', created_at TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_next_run ON scheduled_tasks(next_run); CREATE INDEX IF NOT EXISTS idx_status ON scheduled_tasks(status); CREATE TABLE IF NOT EXISTS task_run_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, task_id TEXT NOT NULL, run_at TEXT NOT NULL, duration_ms INTEGER NOT NULL, status TEXT NOT NULL, result TEXT, error TEXT, FOREIGN KEY (task_id) REFERENCES scheduled_tasks(id) ); CREATE INDEX IF NOT EXISTS idx_task_run_logs ON task_run_logs(task_id, run_at); CREATE TABLE IF NOT EXISTS router_state ( key TEXT PRIMARY KEY, value TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS sessions ( group_folder TEXT NOT NULL, agent_type TEXT NOT NULL DEFAULT 'claude-code', session_id TEXT NOT NULL, PRIMARY KEY (group_folder, agent_type) ); CREATE TABLE IF NOT EXISTS paired_projects ( chat_jid TEXT PRIMARY KEY, group_folder TEXT NOT NULL, canonical_work_dir TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS paired_tasks ( id TEXT PRIMARY KEY, chat_jid TEXT NOT NULL, group_folder TEXT NOT NULL, owner_service_id TEXT NOT NULL, reviewer_service_id TEXT NOT NULL, owner_agent_type TEXT, reviewer_agent_type TEXT, arbiter_agent_type TEXT, title TEXT, source_ref TEXT, plan_notes TEXT, review_requested_at TEXT, round_trip_count INTEGER NOT NULL DEFAULT 0, owner_failure_count INTEGER NOT NULL DEFAULT 0, reviewer_failure_count INTEGER NOT NULL DEFAULT 0, owner_step_done_streak INTEGER NOT NULL DEFAULT 0, finalize_step_done_count INTEGER NOT NULL DEFAULT 0, task_done_then_user_reopen_count INTEGER NOT NULL DEFAULT 0, empty_step_done_streak INTEGER NOT NULL DEFAULT 0, arbiter_intervention_count INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT 'active', arbiter_verdict TEXT, arbiter_requested_at TEXT, completion_reason TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, CHECK (status IN ('active', 'review_ready', 'in_review', 'merge_ready', 'completed', 'arbiter_requested', 'in_arbitration')), CHECK (owner_agent_type IN ('claude-code', 'codex') OR owner_agent_type IS NULL), CHECK (reviewer_agent_type IN ('claude-code', 'codex') OR reviewer_agent_type IS NULL), CHECK (arbiter_agent_type IN ('claude-code', 'codex') OR arbiter_agent_type IS NULL) ); CREATE INDEX IF NOT EXISTS idx_paired_tasks_chat_status ON paired_tasks(chat_jid, status, updated_at); CREATE TABLE IF NOT EXISTS paired_workspaces ( id TEXT PRIMARY KEY, task_id TEXT NOT NULL, role TEXT NOT NULL, workspace_dir TEXT NOT NULL, snapshot_source_dir TEXT, snapshot_ref TEXT, status TEXT NOT NULL DEFAULT 'ready', snapshot_refreshed_at TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, CHECK (role IN ('owner', 'reviewer')), CHECK (status IN ('ready', 'stale')) ); CREATE UNIQUE INDEX IF NOT EXISTS idx_paired_workspaces_task_role ON paired_workspaces(task_id, role); CREATE TABLE IF NOT EXISTS paired_turn_outputs ( id INTEGER PRIMARY KEY AUTOINCREMENT, task_id TEXT NOT NULL, turn_number INTEGER NOT NULL, role TEXT NOT NULL, output_text TEXT NOT NULL, attachment_payload TEXT, verdict TEXT, created_at TEXT NOT NULL, UNIQUE(task_id, turn_number, role) ); CREATE INDEX IF NOT EXISTS idx_paired_turn_outputs_task ON paired_turn_outputs(task_id, turn_number); CREATE TABLE IF NOT EXISTS paired_turn_reservations ( chat_jid TEXT NOT NULL, task_id TEXT NOT NULL, task_status TEXT NOT NULL, round_trip_count INTEGER NOT NULL DEFAULT 0, task_updated_at TEXT NOT NULL, turn_id TEXT NOT NULL, turn_attempt_id TEXT, turn_attempt_no INTEGER, turn_role TEXT NOT NULL, intent_kind TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending', scheduled_run_id TEXT, consumed_run_id TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, consumed_at TEXT, PRIMARY KEY (chat_jid, task_id, task_updated_at, intent_kind), FOREIGN KEY (turn_id, turn_attempt_no) REFERENCES paired_turn_attempts(turn_id, attempt_no) ON DELETE CASCADE, CHECK (status IN ('pending', 'completed')), CHECK (turn_role IN ('owner', 'reviewer', 'arbiter')), CHECK ( intent_kind IN ( 'owner-turn', 'reviewer-turn', 'arbiter-turn', 'owner-follow-up', 'finalize-owner-turn' ) ) ); CREATE INDEX IF NOT EXISTS idx_paired_turn_reservations_task ON paired_turn_reservations(task_id, task_updated_at, status); CREATE TABLE IF NOT EXISTS paired_turns ( turn_id TEXT PRIMARY KEY, task_id TEXT NOT NULL, task_updated_at TEXT NOT NULL, role TEXT NOT NULL, intent_kind TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, CHECK (role IN ('owner', 'reviewer', 'arbiter')), CHECK ( intent_kind IN ( 'owner-turn', 'reviewer-turn', 'arbiter-turn', 'owner-follow-up', 'finalize-owner-turn' ) ) ); CREATE INDEX IF NOT EXISTS idx_paired_turns_task ON paired_turns(task_id, task_updated_at, updated_at); CREATE TABLE IF NOT EXISTS paired_turn_attempts ( attempt_id TEXT NOT NULL PRIMARY KEY, parent_attempt_id TEXT, parent_handoff_id INTEGER, continuation_handoff_id INTEGER, turn_id TEXT NOT NULL, attempt_no INTEGER NOT NULL, task_id TEXT NOT NULL, task_updated_at TEXT NOT NULL, role TEXT NOT NULL, intent_kind TEXT NOT NULL, state TEXT NOT NULL, executor_service_id TEXT, executor_agent_type TEXT, active_run_id TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, completed_at TEXT, last_error TEXT, UNIQUE (turn_id, attempt_no), FOREIGN KEY (parent_attempt_id) REFERENCES paired_turn_attempts(attempt_id) ON DELETE CASCADE, FOREIGN KEY (parent_handoff_id) REFERENCES service_handoffs(id) ON DELETE SET NULL, FOREIGN KEY (continuation_handoff_id) REFERENCES service_handoffs(id) ON DELETE SET NULL, FOREIGN KEY (turn_id) REFERENCES paired_turns(turn_id) ON DELETE CASCADE, CHECK (role IN ('owner', 'reviewer', 'arbiter')), CHECK ( intent_kind IN ( 'owner-turn', 'reviewer-turn', 'arbiter-turn', 'owner-follow-up', 'finalize-owner-turn' ) ), CHECK ( state IN ( 'running', 'delegated', 'completed', 'failed', 'cancelled' ) ), CHECK (executor_agent_type IN ('claude-code', 'codex') OR executor_agent_type IS NULL) ); CREATE INDEX IF NOT EXISTS idx_paired_turn_attempts_turn ON paired_turn_attempts(turn_id, attempt_no); CREATE INDEX IF NOT EXISTS idx_paired_turn_attempts_task ON paired_turn_attempts(task_id, task_updated_at, attempt_no); CREATE TABLE IF NOT EXISTS paired_task_execution_leases ( task_id TEXT PRIMARY KEY, chat_jid TEXT NOT NULL, role TEXT NOT NULL, turn_id TEXT NOT NULL, turn_attempt_id TEXT, turn_attempt_no INTEGER, intent_kind TEXT NOT NULL, claimed_run_id TEXT NOT NULL, claimed_service_id TEXT NOT NULL, task_status TEXT NOT NULL, task_updated_at TEXT NOT NULL, claimed_at TEXT NOT NULL, updated_at TEXT NOT NULL, expires_at TEXT NOT NULL, FOREIGN KEY (turn_id, turn_attempt_no) REFERENCES paired_turn_attempts(turn_id, attempt_no) ON DELETE CASCADE, CHECK (role IN ('owner', 'reviewer', 'arbiter')), CHECK ( intent_kind IN ( 'owner-turn', 'reviewer-turn', 'arbiter-turn', 'owner-follow-up', 'finalize-owner-turn' ) ) ); CREATE INDEX IF NOT EXISTS idx_paired_task_execution_leases_expires_at ON paired_task_execution_leases(expires_at); CREATE TABLE IF NOT EXISTS channel_owner ( chat_jid TEXT PRIMARY KEY, owner_service_id TEXT NOT NULL, reviewer_service_id TEXT, arbiter_service_id TEXT, owner_agent_type TEXT, reviewer_agent_type TEXT, arbiter_agent_type TEXT, activated_at TEXT, reason TEXT, CHECK (owner_agent_type IN ('claude-code', 'codex') OR owner_agent_type IS NULL), CHECK (reviewer_agent_type IN ('claude-code', 'codex') OR reviewer_agent_type IS NULL), CHECK (arbiter_agent_type IN ('claude-code', 'codex') OR arbiter_agent_type IS NULL) ); CREATE TABLE IF NOT EXISTS room_settings ( chat_jid TEXT PRIMARY KEY, room_mode TEXT NOT NULL, mode_source TEXT NOT NULL DEFAULT 'explicit', name TEXT, folder TEXT, trigger_pattern TEXT, requires_trigger INTEGER DEFAULT 1, is_main INTEGER DEFAULT 0, owner_agent_type TEXT, work_dir TEXT, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL, CHECK (room_mode IN ('single', 'tribunal')), CHECK (owner_agent_type IN ('claude-code', 'codex') OR owner_agent_type IS NULL) ); CREATE TABLE IF NOT EXISTS room_role_overrides ( chat_jid TEXT NOT NULL, role TEXT NOT NULL, agent_type TEXT NOT NULL, agent_config_json TEXT, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (chat_jid, role), CHECK (role IN ('owner', 'reviewer', 'arbiter')), CHECK (agent_type IN ('claude-code', 'codex')) ); CREATE TABLE IF NOT EXISTS service_handoffs ( id INTEGER PRIMARY KEY AUTOINCREMENT, chat_jid TEXT NOT NULL, group_folder TEXT NOT NULL, source_service_id TEXT NOT NULL, target_service_id TEXT NOT NULL, paired_task_id TEXT, paired_task_updated_at TEXT, turn_id TEXT, turn_attempt_id TEXT, turn_attempt_no INTEGER, turn_intent_kind TEXT, turn_role TEXT, source_role TEXT, source_agent_type TEXT, target_role TEXT, target_agent_type TEXT NOT NULL, prompt TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending', start_seq INTEGER, end_seq INTEGER, reason TEXT, intended_role TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), claimed_at TEXT, completed_at TEXT, last_error TEXT, FOREIGN KEY (turn_id, turn_attempt_no) REFERENCES paired_turn_attempts(turn_id, attempt_no) ON DELETE CASCADE, CHECK (status IN ('pending', 'claimed', 'completed', 'failed')), CHECK (intended_role IN ('owner', 'reviewer', 'arbiter') OR intended_role IS NULL), CHECK ( turn_intent_kind IN ( 'owner-turn', 'reviewer-turn', 'arbiter-turn', 'owner-follow-up', 'finalize-owner-turn' ) OR turn_intent_kind IS NULL ), CHECK (turn_role IN ('owner', 'reviewer', 'arbiter') OR turn_role IS NULL), CHECK (source_role IN ('owner', 'reviewer', 'arbiter') OR source_role IS NULL), CHECK (target_role IN ('owner', 'reviewer', 'arbiter') OR target_role IS NULL) ); CREATE INDEX IF NOT EXISTS idx_service_handoffs_target ON service_handoffs(status, target_role, target_agent_type, created_at); CREATE TABLE IF NOT EXISTS memories ( id INTEGER PRIMARY KEY, scope_kind TEXT NOT NULL, scope_key TEXT NOT NULL, content TEXT NOT NULL, keywords_json TEXT NOT NULL DEFAULT '[]', memory_kind TEXT, source_kind TEXT NOT NULL, source_ref TEXT, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, last_used_at TEXT, archived_at TEXT ); CREATE INDEX IF NOT EXISTS idx_memories_scope ON memories(scope_kind, scope_key); CREATE INDEX IF NOT EXISTS idx_memories_active ON memories(scope_kind, scope_key, archived_at, created_at); CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5( content, keywords, content='', tokenize='unicode61' ); CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN INSERT INTO memories_fts(rowid, content, keywords) VALUES (new.id, new.content, new.keywords_json); END; CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN INSERT INTO memories_fts(memories_fts, rowid, content, keywords) VALUES ('delete', old.id, old.content, old.keywords_json); END; CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN INSERT INTO memories_fts(memories_fts, rowid, content, keywords) VALUES ('delete', old.id, old.content, old.keywords_json); INSERT INTO memories_fts(rowid, content, keywords) VALUES (new.id, new.content, new.keywords_json); END; `); database.exec(ROOM_SKILL_OVERRIDES_SCHEMA); }