Files
obelisk/references/schema.md
T
tommy0103 b524339d85 feat(app): add Electron desktop UI and evolve memory/retrieval layer
Introduce an Electron app with session browser, memory list, and usage
  views (vanilla JS + Vue scaffolding). On the data layer: add content_type
  and is_meta to messages for transcript control-plane filtering, introduce
  FTS5-backed memory recall with safe tokenization, support memory archival
  via forget() through the renamed --attune runtime, and expose anchors on
  memory records.
2026-06-12 22:20:29 +08:00

33 KiB

Obelisk -- Schema and API Reference

Advanced reference for the obelisk database. Read this when search(), context(), or sql() are not enough.


1. Database Schema

Database location: ~/.claude/obelisk.sqlite

sessions

One row per Claude Code session.

CREATE TABLE sessions (
  id            TEXT PRIMARY KEY,   -- session UUID (matches JSONL filename)
  title         TEXT,               -- AI-generated session title (may be NULL)
  project       TEXT,               -- Claude project slug (e.g. "-Users-tomiya-Code-quiet-zero")
  project_path  TEXT,               -- absolute session cwd-derived path, with slug fallback (e.g. "/Users/tomiya/Code/quiet-zero")
  started_at    TEXT,               -- ISO 8601 timestamp of first message
  ended_at      TEXT,               -- ISO 8601 timestamp of last message
  git_branch    TEXT,               -- git branch active during session (if any)
  version       TEXT,               -- Claude Code version string
  message_count INTEGER DEFAULT 0,  -- total user + assistant messages
  jsonl_path    TEXT                -- absolute path to source JSONL file
);

messages

Every user and assistant message. Core table for all queries.

CREATE TABLE messages (
  uuid          TEXT PRIMARY KEY,   -- message UUID
  session_id    TEXT,               -- FK -> sessions.id
  type          TEXT,               -- "user" or "assistant"
  parent_uuid   TEXT,               -- UUID of parent message (conversation tree)
  timestamp     TEXT,               -- ISO 8601
  role          TEXT,               -- "user" or "assistant" (from message payload)
  text          TEXT,               -- extracted text content (thinking + text blocks, truncated to 10k chars)
  content_type  TEXT,               -- "text", "thinking", "tool_use", "tool_result", or "unknown"
  is_meta       INTEGER DEFAULT 0,  -- 1 for injected/control-plane transcript messages
  model         TEXT,               -- model name (e.g. "claude-opus-4-6-20250529"), NULL for user messages
  is_sidechain  INTEGER DEFAULT 0,  -- 1 if this message is on a sidechain (retry/branch)
  agent_id      TEXT,               -- subagent or workflow agent UUID (NULL for main conversation)
  input_tokens  INTEGER,            -- token usage (assistant messages only)
  output_tokens INTEGER,            -- token usage (assistant messages only)
  cwd           TEXT,               -- working directory at message time (may differ from session project_path)
  skill         TEXT,               -- skill that generated this response (e.g. "obelisk"), NULL if none
  turn_duration_ms INTEGER          -- wall-clock duration of the turn ending at this message (from system turn_duration event)
);

Indexes: idx_messages_session(session_id), idx_messages_agent(agent_id), idx_messages_ts(session_id, timestamp).

content_type preserves the top-level Claude Code content block shape for the message row. Treat text as user/assistant visible language, thinking as trace/debug material, and tool_use as a marker that the assistant message contains tool calls. tool_result marks a tool-result message, but the structured payload remains in tool_results. Tool-call details remain in tool_calls. Messages whose top-level content is not one of these four raw message surfaces are unknown. Real user input is represented by type='user' and content_type='text', not by a separate user_message content type.

is_meta marks transcript control-plane content: injected caveats, command envelopes such as <command-name>/exit</command-name>, and similar messages that may appear as user-role text but are not ordinary user intent. It is separate from type, role, and content_type. Default helpers hide meta messages from ordinary recall; use includeMeta: true or explicit SQL when investigating injected context, command messages, or transcript structure.

messages_fts

FTS5 virtual table for full-text search over message text.

CREATE VIRTUAL TABLE messages_fts USING fts5(
  uuid UNINDEXED,        -- not searchable, carried for JOINs
  session_id UNINDEXED,  -- not searchable, carried for filtering
  text,                  -- the searchable column
  content=messages,      -- content-sync with messages table
  content_rowid=rowid
);

