a0dd516cd5
Systematic cleanup of every error handling anti-pattern detected by the automated scanner. 289 issues fixed via code changes, 12 approved with specific technical justifications. Changes across 90 files: - GENERIC_CATCH (141): Added instanceof Error type discrimination - LARGE_TRY_BLOCK (82): Extracted helper methods to narrow try scope to ≤10 lines - NO_LOGGING_IN_CATCH (65): Added logger/console calls for error visibility - CATCH_AND_CONTINUE_CRITICAL_PATH (10): Added throw/return or approved overrides - ERROR_STRING_MATCHING (2): Approved with rationale (no typed error classes) - ERROR_MESSAGE_GUESSING (1): Replaced chained .includes() with documented pattern array - PROMISE_CATCH_NO_LOGGING (1): Added logging to .catch() handler Also fixes a detector bug where nested try/catch inside a catch block corrupted brace-depth tracking, causing false positives. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
646 lines
25 KiB
TypeScript
646 lines
25 KiB
TypeScript
import { Database } from 'bun:sqlite';
|
|
import { Migration } from './Database.js';
|
|
import { logger } from '../../utils/logger.js';
|
|
|
|
// Re-export MigrationRunner for SessionStore migration extraction
|
|
export { MigrationRunner } from './migrations/runner.js';
|
|
|
|
/**
|
|
* Initial schema migration - creates all core tables
|
|
*/
|
|
export const migration001: Migration = {
|
|
version: 1,
|
|
up: (db: Database) => {
|
|
// Sessions table - core session tracking
|
|
db.run(`
|
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
session_id TEXT UNIQUE NOT NULL,
|
|
project TEXT NOT NULL,
|
|
created_at TEXT NOT NULL,
|
|
created_at_epoch INTEGER NOT NULL,
|
|
source TEXT NOT NULL DEFAULT 'compress',
|
|
archive_path TEXT,
|
|
archive_bytes INTEGER,
|
|
archive_checksum TEXT,
|
|
archived_at TEXT,
|
|
metadata_json TEXT
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project);
|
|
CREATE INDEX IF NOT EXISTS idx_sessions_created_at ON sessions(created_at_epoch DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_sessions_project_created ON sessions(project, created_at_epoch DESC);
|
|
`);
|
|
|
|
// Memories table - compressed memory chunks
|
|
db.run(`
|
|
CREATE TABLE IF NOT EXISTS memories (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
session_id TEXT NOT NULL,
|
|
text TEXT NOT NULL,
|
|
document_id TEXT UNIQUE,
|
|
keywords TEXT,
|
|
created_at TEXT NOT NULL,
|
|
created_at_epoch INTEGER NOT NULL,
|
|
project TEXT NOT NULL,
|
|
archive_basename TEXT,
|
|
origin TEXT NOT NULL DEFAULT 'transcript',
|
|
FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_memories_session ON memories(session_id);
|
|
CREATE INDEX IF NOT EXISTS idx_memories_project ON memories(project);
|
|
CREATE INDEX IF NOT EXISTS idx_memories_created_at ON memories(created_at_epoch DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_memories_project_created ON memories(project, created_at_epoch DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_memories_document_id ON memories(document_id);
|
|
CREATE INDEX IF NOT EXISTS idx_memories_origin ON memories(origin);
|
|
`);
|
|
|
|
// Overviews table - session summaries (one per project)
|
|
db.run(`
|
|
CREATE TABLE IF NOT EXISTS overviews (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
session_id TEXT NOT NULL,
|
|
content TEXT NOT NULL,
|
|
created_at TEXT NOT NULL,
|
|
created_at_epoch INTEGER NOT NULL,
|
|
project TEXT NOT NULL,
|
|
origin TEXT NOT NULL DEFAULT 'claude',
|
|
FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_overviews_session ON overviews(session_id);
|
|
CREATE INDEX IF NOT EXISTS idx_overviews_project ON overviews(project);
|
|
CREATE INDEX IF NOT EXISTS idx_overviews_created_at ON overviews(created_at_epoch DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_overviews_project_created ON overviews(project, created_at_epoch DESC);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_overviews_project_latest ON overviews(project, created_at_epoch DESC);
|
|
`);
|
|
|
|
// Diagnostics table - system health and debug info
|
|
db.run(`
|
|
CREATE TABLE IF NOT EXISTS diagnostics (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
session_id TEXT,
|
|
message TEXT NOT NULL,
|
|
severity TEXT NOT NULL DEFAULT 'info',
|
|
created_at TEXT NOT NULL,
|
|
created_at_epoch INTEGER NOT NULL,
|
|
project TEXT NOT NULL,
|
|
origin TEXT NOT NULL DEFAULT 'system',
|
|
FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE SET NULL
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_diagnostics_session ON diagnostics(session_id);
|
|
CREATE INDEX IF NOT EXISTS idx_diagnostics_project ON diagnostics(project);
|
|
CREATE INDEX IF NOT EXISTS idx_diagnostics_severity ON diagnostics(severity);
|
|
CREATE INDEX IF NOT EXISTS idx_diagnostics_created ON diagnostics(created_at_epoch DESC);
|
|
`);
|
|
|
|
// Transcript events table - raw conversation events
|
|
db.run(`
|
|
CREATE TABLE IF NOT EXISTS transcript_events (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
session_id TEXT NOT NULL,
|
|
project TEXT,
|
|
event_index INTEGER NOT NULL,
|
|
event_type TEXT,
|
|
raw_json TEXT NOT NULL,
|
|
captured_at TEXT NOT NULL,
|
|
captured_at_epoch INTEGER NOT NULL,
|
|
FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE,
|
|
UNIQUE(session_id, event_index)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_transcript_events_session ON transcript_events(session_id, event_index);
|
|
CREATE INDEX IF NOT EXISTS idx_transcript_events_project ON transcript_events(project);
|
|
CREATE INDEX IF NOT EXISTS idx_transcript_events_type ON transcript_events(event_type);
|
|
CREATE INDEX IF NOT EXISTS idx_transcript_events_captured ON transcript_events(captured_at_epoch DESC);
|
|
`);
|
|
|
|
console.log('✅ Created all database tables successfully');
|
|
},
|
|
|
|
down: (db: Database) => {
|
|
db.run(`
|
|
DROP TABLE IF EXISTS transcript_events;
|
|
DROP TABLE IF EXISTS diagnostics;
|
|
DROP TABLE IF EXISTS overviews;
|
|
DROP TABLE IF EXISTS memories;
|
|
DROP TABLE IF EXISTS sessions;
|
|
`);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Migration 002 - Add hierarchical memory fields (v2 format)
|
|
*/
|
|
export const migration002: Migration = {
|
|
version: 2,
|
|
up: (db: Database) => {
|
|
// Add new columns for hierarchical memory structure
|
|
db.run(`
|
|
ALTER TABLE memories ADD COLUMN title TEXT;
|
|
ALTER TABLE memories ADD COLUMN subtitle TEXT;
|
|
ALTER TABLE memories ADD COLUMN facts TEXT;
|
|
ALTER TABLE memories ADD COLUMN concepts TEXT;
|
|
ALTER TABLE memories ADD COLUMN files_touched TEXT;
|
|
`);
|
|
|
|
// Create indexes for the new fields to improve search performance
|
|
db.run(`
|
|
CREATE INDEX IF NOT EXISTS idx_memories_title ON memories(title);
|
|
CREATE INDEX IF NOT EXISTS idx_memories_concepts ON memories(concepts);
|
|
`);
|
|
|
|
console.log('✅ Added hierarchical memory fields to memories table');
|
|
},
|
|
|
|
down: (_db: Database) => {
|
|
// Note: SQLite doesn't support DROP COLUMN in all versions
|
|
// In production, we'd need to recreate the table without these columns
|
|
// For now, we'll just log a warning
|
|
console.log('⚠️ Warning: SQLite ALTER TABLE DROP COLUMN not fully supported');
|
|
console.log('⚠️ To rollback, manually recreate the memories table');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Migration 003 - Add streaming_sessions table for real-time session tracking
|
|
*/
|
|
export const migration003: Migration = {
|
|
version: 3,
|
|
up: (db: Database) => {
|
|
// Streaming sessions table - tracks active SDK compression sessions
|
|
db.run(`
|
|
CREATE TABLE IF NOT EXISTS streaming_sessions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
content_session_id TEXT UNIQUE NOT NULL,
|
|
memory_session_id TEXT,
|
|
project TEXT NOT NULL,
|
|
title TEXT,
|
|
subtitle TEXT,
|
|
user_prompt TEXT,
|
|
started_at TEXT NOT NULL,
|
|
started_at_epoch INTEGER NOT NULL,
|
|
updated_at TEXT,
|
|
updated_at_epoch INTEGER,
|
|
completed_at TEXT,
|
|
completed_at_epoch INTEGER,
|
|
status TEXT NOT NULL DEFAULT 'active'
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_claude_id ON streaming_sessions(content_session_id);
|
|
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_sdk_id ON streaming_sessions(memory_session_id);
|
|
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_project ON streaming_sessions(project);
|
|
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_status ON streaming_sessions(status);
|
|
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_started ON streaming_sessions(started_at_epoch DESC);
|
|
`);
|
|
|
|
console.log('✅ Created streaming_sessions table for real-time session tracking');
|
|
},
|
|
|
|
down: (db: Database) => {
|
|
db.run(`
|
|
DROP TABLE IF EXISTS streaming_sessions;
|
|
`);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Migration 004 - Add SDK agent architecture tables
|
|
* Implements the refactor plan for hook-driven memory with SDK agent synthesis
|
|
*/
|
|
export const migration004: Migration = {
|
|
version: 4,
|
|
up: (db: Database) => {
|
|
// SDK sessions table - tracks SDK streaming sessions
|
|
db.run(`
|
|
CREATE TABLE IF NOT EXISTS sdk_sessions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
content_session_id TEXT UNIQUE NOT NULL,
|
|
memory_session_id TEXT UNIQUE,
|
|
project TEXT NOT NULL,
|
|
user_prompt TEXT,
|
|
started_at TEXT NOT NULL,
|
|
started_at_epoch INTEGER NOT NULL,
|
|
completed_at TEXT,
|
|
completed_at_epoch INTEGER,
|
|
status TEXT CHECK(status IN ('active', 'completed', 'failed')) NOT NULL DEFAULT 'active'
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_claude_id ON sdk_sessions(content_session_id);
|
|
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_sdk_id ON sdk_sessions(memory_session_id);
|
|
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_project ON sdk_sessions(project);
|
|
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_status ON sdk_sessions(status);
|
|
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_started ON sdk_sessions(started_at_epoch DESC);
|
|
`);
|
|
|
|
// Observation queue table - tracks pending observations for SDK processing
|
|
db.run(`
|
|
CREATE TABLE IF NOT EXISTS observation_queue (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
memory_session_id TEXT NOT NULL,
|
|
tool_name TEXT NOT NULL,
|
|
tool_input TEXT NOT NULL,
|
|
tool_output TEXT NOT NULL,
|
|
created_at_epoch INTEGER NOT NULL,
|
|
processed_at_epoch INTEGER,
|
|
FOREIGN KEY(memory_session_id) REFERENCES sdk_sessions(memory_session_id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_observation_queue_sdk_session ON observation_queue(memory_session_id);
|
|
CREATE INDEX IF NOT EXISTS idx_observation_queue_processed ON observation_queue(processed_at_epoch);
|
|
CREATE INDEX IF NOT EXISTS idx_observation_queue_pending ON observation_queue(memory_session_id, processed_at_epoch);
|
|
`);
|
|
|
|
// Observations table - stores extracted observations (what SDK decides is important)
|
|
db.run(`
|
|
CREATE TABLE IF NOT EXISTS observations (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
memory_session_id TEXT NOT NULL,
|
|
project TEXT NOT NULL,
|
|
text TEXT NOT NULL,
|
|
type TEXT NOT NULL,
|
|
created_at TEXT NOT NULL,
|
|
created_at_epoch INTEGER NOT NULL,
|
|
FOREIGN KEY(memory_session_id) REFERENCES sdk_sessions(memory_session_id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_observations_sdk_session ON observations(memory_session_id);
|
|
CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project);
|
|
CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
|
|
CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch DESC);
|
|
`);
|
|
|
|
// Session summaries table - stores structured session summaries
|
|
db.run(`
|
|
CREATE TABLE IF NOT EXISTS session_summaries (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
memory_session_id TEXT UNIQUE NOT NULL,
|
|
project TEXT NOT NULL,
|
|
request TEXT,
|
|
investigated TEXT,
|
|
learned TEXT,
|
|
completed TEXT,
|
|
next_steps TEXT,
|
|
files_read TEXT,
|
|
files_edited TEXT,
|
|
notes TEXT,
|
|
created_at TEXT NOT NULL,
|
|
created_at_epoch INTEGER NOT NULL,
|
|
FOREIGN KEY(memory_session_id) REFERENCES sdk_sessions(memory_session_id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_session_summaries_sdk_session ON session_summaries(memory_session_id);
|
|
CREATE INDEX IF NOT EXISTS idx_session_summaries_project ON session_summaries(project);
|
|
CREATE INDEX IF NOT EXISTS idx_session_summaries_created ON session_summaries(created_at_epoch DESC);
|
|
`);
|
|
|
|
console.log('✅ Created SDK agent architecture tables');
|
|
},
|
|
|
|
down: (db: Database) => {
|
|
db.run(`
|
|
DROP TABLE IF EXISTS session_summaries;
|
|
DROP TABLE IF EXISTS observations;
|
|
DROP TABLE IF EXISTS observation_queue;
|
|
DROP TABLE IF EXISTS sdk_sessions;
|
|
`);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Migration 005 - Remove orphaned tables
|
|
* Drops streaming_sessions (superseded by sdk_sessions)
|
|
* Drops observation_queue (superseded by Unix socket communication)
|
|
*/
|
|
export const migration005: Migration = {
|
|
version: 5,
|
|
up: (db: Database) => {
|
|
// Drop streaming_sessions - superseded by sdk_sessions in migration004
|
|
// This table was from v2 architecture and is no longer used
|
|
db.run(`DROP TABLE IF EXISTS streaming_sessions`);
|
|
|
|
// Drop observation_queue - superseded by Unix socket communication
|
|
// Worker now uses sockets instead of database polling for observations
|
|
db.run(`DROP TABLE IF EXISTS observation_queue`);
|
|
|
|
console.log('✅ Dropped orphaned tables: streaming_sessions, observation_queue');
|
|
},
|
|
|
|
down: (db: Database) => {
|
|
// Recreate tables if needed (though they should never be used)
|
|
db.run(`
|
|
CREATE TABLE IF NOT EXISTS streaming_sessions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
content_session_id TEXT UNIQUE NOT NULL,
|
|
memory_session_id TEXT,
|
|
project TEXT NOT NULL,
|
|
title TEXT,
|
|
subtitle TEXT,
|
|
user_prompt TEXT,
|
|
started_at TEXT NOT NULL,
|
|
started_at_epoch INTEGER NOT NULL,
|
|
updated_at TEXT,
|
|
updated_at_epoch INTEGER,
|
|
completed_at TEXT,
|
|
completed_at_epoch INTEGER,
|
|
status TEXT NOT NULL DEFAULT 'active'
|
|
)
|
|
`);
|
|
|
|
db.run(`
|
|
CREATE TABLE IF NOT EXISTS observation_queue (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
memory_session_id TEXT NOT NULL,
|
|
tool_name TEXT NOT NULL,
|
|
tool_input TEXT NOT NULL,
|
|
tool_output TEXT NOT NULL,
|
|
created_at_epoch INTEGER NOT NULL,
|
|
processed_at_epoch INTEGER,
|
|
FOREIGN KEY(memory_session_id) REFERENCES sdk_sessions(memory_session_id) ON DELETE CASCADE
|
|
)
|
|
`);
|
|
|
|
console.log('⚠️ Recreated streaming_sessions and observation_queue (for rollback only)');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Migration 006 - Add FTS5 full-text search tables
|
|
* Creates virtual tables for fast text search on observations and session_summaries
|
|
*/
|
|
export const migration006: Migration = {
|
|
version: 6,
|
|
up: (db: Database) => {
|
|
// FTS5 may be unavailable on some platforms (e.g., Bun on Windows #791).
|
|
// Probe before creating tables — search falls back to ChromaDB when unavailable.
|
|
try {
|
|
db.run('CREATE VIRTUAL TABLE _fts5_probe USING fts5(test_column)');
|
|
db.run('DROP TABLE _fts5_probe');
|
|
} catch (error) {
|
|
logger.warn('DB', 'FTS5 not available on this platform — skipping FTS migration (search uses ChromaDB)', {}, error instanceof Error ? error : undefined);
|
|
return;
|
|
}
|
|
|
|
// FTS5 virtual table for observations
|
|
// Note: This assumes the hierarchical fields (title, subtitle, etc.) already exist
|
|
// from the inline migrations in SessionStore constructor
|
|
db.run(`
|
|
CREATE VIRTUAL TABLE IF NOT EXISTS observations_fts USING fts5(
|
|
title,
|
|
subtitle,
|
|
narrative,
|
|
text,
|
|
facts,
|
|
concepts,
|
|
content='observations',
|
|
content_rowid='id'
|
|
);
|
|
`);
|
|
|
|
// Populate FTS table with existing data
|
|
db.run(`
|
|
INSERT INTO observations_fts(rowid, title, subtitle, narrative, text, facts, concepts)
|
|
SELECT id, title, subtitle, narrative, text, facts, concepts
|
|
FROM observations;
|
|
`);
|
|
|
|
// Triggers to keep observations_fts in sync
|
|
db.run(`
|
|
CREATE TRIGGER IF NOT EXISTS observations_ai AFTER INSERT ON observations BEGIN
|
|
INSERT INTO observations_fts(rowid, title, subtitle, narrative, text, facts, concepts)
|
|
VALUES (new.id, new.title, new.subtitle, new.narrative, new.text, new.facts, new.concepts);
|
|
END;
|
|
|
|
CREATE TRIGGER IF NOT EXISTS observations_ad AFTER DELETE ON observations BEGIN
|
|
INSERT INTO observations_fts(observations_fts, rowid, title, subtitle, narrative, text, facts, concepts)
|
|
VALUES('delete', old.id, old.title, old.subtitle, old.narrative, old.text, old.facts, old.concepts);
|
|
END;
|
|
|
|
CREATE TRIGGER IF NOT EXISTS observations_au AFTER UPDATE ON observations BEGIN
|
|
INSERT INTO observations_fts(observations_fts, rowid, title, subtitle, narrative, text, facts, concepts)
|
|
VALUES('delete', old.id, old.title, old.subtitle, old.narrative, old.text, old.facts, old.concepts);
|
|
INSERT INTO observations_fts(rowid, title, subtitle, narrative, text, facts, concepts)
|
|
VALUES (new.id, new.title, new.subtitle, new.narrative, new.text, new.facts, new.concepts);
|
|
END;
|
|
`);
|
|
|
|
// FTS5 virtual table for session_summaries
|
|
db.run(`
|
|
CREATE VIRTUAL TABLE IF NOT EXISTS session_summaries_fts USING fts5(
|
|
request,
|
|
investigated,
|
|
learned,
|
|
completed,
|
|
next_steps,
|
|
notes,
|
|
content='session_summaries',
|
|
content_rowid='id'
|
|
);
|
|
`);
|
|
|
|
// Populate FTS table with existing data
|
|
db.run(`
|
|
INSERT INTO session_summaries_fts(rowid, request, investigated, learned, completed, next_steps, notes)
|
|
SELECT id, request, investigated, learned, completed, next_steps, notes
|
|
FROM session_summaries;
|
|
`);
|
|
|
|
// Triggers to keep session_summaries_fts in sync
|
|
db.run(`
|
|
CREATE TRIGGER IF NOT EXISTS session_summaries_ai AFTER INSERT ON session_summaries BEGIN
|
|
INSERT INTO session_summaries_fts(rowid, request, investigated, learned, completed, next_steps, notes)
|
|
VALUES (new.id, new.request, new.investigated, new.learned, new.completed, new.next_steps, new.notes);
|
|
END;
|
|
|
|
CREATE TRIGGER IF NOT EXISTS session_summaries_ad AFTER DELETE ON session_summaries BEGIN
|
|
INSERT INTO session_summaries_fts(session_summaries_fts, rowid, request, investigated, learned, completed, next_steps, notes)
|
|
VALUES('delete', old.id, old.request, old.investigated, old.learned, old.completed, old.next_steps, old.notes);
|
|
END;
|
|
|
|
CREATE TRIGGER IF NOT EXISTS session_summaries_au AFTER UPDATE ON session_summaries BEGIN
|
|
INSERT INTO session_summaries_fts(session_summaries_fts, rowid, request, investigated, learned, completed, next_steps, notes)
|
|
VALUES('delete', old.id, old.request, old.investigated, old.learned, old.completed, old.next_steps, old.notes);
|
|
INSERT INTO session_summaries_fts(rowid, request, investigated, learned, completed, next_steps, notes)
|
|
VALUES (new.id, new.request, new.investigated, new.learned, new.completed, new.next_steps, new.notes);
|
|
END;
|
|
`);
|
|
|
|
console.log('✅ Created FTS5 virtual tables and triggers for full-text search');
|
|
},
|
|
|
|
down: (db: Database) => {
|
|
db.run(`
|
|
DROP TRIGGER IF EXISTS observations_au;
|
|
DROP TRIGGER IF EXISTS observations_ad;
|
|
DROP TRIGGER IF EXISTS observations_ai;
|
|
DROP TABLE IF EXISTS observations_fts;
|
|
|
|
DROP TRIGGER IF EXISTS session_summaries_au;
|
|
DROP TRIGGER IF EXISTS session_summaries_ad;
|
|
DROP TRIGGER IF EXISTS session_summaries_ai;
|
|
DROP TABLE IF EXISTS session_summaries_fts;
|
|
`);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Migration 007 - Add discovery_tokens column for ROI metrics
|
|
* Tracks token cost of discovering/creating each observation and summary
|
|
*/
|
|
export const migration007: Migration = {
|
|
version: 7,
|
|
up: (db: Database) => {
|
|
// Add discovery_tokens to observations table
|
|
db.run(`ALTER TABLE observations ADD COLUMN discovery_tokens INTEGER DEFAULT 0`);
|
|
|
|
// Add discovery_tokens to session_summaries table
|
|
db.run(`ALTER TABLE session_summaries ADD COLUMN discovery_tokens INTEGER DEFAULT 0`);
|
|
|
|
console.log('✅ Added discovery_tokens columns for ROI tracking');
|
|
},
|
|
|
|
down: (db: Database) => {
|
|
// Note: SQLite doesn't support DROP COLUMN in all versions
|
|
// In production, would need to recreate tables without these columns
|
|
console.log('⚠️ Warning: SQLite ALTER TABLE DROP COLUMN not fully supported');
|
|
console.log('⚠️ To rollback, manually recreate the observations and session_summaries tables');
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* All migrations in order
|
|
*/
|
|
/**
|
|
* Migration 008: Observation feedback table for tracking observation usage
|
|
*
|
|
* Tracks how observations are used (semantic injection hits, search access,
|
|
* explicit retrieval). Foundation for future Thompson Sampling optimization.
|
|
*/
|
|
export const migration008: Migration = {
|
|
version: 25,
|
|
up: (db: Database) => {
|
|
db.run(`
|
|
CREATE TABLE IF NOT EXISTS observation_feedback (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
observation_id INTEGER NOT NULL,
|
|
signal_type TEXT NOT NULL,
|
|
session_db_id INTEGER,
|
|
created_at_epoch INTEGER NOT NULL,
|
|
metadata TEXT,
|
|
FOREIGN KEY (observation_id) REFERENCES observations(id) ON DELETE CASCADE
|
|
)
|
|
`);
|
|
db.run(`CREATE INDEX IF NOT EXISTS idx_feedback_observation ON observation_feedback(observation_id)`);
|
|
db.run(`CREATE INDEX IF NOT EXISTS idx_feedback_signal ON observation_feedback(signal_type)`);
|
|
console.log('✅ Created observation_feedback table for usage tracking');
|
|
},
|
|
down: (db: Database) => {
|
|
db.run(`DROP TABLE IF EXISTS observation_feedback`);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Migration 009: Add missing columns to observations table
|
|
*
|
|
* The generated_by_model column tracks which model generated each observation
|
|
* (required for model selection optimization via Thompson Sampling).
|
|
* The relevance_count column tracks how many times an observation was reused
|
|
* (incremented by the feedback recording pipeline).
|
|
*
|
|
* Both columns may already exist in databases created by the compiled binary
|
|
* (v10.6.3) but are missing from the migration source. This migration
|
|
* conditionally adds them.
|
|
*/
|
|
export const migration009: Migration = {
|
|
version: 26,
|
|
up: (db: Database) => {
|
|
const columns = db.prepare('PRAGMA table_info(observations)').all() as any[];
|
|
const hasGeneratedByModel = columns.some((c: any) => c.name === 'generated_by_model');
|
|
const hasRelevanceCount = columns.some((c: any) => c.name === 'relevance_count');
|
|
|
|
if (!hasGeneratedByModel) {
|
|
db.run('ALTER TABLE observations ADD COLUMN generated_by_model TEXT');
|
|
}
|
|
if (!hasRelevanceCount) {
|
|
db.run('ALTER TABLE observations ADD COLUMN relevance_count INTEGER DEFAULT 0');
|
|
}
|
|
},
|
|
down: (_db: Database) => {
|
|
// SQLite does not support DROP COLUMN in older versions; no-op
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Migration 010: Label observations (and their queue rows) with the subagent identity.
|
|
*
|
|
* Claude Code hooks that fire inside a subagent carry agent_id and agent_type on the
|
|
* stdin payload. These flow hook → worker → pending_messages → SDK storage so that
|
|
* observation rows can be attributed to the originating subagent. Main-session rows
|
|
* keep NULL for both columns.
|
|
*/
|
|
export const migration010: Migration = {
|
|
version: 27,
|
|
up: (db: Database) => {
|
|
const added: string[] = [];
|
|
|
|
const obsColumns = db.prepare('PRAGMA table_info(observations)').all() as Array<{ name: string }>;
|
|
const obsHasAgentType = obsColumns.some(c => c.name === 'agent_type');
|
|
const obsHasAgentId = obsColumns.some(c => c.name === 'agent_id');
|
|
if (!obsHasAgentType) {
|
|
db.run('ALTER TABLE observations ADD COLUMN agent_type TEXT');
|
|
added.push('observations.agent_type');
|
|
}
|
|
if (!obsHasAgentId) {
|
|
db.run('ALTER TABLE observations ADD COLUMN agent_id TEXT');
|
|
added.push('observations.agent_id');
|
|
}
|
|
db.run('CREATE INDEX IF NOT EXISTS idx_observations_agent_type ON observations(agent_type)');
|
|
db.run('CREATE INDEX IF NOT EXISTS idx_observations_agent_id ON observations(agent_id)');
|
|
|
|
// Also thread the same fields through the pending_messages queue so the label
|
|
// survives worker restarts between enqueue and SDK-agent processing.
|
|
const pendingColumns = db.prepare('PRAGMA table_info(pending_messages)').all() as Array<{ name: string }>;
|
|
if (pendingColumns.length > 0) {
|
|
const pendingHasAgentType = pendingColumns.some(c => c.name === 'agent_type');
|
|
const pendingHasAgentId = pendingColumns.some(c => c.name === 'agent_id');
|
|
if (!pendingHasAgentType) {
|
|
db.run('ALTER TABLE pending_messages ADD COLUMN agent_type TEXT');
|
|
added.push('pending_messages.agent_type');
|
|
}
|
|
if (!pendingHasAgentId) {
|
|
db.run('ALTER TABLE pending_messages ADD COLUMN agent_id TEXT');
|
|
added.push('pending_messages.agent_id');
|
|
}
|
|
}
|
|
|
|
logger.debug(
|
|
'DB',
|
|
added.length > 0
|
|
? `[migration010] Added columns: ${added.join(', ')}`
|
|
: '[migration010] Subagent identity columns already present; ensured indexes'
|
|
);
|
|
},
|
|
down: (_db: Database) => {
|
|
// SQLite DROP COLUMN not fully supported; no-op
|
|
}
|
|
};
|
|
|
|
/**
|
|
* All migrations in order
|
|
*/
|
|
export const migrations: Migration[] = [
|
|
migration001,
|
|
migration002,
|
|
migration003,
|
|
migration004,
|
|
migration005,
|
|
migration006,
|
|
migration007,
|
|
migration008,
|
|
migration009,
|
|
migration010
|
|
];
|