feat(obelisk): index session summaries and guide incremental retrieval
- Add generic summaries table (source-agnostic, ready for Codex)
- Index Claude Code away_summary events as session-level recaps
- Add summaries() query API
- Rewrite retrieval strategy in SKILL.md: never pull entire sessions,
navigate horizontally (by timestamp) or vertically (by parent chain)
This commit is contained in:
@@ -71,8 +71,24 @@ Tables: `sessions`, `messages`, `tool_calls`, `tool_results`, `subagents`, `work
|
|||||||
- `workflowTree(runId)` -- workflow + its agents + all their messages
|
- `workflowTree(runId)` -- workflow + its agents + all their messages
|
||||||
- `fileHistory(filePath)` -- every Edit/Write/Read on a file across sessions
|
- `fileHistory(filePath)` -- every Edit/Write/Read on a file across sessions
|
||||||
- `failures(sessionId?)` -- tool calls that returned errors, with surrounding context
|
- `failures(sessionId?)` -- tool calls that returned errors, with surrounding context
|
||||||
|
- `summaries(sessionId?)` -- session summaries (away recaps, compaction summaries when available)
|
||||||
- `raw(uuid, opts?)` -- windowed access to the original JSONL line (bypasses index truncation)
|
- `raw(uuid, opts?)` -- windowed access to the original JSONL line (bypasses index truncation)
|
||||||
|
|
||||||
|
### Retrieval strategy
|
||||||
|
|
||||||
|
**Never pull an entire session.** Navigate incrementally:
|
||||||
|
|
||||||
|
1. `summaries()` — read session summaries to judge relevance (cheapest)
|
||||||
|
2. `search()` — find specific messages matching a query
|
||||||
|
3. When you find a relevant message and want more context, expand from that point:
|
||||||
|
- **Horizontally**: use `sql()` to fetch neighboring messages by timestamp
|
||||||
|
```js
|
||||||
|
sql('SELECT uuid,role,text FROM messages WHERE session_id=? AND timestamp>? ORDER BY timestamp LIMIT 5', sid, msg.timestamp)
|
||||||
|
```
|
||||||
|
- **Vertically**: use `trace(uuid)` to walk up the parent chain, or `context(uuid)` to see subagent/workflow relationships
|
||||||
|
4. `raw(uuid, opts?)` — recover truncated content from a specific message
|
||||||
|
5. `thread(sessionId)` — full session dump, **last resort only**
|
||||||
|
|
||||||
### raw(uuid, opts?)
|
### raw(uuid, opts?)
|
||||||
|
|
||||||
Some indexed fields (tool call inputs, tool results) are truncated to 10k chars. `raw()` reads the original JSONL line to recover the full content.
|
Some indexed fields (tool call inputs, tool results) are truncated to 10k chars. `raw()` reads the original JSONL line to recover the full content.
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ CREATE TABLE IF NOT EXISTS workflow_agents (
|
|||||||
agent_type TEXT, description TEXT);
|
agent_type TEXT, description TEXT);
|
||||||
CREATE TABLE IF NOT EXISTS index_state (
|
CREATE TABLE IF NOT EXISTS index_state (
|
||||||
jsonl_path TEXT PRIMARY KEY, mtime REAL, lines_processed INTEGER);
|
jsonl_path TEXT PRIMARY KEY, mtime REAL, lines_processed INTEGER);
|
||||||
|
CREATE TABLE IF NOT EXISTS summaries (
|
||||||
|
id TEXT PRIMARY KEY, session_id TEXT, timestamp TEXT,
|
||||||
|
source TEXT, content TEXT);
|
||||||
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
|
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
|
||||||
uuid UNINDEXED, session_id UNINDEXED, text, content=messages, content_rowid=rowid);
|
uuid UNINDEXED, session_id UNINDEXED, text, content=messages, content_rowid=rowid);
|
||||||
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
|
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
|
||||||
@@ -46,6 +49,7 @@ CREATE INDEX IF NOT EXISTS idx_tc_file ON tool_calls(file_path);
|
|||||||
CREATE INDEX IF NOT EXISTS idx_sa_session ON subagents(session_id);
|
CREATE INDEX IF NOT EXISTS idx_sa_session ON subagents(session_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_wf_session ON workflows(session_id);
|
CREATE INDEX IF NOT EXISTS idx_wf_session ON workflows(session_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_wa_run ON workflow_agents(run_id);
|
CREATE INDEX IF NOT EXISTS idx_wa_run ON workflow_agents(run_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_summaries_session ON summaries(session_id);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function openDb() {
|
function openDb() {
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ function indexJsonl(db, fi) {
|
|||||||
msg: db.prepare('INSERT OR REPLACE INTO messages (uuid,session_id,type,parent_uuid,timestamp,role,text,model,is_sidechain,agent_id,input_tokens,output_tokens) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)'),
|
msg: db.prepare('INSERT OR REPLACE INTO messages (uuid,session_id,type,parent_uuid,timestamp,role,text,model,is_sidechain,agent_id,input_tokens,output_tokens) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)'),
|
||||||
tc: db.prepare('INSERT OR REPLACE INTO tool_calls (id,message_uuid,session_id,name,input_json,file_path) VALUES (?,?,?,?,?,?)'),
|
tc: db.prepare('INSERT OR REPLACE INTO tool_calls (id,message_uuid,session_id,name,input_json,file_path) VALUES (?,?,?,?,?,?)'),
|
||||||
tr: db.prepare('INSERT OR REPLACE INTO tool_results (tool_use_id,message_uuid,session_id,content,file_path) VALUES (?,?,?,?,?)'),
|
tr: db.prepare('INSERT OR REPLACE INTO tool_results (tool_use_id,message_uuid,session_id,content,file_path) VALUES (?,?,?,?,?)'),
|
||||||
|
sum: db.prepare('INSERT OR REPLACE INTO summaries (id,session_id,timestamp,source,content) VALUES (?,?,?,?,?)'),
|
||||||
idx: db.prepare('INSERT OR REPLACE INTO index_state (jsonl_path,mtime,lines_processed) VALUES (?,?,?)'),
|
idx: db.prepare('INSERT OR REPLACE INTO index_state (jsonl_path,mtime,lines_processed) VALUES (?,?,?)'),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -85,6 +86,10 @@ function indexJsonl(db, fi) {
|
|||||||
const ts = obj.timestamp || null;
|
const ts = obj.timestamp || null;
|
||||||
|
|
||||||
if (obj.type === 'ai-title' && obj.aiTitle) { sm.title = obj.aiTitle; return; }
|
if (obj.type === 'ai-title' && obj.aiTitle) { sm.title = obj.aiTitle; return; }
|
||||||
|
if (obj.type === 'system' && obj.subtype === 'away_summary' && obj.content) {
|
||||||
|
ins.sum.run(obj.uuid || `${sid}-away-${ts}`, sid, ts, 'away_summary', obj.content);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (obj.type !== 'user' && obj.type !== 'assistant') return;
|
if (obj.type !== 'user' && obj.type !== 'assistant') return;
|
||||||
|
|
||||||
if (ts && (!sm.started_at || ts < sm.started_at)) sm.started_at = ts;
|
if (ts && (!sm.started_at || ts < sm.started_at)) sm.started_at = ts;
|
||||||
|
|||||||
+5
-1
@@ -104,6 +104,10 @@ function createQueryApi(db) {
|
|||||||
|
|
||||||
const recent = (n = 10) => db.prepare('SELECT * FROM sessions ORDER BY ended_at DESC LIMIT ?').all(n);
|
const recent = (n = 10) => db.prepare('SELECT * FROM sessions ORDER BY ended_at DESC LIMIT ?').all(n);
|
||||||
|
|
||||||
|
const summaries = (sessionId) => sessionId
|
||||||
|
? db.prepare('SELECT * FROM summaries WHERE session_id=? ORDER BY timestamp').all(sessionId)
|
||||||
|
: db.prepare('SELECT su.*, s.title as session_title, s.project FROM summaries su LEFT JOIN sessions s ON s.id=su.session_id ORDER BY su.timestamp DESC').all();
|
||||||
|
|
||||||
const resolveJsonlPath = (messageUuid) => {
|
const resolveJsonlPath = (messageUuid) => {
|
||||||
const msg = db.prepare('SELECT session_id, agent_id FROM messages WHERE uuid=?').get(messageUuid);
|
const msg = db.prepare('SELECT session_id, agent_id FROM messages WHERE uuid=?').get(messageUuid);
|
||||||
if (!msg) return null;
|
if (!msg) return null;
|
||||||
@@ -149,7 +153,7 @@ function createQueryApi(db) {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
return { sql: q, search, context, trace, thread, subagents, workflows, workflowTree, fileHistory, failures, recent, raw };
|
return { sql: q, search, context, trace, thread, subagents, workflows, workflowTree, fileHistory, failures, recent, summaries, raw };
|
||||||
}
|
}
|
||||||
|
|
||||||
export { createQueryApi };
|
export { createQueryApi };
|
||||||
|
|||||||
Reference in New Issue
Block a user