Queried via MATCH syntax. Rebuilt on each index pass.

memories_fts

FTS5 virtual table for ranked memory recall over registered memory summaries and paths.

CREATE VIRTUAL TABLE memories_fts USING fts5(
  id UNINDEXED,         -- memory record ID, carried for inspection
  path,                 -- searchable memory file path
  summary,              -- searchable compact memory summary
  content=memories,
  content_rowid=rowid,
  tokenize='unicode61 remove_diacritics 1'
);

memories({ query }) queries this table with safe tokenization and joins back to memories, omitting archived rows. It is rebuilt during index finalization; remember() also inserts the new memory row into FTS immediately.

tool_calls

Every tool invocation by the assistant. One row per tool_use content block.

CREATE TABLE tool_calls (
  id            TEXT PRIMARY KEY,   -- tool_use ID (from API response)
  message_uuid  TEXT,               -- FK -> messages.uuid (the assistant message containing this call)
  session_id    TEXT,               -- FK -> sessions.id (denormalized for fast queries)
  name          TEXT,               -- tool name: "Read", "Edit", "Write", "Bash", "WebSearch", etc.
  input_json    TEXT,               -- JSON-serialized tool input (truncated to 10k chars)
  file_path     TEXT                -- extracted file_path for Read/Edit/Write/NotebookEdit (NULL otherwise)
);

Indexes: idx_tc_session_name(session_id, name), idx_tc_file(file_path).

tool_results

The result returned for each tool call. Appears in the next user message.

CREATE TABLE tool_results (
  tool_use_id   TEXT PRIMARY KEY,   -- FK -> tool_calls.id
  message_uuid  TEXT,               -- FK -> messages.uuid (the user message carrying this result)
  session_id    TEXT,               -- FK -> sessions.id (denormalized)
  content       TEXT,               -- result text (truncated to 10k chars)
  file_path     TEXT,               -- file path from toolUseResult metadata (if any)
  is_error      INTEGER DEFAULT 0   -- 1 if the tool call returned an error (from API is_error field)
);

subagents

Metadata for subagent spawns (non-workflow agents).

CREATE TABLE subagents (
  agent_id          TEXT PRIMARY KEY,   -- subagent UUID
  session_id        TEXT,               -- FK -> sessions.id (parent session)
  parent_tool_use_id TEXT,              -- tool_use ID that spawned this agent
  agent_type        TEXT,               -- e.g. "code-review", "research"
  description       TEXT,               -- task description given to the subagent
  duration_ms       INTEGER,            -- wall-clock duration (computed from message timestamps)
  total_tokens      INTEGER             -- sum of input_tokens + output_tokens across all agent messages
);

Index: idx_sa_session(session_id).

workflows

Workflow execution records. A workflow orchestrates multiple agents.

CREATE TABLE workflows (
  run_id        TEXT PRIMARY KEY,   -- workflow run UUID
  session_id    TEXT,               -- FK -> sessions.id (parent session)
  task_id       TEXT,               -- task identifier (if any)
  script        TEXT,               -- workflow script content (truncated)
  result_json   TEXT,               -- JSON-serialized workflow result
  timestamp     TEXT,               -- ISO 8601 execution time
  agent_count   INTEGER DEFAULT 0,  -- number of agents in this workflow
  duration_ms   INTEGER,            -- wall-clock duration of the workflow run
  total_tokens  INTEGER,            -- total tokens across all agents
  status        TEXT,               -- "completed", "failed", etc.
  workflow_name TEXT                 -- name from the workflow script meta
);

Index: idx_wf_session(session_id).

workflow_agents

Individual agents within a workflow run.

CREATE TABLE workflow_agents (
  agent_id      TEXT PRIMARY KEY,   -- agent UUID (prefixed with "agent-")
  run_id        TEXT,               -- FK -> workflows.run_id
  session_id    TEXT,               -- FK -> sessions.id
  agent_type    TEXT,               -- agent type label
  description   TEXT,               -- task description
  phase         TEXT,               -- workflow phase title (e.g. "Review", "Verify")
  label         TEXT,               -- agent label from workflow script
  model         TEXT,               -- model used (e.g. "claude-opus-4-6[1m]")
  state         TEXT,               -- "done", "error", etc.
  duration_ms   INTEGER,            -- wall-clock duration of this agent
  tokens        INTEGER,            -- total tokens used by this agent
  tool_calls    INTEGER             -- number of tool calls made
);

