-- Lethe schema v1. -- -- FTS5 mode choice: REGULAR (no `content=` clause). -- -- Why not contentless (`content=''`): contentless FTS5 tables do not store -- UNINDEXED column values either, which defeats `WHERE owner = ?` filtering -- without a join back to `turns`. UNINDEXED columns are only meaningful in a -- regular or external-content FTS5 table. -- -- Why not external content (`content='turns'`): SQLite's -- `INSERT ... ON CONFLICT ... DO UPDATE` upsert path fires the UPDATE -- trigger (not INSERT) when the conflict branch is taken, and across SQLite -- versions the external-content trigger contract has had subtle issues -- around UPSERT ordering. Phase 7 (ingest) uses upserts heavily, so we -- avoid that risk here. -- -- Regular FTS5 stores `content` twice (once in `turns`, once in the FTS -- shadow table); the storage cost is the price for trigger-correctness -- robustness across SQLite versions. Triggers are the only writers -- -- never INSERT/UPDATE/DELETE *_fts directly outside this file. -- -- Trigger semantics: each trigger is keyed by the source-table rowid (every -- non-WITHOUT-ROWID table has one). On UPDATE we DELETE then INSERT to -- avoid an UPDATE-in-place edge case that has historically misbehaved in -- some FTS5 builds. UPSERT (INSERT ... ON CONFLICT DO UPDATE) fires the -- UPDATE trigger when the conflict branch is taken; the DELETE+INSERT -- pattern keeps FTS in sync regardless. CREATE TABLE sessions ( owner TEXT NOT NULL, tool TEXT NOT NULL, host TEXT NOT NULL, session_id TEXT NOT NULL, started_at INTEGER NOT NULL, ended_at INTEGER NOT NULL, working_dir TEXT, source_file TEXT NOT NULL, metadata TEXT, PRIMARY KEY (owner, tool, host, session_id) ); CREATE INDEX sessions_owner_started ON sessions(owner, started_at DESC); CREATE TABLE turns ( owner TEXT NOT NULL, tool TEXT NOT NULL, host TEXT NOT NULL, session_id TEXT NOT NULL, turn_id TEXT NOT NULL, seq INTEGER NOT NULL, role TEXT NOT NULL, timestamp INTEGER NOT NULL, content TEXT NOT NULL, model TEXT, tokens_in INTEGER, tokens_out INTEGER, cost_usd REAL, tool_calls TEXT, metadata TEXT, PRIMARY KEY (owner, tool, host, session_id, turn_id), FOREIGN KEY (owner, tool, host, session_id) REFERENCES sessions(owner, tool, host, session_id) ON DELETE CASCADE ); -- FTS5 over turn prose `content`. UNINDEXED columns let `WHERE owner = ?` -- filter pre-FTS without a join back to `turns`. CREATE VIRTUAL TABLE turns_fts USING fts5 ( content, owner UNINDEXED, tool UNINDEXED, host UNINDEXED, session_id UNINDEXED, turn_id UNINDEXED ); CREATE TRIGGER turns_fts_insert AFTER INSERT ON turns BEGIN INSERT INTO turns_fts(rowid, content, owner, tool, host, session_id, turn_id) VALUES (new.rowid, new.content, new.owner, new.tool, new.host, new.session_id, new.turn_id); END; CREATE TRIGGER turns_fts_update AFTER UPDATE ON turns BEGIN DELETE FROM turns_fts WHERE rowid = old.rowid; INSERT INTO turns_fts(rowid, content, owner, tool, host, session_id, turn_id) VALUES (new.rowid, new.content, new.owner, new.tool, new.host, new.session_id, new.turn_id); END; CREATE TRIGGER turns_fts_delete AFTER DELETE ON turns BEGIN DELETE FROM turns_fts WHERE rowid = old.rowid; END; -- FTS5 over `tool_calls` JSON text. INSERT trigger fires only when the -- column is non-NULL; UPDATE trigger always deletes any prior row, then -- re-inserts only if NEW.tool_calls is non-NULL (covers all four NULL/ -- non-NULL transitions). CREATE VIRTUAL TABLE tool_outputs_fts USING fts5 ( tool_calls, owner UNINDEXED, tool UNINDEXED, host UNINDEXED, session_id UNINDEXED, turn_id UNINDEXED ); CREATE TRIGGER tool_outputs_fts_insert AFTER INSERT ON turns WHEN new.tool_calls IS NOT NULL BEGIN INSERT INTO tool_outputs_fts(rowid, tool_calls, owner, tool, host, session_id, turn_id) VALUES (new.rowid, new.tool_calls, new.owner, new.tool, new.host, new.session_id, new.turn_id); END; CREATE TRIGGER tool_outputs_fts_update AFTER UPDATE ON turns BEGIN DELETE FROM tool_outputs_fts WHERE rowid = old.rowid; INSERT INTO tool_outputs_fts(rowid, tool_calls, owner, tool, host, session_id, turn_id) SELECT new.rowid, new.tool_calls, new.owner, new.tool, new.host, new.session_id, new.turn_id WHERE new.tool_calls IS NOT NULL; END; CREATE TRIGGER tool_outputs_fts_delete AFTER DELETE ON turns BEGIN DELETE FROM tool_outputs_fts WHERE rowid = old.rowid; END;