Index: idx_wa_run(run_id).

index_state

Tracks incremental indexing progress per JSONL file.

CREATE TABLE index_state (
  jsonl_path      TEXT PRIMARY KEY,   -- absolute path to JSONL file
  mtime           REAL,               -- file mtime at last index (milliseconds)
  lines_processed INTEGER             -- number of lines already processed
);

memories

Human-approved markdown memory records registered in Obelisk. The markdown file at path is the durable memory content; summary is the compact retrieval surface.

CREATE TABLE memories (
  id            TEXT PRIMARY KEY,   -- memory record ID
  session_id    TEXT,               -- FK -> sessions.id where the memory was drawn, if known
  project       TEXT,               -- project slug used for scoped recall
  message_start TEXT,               -- first relevant message UUID, if known
  message_end   TEXT,               -- last relevant message UUID, if known
  path          TEXT,               -- normalized absolute markdown memory file path
  anchors       TEXT,               -- optional JSON array of recall anchors
  summary       TEXT,               -- retrieval summary of the memory
  created_at    TEXT,               -- ISO 8601 registration time
  deleted_at    TEXT,               -- ISO 8601 archive time, if forgotten
  deleted_reason TEXT               -- human/agent deletion reason, if forgotten
);

Indexes: idx_memories_project(project), idx_memories_session(session_id), idx_memories_created(created_at).

Active memory means deleted_at IS NULL. Recall helpers return active memories only. Archived memories are management/audit data, not recall data. Query recall uses memories_fts joined back to memories; when using raw SQL for memory recall, include deleted_at IS NULL.

Key Relationships

sessions.id        <--  messages.session_id
sessions.id        <--  tool_calls.session_id
sessions.id        <--  tool_results.session_id
sessions.id        <--  subagents.session_id
sessions.id        <--  workflows.session_id
sessions.id        <--  memories.session_id
messages.uuid      <--  tool_calls.message_uuid
messages.uuid      <--  tool_results.message_uuid
messages.uuid      <--  memories.message_start / memories.message_end
messages.agent_id  -->  subagents.agent_id      (for subagent messages)
messages.agent_id  -->  workflow_agents.agent_id (for workflow agent messages)
tool_calls.id      <--  tool_results.tool_use_id
workflows.run_id   <--  workflow_agents.run_id

2. Query API Reference

Read helpers are available as globals inside --query scripts. Memory mutation helpers are available only inside --attune scripts. Scripts run in an async IIFE with a 30-second timeout.

Simple Layer

search(text, opts?)

Full-text search across all message text using FTS5.

Param Type Description
text string FTS5 query (terms, phrases, prefix)
opts.limit number Max results (default 20)
opts.sessionId string Restrict to one session
opts.project string SQL LIKE pattern over sessions.project
opts.after string ISO 8601 lower bound on timestamp
opts.before string ISO 8601 upper bound on timestamp
opts.cwd string Filter by working directory (supports LIKE)
opts.includeMeta boolean Include injected/control-plane messages (default false)

Scope note: sessions.project is the stored Claude Code project slug, sessions.project_path is the absolute session path derived from message cwd when available, and messages.cwd is the working directory at message time. Helper project filters are fuzzy LIKE filters over sessions.project. For exact project membership, use sql() with s.project = ? or s.project_path = ?.

Returns: Array<{ message, session, rank, context }> where message includes { uuid, text, content_type, is_meta, role, timestamp, model, cwd } and context is the 6 nearest non-meta messages by timestamp in the same session unless includeMeta: true is passed. It is temporal neighbor context, not a parent chain. rank is the FTS5 relevance score used by ORDER BY rank; lower values sort earlier, so treat the returned order as the relevance order unless you are deliberately using FTS5 ranking details.

const hits = search('MCTS exploration');
return hits.map(h => ({
  title: h.session.title,
  content_type: h.message.content_type,
  is_meta: h.message.is_meta,
  text: h.message.text?.slice(0, 200),
}));

context(uuid)

Full context around a single message: parent chain, session metadata, subagent/workflow info.

Param Type Description
uuid string Message UUID

Returns: { message, parentChain, session, subagent, workflow } or null.

const c = context('abc-123-def');
return { chain_length: c.parentChain.length, session_title: c.session?.title };

sql(query, ...params)

Read-only SQL with parameterized bindings. Returns an array of row objects.

Param Type Description
query string SQL SELECT/WITH statement
...params any Bind parameters (positional ?)

Returns: Array<Object> -- each row as { column: value }.

Write statements are rejected. Use --attune with remember() or forget() for memory mutation after user approval.

const rows = sql('SELECT id, title FROM sessions WHERE project = ? ORDER BY ended_at DESC LIMIT 5', 'Users-tomiya-Code-quiet-zero');
return rows;

Advanced Layer

trace(uuid)

Walk the parent_uuid chain from a message up to the conversation root.

Returns: Array<message> ordered root-first.

const chain = trace('some-uuid');
return chain.map(m => ({ role: m.role, text: m.text?.slice(0, 100) }));

thread(sessionId, opts?)

Session messages ordered by timestamp. Meta messages are omitted by default; pass { includeMeta: true } to include injected caveats, command envelopes, and other control-plane transcript rows.

Returns: Array<message>.

const msgs = thread('session-uuid');
return { count: msgs.length, first: msgs[0]?.text?.slice(0, 100) };

subagents(opts?)

All subagent spawns, with message counts. For backward compatibility, passing a string is treated as sessionId.

Param Type Description
opts.sessionId string Restrict to one session
opts.project string SQL LIKE pattern over sessions.project
opts.limit number Max results (default 100)

Returns: Array<{ ...subagent_row, messageCount }>.

const subs = subagents({ project: '%quiet-zero%' });
return subs.map(s => ({ type: s.agent_type, desc: s.description, msgs: s.messageCount, tokens: s.total_tokens }));

workflows(opts?)

Workflow executions. For backward compatibility, passing a string is treated as sessionId.

Param Type Description
opts.sessionId string Restrict to one session
opts.project string SQL LIKE pattern over sessions.project
opts.after string ISO 8601 lower bound on timestamp
opts.before string ISO 8601 upper bound on timestamp
opts.limit number Max results (default 100)

Returns: Array<workflow_row>.

const wfs = workflows({ project: '%quiet-zero%' });
return wfs.map(w => ({ run: w.run_id, agents: w.agent_count, time: w.timestamp }));

workflowTree(runId)

Lightweight execution tree for a workflow: metadata, parsed result, and agent summaries with phase/label/performance data. Does not load agent messages — use sql() with agent_id to drill into a specific agent.

Returns: { ...workflow_row, result: object, agents: Array<{ ...agent_row, messageCount }> } or null.

const tree = workflowTree('run-uuid');
return tree?.agents.map(a => ({ phase: a.phase, label: a.label, tokens: a.tokens, msgs: a.messageCount }));

fileHistory(filePath, opts?)

All tool calls that touched a specific file, across every session.

Param Type Description
filePath string Absolute file path (required)
opts.after string ISO 8601 lower bound
opts.before string ISO 8601 upper bound
opts.limit number Max results (default 200)

Returns: Array<{ toolCall, session, timestamp }>.

Default order is oldest first (ORDER BY m.timestamp). For recent file changes, use raw SQL with ORDER BY m.timestamp DESC.

const edits = fileHistory('/Users/tomiya/Code/quiet-zero/src/mcts.ts', { after: '2026-05-28' });
return edits.map(e => ({ tool: e.toolCall.name, session: e.session.title, time: e.timestamp }));

failures(opts?)

Tool calls whose results contain error patterns (Error, ENOENT, failed, permission denied, etc.). Includes the 3 messages immediately after each failure for retry context. For backward compatibility, passing a string is treated as sessionId.

Param Type Description
opts.sessionId string Restrict to one session
opts.project string SQL LIKE pattern over sessions.project
opts.after string ISO 8601 lower bound
opts.before string ISO 8601 upper bound
opts.limit number Max results (default 50)

Returns: Array<{ toolCall, result, session, nextMessages }>.

Default order is newest first by the result message timestamp.

const fails = failures({ project: '%quiet-zero%', limit: 10 });
return fails.map(f => ({ tool: f.toolCall?.name, error: f.result.content?.slice(0, 200) }));

recent(n?)

Shorthand for sessions({ limit: n }). Last n sessions (default 10), ordered by ended_at descending.

Returns: Array<session_row>.

const last5 = recent(5);
return last5.map(s => ({ title: s.title, project: s.project_path, ended: s.ended_at }));

overview(opts?)

Compact orientation map for choosing the next retrieval scope. It is not an evidence helper: it does not return snippets, full messages, or markdown file contents. Passing a string is treated as project, and passing a number is treated as the current-project session limit.

Param Type Description
opts.project string Project slug or SQL LIKE pattern to use as the current project scope
opts.limit number Max recent sessions in current_project.sessions (default 8)
opts.projectLimit number Max rows in the global projects map (default 20)
opts.memoryLimit number Max memory records in current_project.memories (default 100)

If opts.project is absent, overview() tries to identify the current project from process.cwd() against sessions.project_path, then from exact messages.cwd matches. It does not guess the current session.

Returns:

{
  current: {
    cwd,
    project: {
      project,
      project_path,
      source: 'opts' | 'cwd_project_path' | 'cwd_messages',
      confidence: 'exact' | 'inferred' | 'unknown'
    } | null
  },
  current_project: {
    project,
    project_path,
    session_total,
    sessions: [
      { id, title, project, project_path, started_at, ended_at, git_branch, message_count }
    ],
    memory_total,
    memories: [
      { id, path, anchors, summary, session_id, project, created_at }
    ]
  } | null,
  projects: [
    {
      project,
      project_path,
      session_count,
      memory_count,
      last_session_at,
      last_memory_at,
      recent_branches
    }
  ],
  totals: { projects, sessions, memories }
}

Use current_project.sessions as recent entry points only. session_total and memory_total tell you whether the returned arrays are complete enough for the task. Confirm facts with memories(), search(), other helpers, or sql().

const map = overview({ limit: 5 });
return {
  current: map.current,
  sessions: map.current_project?.sessions.map(s => ({
    id: s.id,
    title: s.title,
    ended_at: s.ended_at,
  })),
  memories: map.current_project?.memories.map(m => ({
    id: m.id,
    path: m.path,
    anchors: m.anchors,
    summary: m.summary,
  })),
};

sessions(opts?)

Query sessions with filters. For backward compatibility, passing a number is treated as limit.

Param Type Description
opts.project string SQL LIKE pattern over sessions.project
opts.after string ISO 8601 lower bound on started_at
opts.before string ISO 8601 upper bound on started_at
opts.limit number Max results (default 50)
opts.branch string Filter by git branch (exact match)
opts.sessionId string Restrict to one session
opts.sessions string[] Restrict to a set of session IDs

Returns: Array<session_row> ordered by ended_at descending.

For exact slug/path membership, use raw SQL with project = ? or project_path = ?.

const qz = sessions({ project: '%quiet-zero%', limit: 5 });
return qz.map(s => ({ title: s.title, branch: s.git_branch, ended: s.ended_at }));

memories(opts?)

Active registered markdown memory records. Like other list helpers, passing a string is treated as sessionId, and passing a number is treated as limit.

Param Type Description
opts.query string English FTS recall query over summary and path; hyphens/underscores/punctuation are safely tokenized
opts.project string SQL LIKE pattern over memories.project
opts.sessionId string Restrict to one source session
opts.sessions string[] Restrict to a set of source session IDs
opts.after string ISO 8601 lower bound on created_at
opts.before string ISO 8601 upper bound on created_at
opts.branch string Filter by source session git branch (exact match)
opts.limit number Max results (default 50)

Returns: Array<memory_row & { rank?: number }> with archived memories omitted. Without query, results are ordered by created_at descending. With query, results are ordered by FTS rank first, then created_at descending; lower rank sorts earlier.

query uses safe FTS5 tokenization rather than raw MATCH, so punctuation-only queries return no rows instead of broadening into all memories. Translate non-English user requests into concise English query terms before calling memories(). Use it to avoid pulling all recent memories, then read the markdown file at path when a memory looks relevant. The runtime rejects obvious CJK text in memory queries.

const prior = memories({
  project: '%quiet-zero%',
  query: 'memory layer markdown',
  limit: 5,
});
return prior.map(m => ({
  id: m.id,
  path: m.path,
  anchors: m.anchors,
  session_id: m.session_id,
  summary: m.summary?.slice(0, 240),
}));

remember(record)

Register a human-approved markdown memory file. This is a write helper, not a recall helper; use it only after the user has approved writing memory. It is available only in scripts run with runtime.mjs --attune.

--attune exposes only remember() and forget(), not search(), sql(), memories(), or other retrieval helpers. If source IDs or memory IDs are unknown, find them first with a normal --query script.

Param Type Description
record.path string Existing markdown file path. Relative paths resolve against the source session project_path when session_id is provided, otherwise against the runtime cwd
record.summary string Required English retrieval summary: decision, reasoning, constraints
record.session_id string Source session ID, if known
record.message_start string First relevant source message UUID, if known
record.message_end string Last relevant source message UUID, if known
record.project string Project slug override. Defaults from sessions.project for session_id
record.anchors array or JSON string Optional recall anchors stored as JSON text. Expected shape is an array of objects, such as { kind: 'file', path: 'src/index/builder.ts' }

remember() validates that path exists and is a regular file, and rejects obvious CJK text in summary. It stores the normalized absolute path in memories.path. anchors is nullable; omit it or pass an empty array when the memory has no explicit file or object anchors.

Returns: { id, path, project, anchors, created_at }.

return remember({
  path: '.obelisk/memories/memory-layer-design.md',
  session_id: 'source-session-id',
  message_start: 'first-message-uuid',
  message_end: 'last-message-uuid',
  anchors: [{ kind: 'file', path: 'src/index/builder.ts' }],
  summary: 'Decision: keep Obelisk as one user-facing entry that queries both memory and raw session evidence. Memory is prior notes, not final authority.',
});

forget(record)

Archive a human-approved memory record. Use it when the user says a memory is outdated, wrong, or should be forgotten. It is available only in scripts run with runtime.mjs --attune.

forget() requires a precise memory ID. Do not pass a query string and let the helper choose. If the ID is unknown, first use a normal --query script with memories() to identify candidates. If exactly one candidate clearly matches the user's request, the request is approval to archive it. If multiple memories could match, ask the user which one to forget.

Param Type Description
record.id string Memory record ID to archive
record.reason string Required reason for audit and future management views

forget() sets deleted_at and deleted_reason. It does not delete the markdown file at path. Active recall helpers omit archived memories.

Returns: { id, deleted_at, deleted_reason }, or the same fields plus already_deleted: true if the record had already been forgotten.

return forget({
  id: 'mem-20260610-example',
  reason: 'Outdated by newer project guidance.',
});

Memory Mutation Approval

Agents may decide whether to use, ignore, or verify memory in a single answer without approval. Approval is required only for persistent memory mutations. When the user explicitly says a memory is wrong, outdated, should be forgotten, or should say something else, that utterance is approval to mutate the exact matching memory. If multiple memories could match, ask the user to choose.

Updating is not an in-place edit. Archive the old record with forget(), then write and register a replacement markdown file with remember() under the same approval.


3. Common Query Patterns

Find sessions about a topic

const hits = search('reinforcement learning');
const sessions = [...new Set(hits.map(h => h.session.id))];
return hits.slice(0, 10).map(h => ({
  session: h.session.title,
  snippet: h.message.text?.slice(0, 150),
}));

Trace a decision chain

// Find a message, then trace its full parent chain to understand how we got there
const hits = search('"switched to PPO"');
if (!hits.length) return 'not found';
const chain = trace(hits[0].message.uuid);
return chain.map(m => ({ role: m.role, text: m.text?.slice(0, 120), ts: m.timestamp }));

Find all edits to a file across sessions

const edits = fileHistory('/Users/tomiya/Code/quiet-zero/src/mcts.ts');
return edits.map(e => ({
  action: e.toolCall.name,
  session: e.session.title,
  time: e.timestamp,
}));

Find churned files (most-edited across all sessions)

const rows = sql(`
  SELECT file_path, COUNT(*) as edit_count, COUNT(DISTINCT session_id) as session_count
  FROM tool_calls
  WHERE file_path IS NOT NULL AND name IN ('Edit','Write')
  GROUP BY file_path
  ORDER BY edit_count DESC
  LIMIT 20
`);
return rows;

Token usage analysis

const rows = sql(`
  SELECT s.id, s.title,
    SUM(m.input_tokens) as total_in,
    SUM(m.output_tokens) as total_out,
    SUM(m.input_tokens) + SUM(m.output_tokens) as total
  FROM messages m JOIN sessions s ON s.id = m.session_id
  WHERE m.input_tokens IS NOT NULL
  GROUP BY s.id
  ORDER BY total DESC
  LIMIT 10
`);
return rows;

Find workflow results

const wfs = workflows();
for (const wf of wfs.slice(0, 3)) {
  const tree = workflowTree(wf.run_id);
  wf.agent_details = tree?.agents.map(a => ({
    type: a.agent_type, desc: a.description, msgs: a.messages.length,
  }));
}
return wfs.slice(0, 3);

Find error patterns

const fails = failures();
// Group by tool name
const byTool = {};
for (const f of fails) {
  const name = f.toolCall?.name || 'unknown';
  byTool[name] = (byTool[name] || 0) + 1;
}
return { total: fails.length, byTool };

Find what tools were used most

const rows = sql(`
  SELECT name, COUNT(*) as call_count, COUNT(DISTINCT session_id) as session_count
  FROM tool_calls
  GROUP BY name
  ORDER BY call_count DESC
`);
return rows;

Find sessions by time range

return sessions({ after: '2026-05-28T00:00:00Z', before: '2026-05-30T00:00:00Z' }).map(s => ({
  title: s.title, project: s.project_path, started: s.started_at, messages: s.message_count,
}));

Cross-reference subagent findings

// See what all subagents did in a session
const subs = subagents('session-uuid');
const details = subs.map(s => {
  const msgs = sql('SELECT text, role FROM messages WHERE agent_id = ? ORDER BY timestamp', s.agent_id);
  return { type: s.agent_type, desc: s.description, summary: msgs.slice(-1)[0]?.text?.slice(0, 300) };
});
return details;

Find all sessions for a project

return sessions({ project: '%quiet-zero%' }).map(s => ({
  title: s.title, started: s.started_at, ended: s.ended_at,
  messages: s.message_count, branch: s.git_branch,
}));

Reconstruct what happened in a session

// Full timeline: messages + tool calls interleaved
const msgs = thread('session-uuid');
return msgs.map(m => {
  const tools = sql('SELECT name, file_path FROM tool_calls WHERE message_uuid = ?', m.uuid);
  return {
    role: m.role, text: m.text?.slice(0, 100), ts: m.timestamp,
    tools: tools.length ? tools.map(t => `${t.name}(${t.file_path || ''})`) : undefined,
  };
});

4. Tips

When to use search() vs sql()

  • search() -- when you are looking for messages containing specific words or phrases. Uses FTS5 under the hood, returns ranked results with surrounding context. Best for: "find where we discussed X", "when did I mention Y".
  • sql() -- when you need structured queries: aggregations, JOINs, GROUP BY, date ranges, or anything involving tables other than messages. Best for: "how many edits to this file", "total tokens this week", "most-used tools".

FTS5 Match Syntax

The text argument to search() uses SQLite FTS5 query syntax:

Pattern Meaning Example
word Match token search('MCTS')
word1 word2 Implicit AND search('MCTS exploration')
"exact phrase" Phrase match search('"Monte Carlo tree"')
word* Prefix match search('optim*') matches optimize, optimizer, optimization
word1 OR word2 Either term search('PPO OR TRPO')
word1 NOT word2 Exclude search('MCTS NOT debug')

Terms are case-insensitive. FTS5 tokenizes on whitespace and punctuation, so camelCase is indexed as two tokens (camel, case).

Performance

  • FTS5 searches are fast (milliseconds) regardless of database size.
  • sql() with indexes is fast. The indexed columns cover the common patterns: messages(session_id), messages(agent_id), messages(session_id, timestamp), tool_calls(session_id, name), tool_calls(file_path).
  • JOINs across large sessions (1000+ messages) can be slow if you join messages with tool_calls and tool_results without filtering by session_id first. Always add a session_id filter when working within a session.
  • Full table scans on tool_results (used by failures() with no session ID) can be slow on large databases because it pattern-matches every result row. Pass a sessionId when possible.
  • Text fields are truncated to 10,000 characters at index time. If you need the full content of a long message or tool result, read the source JSONL directly (path available in sessions.jsonl_path).
  • The database uses WAL mode and NORMAL synchronous, so reads never block writes during re-indexing.