feat(cli): extract Obelisk runtime into npm package
Add @obelisk-apps/cli with the existing build, search, query, and attune contract plus official skill installation. Separate the docs-only skill artifact, bootstrap installer, release layout, cross-platform CI, and package-level regression coverage.
This commit is contained in:
@@ -0,0 +1,469 @@
|
||||
# Obelisk -- Helper API Reference
|
||||
|
||||
Detailed reference for globals available inside `obelisk --query` and
|
||||
`obelisk --attune` scripts.
|
||||
|
||||
- Use `references/schema.md` for raw SQL table/field/join checks.
|
||||
- Use `references/query-patterns.md` for copyable retrieval plans.
|
||||
- Use `references/retrieval-semantics.md` for query design and scope choices.
|
||||
- Use `references/pitfalls.md` after runtime errors or confusing row shapes.
|
||||
|
||||
Query scripts run inside an async IIFE with a 30-second timeout. Use `return` to
|
||||
emit JSON. `--query` scripts are read-only. `--attune` scripts expose only
|
||||
memory mutation helpers.
|
||||
|
||||
## Query API Reference
|
||||
|
||||
### Read Helpers
|
||||
|
||||
These globals are available only in `obelisk --query` scripts:
|
||||
|
||||
```js
|
||||
sql, search, context, trace, thread, raw,
|
||||
overview, sessions, recent, summaries, memories,
|
||||
subagents, workflows, workflowTree, fileHistory, failures
|
||||
```
|
||||
|
||||
All list helpers accept bounded `limit` options. Many helpers also accept
|
||||
`project`, `sessionId`, `sessions`, `after`, `before`, `branch`, and `source`
|
||||
when the underlying table can express that scope. Passing a string to many list
|
||||
helpers is treated as `sessionId`; passing a number is treated as `limit`.
|
||||
|
||||
### Mutation Helpers
|
||||
|
||||
These globals are available only in `obelisk --attune` scripts:
|
||||
|
||||
```js
|
||||
remember, forget
|
||||
```
|
||||
|
||||
`--attune` does not expose `search()`, `sql()`, `memories()`, or other read
|
||||
helpers. If you need IDs, discover them first with a normal `--query` script.
|
||||
|
||||
---
|
||||
|
||||
## Core Helpers
|
||||
|
||||
#### `search(text, opts?)`
|
||||
|
||||
Full-text search across all indexed message text using FTS5.
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `text` | `string` | FTS5 query string |
|
||||
| `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 lower bound on message timestamp |
|
||||
| `opts.before` | `string` | ISO upper bound on message timestamp |
|
||||
| `opts.cwd` | `string` | SQL `LIKE` filter over `messages.cwd` |
|
||||
| `opts.source` | `string` | `"claude"`, `"codex"`, or omitted/all |
|
||||
| `opts.includeMeta` | `boolean` | Include `is_meta=1` rows, default false |
|
||||
|
||||
Returns:
|
||||
|
||||
```js
|
||||
Array<{
|
||||
message: { uuid, text, content_type, is_meta, role, timestamp, model, cwd, source },
|
||||
session: { id, title, project, started_at, source },
|
||||
rank,
|
||||
context
|
||||
}>
|
||||
```
|
||||
|
||||
`context` is temporal neighbor context in the same session, not a parent chain.
|
||||
Use `context(uuid)` or `trace(uuid)` for causal/parent-chain expansion. Lower
|
||||
FTS rank sorts earlier; prefer returned order unless deliberately inspecting
|
||||
FTS ranking.
|
||||
|
||||
Valid FTS5 syntax in `text` is honored. Input that FTS5 would reject as
|
||||
malformed (for example a hyphenated term like `foo-bar`) does not error: it
|
||||
falls back to safe per-token quoting — the same tokenization `memories()` uses —
|
||||
so ordinary text never crashes the query.
|
||||
|
||||
#### `context(uuid)`
|
||||
|
||||
Full indexed context around one message.
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `uuid` | `string` | Message UUID |
|
||||
|
||||
Returns:
|
||||
|
||||
```js
|
||||
{ message, parentChain, session, subagent, workflow } | null
|
||||
```
|
||||
|
||||
`parentChain` contains ancestors, not temporal neighbors. If the message belongs
|
||||
to a subagent or workflow agent, `subagent` or `workflow` is populated when the
|
||||
metadata exists.
|
||||
|
||||
#### `sql(query, ...params)`
|
||||
|
||||
Read-only SQL helper with positional `?` bindings.
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `query` | `string` | `SELECT` or `WITH` statement |
|
||||
| `...params` | `any` | Bind values |
|
||||
|
||||
Returns `Array<object>`.
|
||||
|
||||
Write statements are rejected. Use `references/schema.md` before non-trivial SQL
|
||||
joins, and use `--attune` with `remember()` / `forget()` for memory mutation.
|
||||
|
||||
---
|
||||
|
||||
## Orientation And Lists
|
||||
|
||||
#### `overview(opts?)`
|
||||
|
||||
Compact orientation map for choosing retrieval scope. It is not evidence: it
|
||||
does not return snippets, full messages, or markdown memory contents.
|
||||
|
||||
Passing a string is treated as `project`. Passing a number is treated as
|
||||
`limit`.
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `opts.project` | `string` | Project slug or SQL `LIKE` pattern to use as current scope |
|
||||
| `opts.limit` | `number` | Max recent sessions in `current_project.sessions`, default 8 |
|
||||
| `opts.projectLimit` | `number` | Max global project rows, default 20 |
|
||||
| `opts.memoryLimit` | `number` | Max memories 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:
|
||||
|
||||
```js
|
||||
{
|
||||
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, source }
|
||||
],
|
||||
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,
|
||||
sources: [{ source: 'claude' | 'codex', session_count, last_session_at }]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Confirm facts with `memories()`, `search()`, other helpers, or `sql()`.
|
||||
|
||||
#### `sessions(opts?)`
|
||||
|
||||
Session rows ordered by `ended_at` descending. Passing a number is treated as
|
||||
`limit`.
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `opts.project` | `string` | SQL `LIKE` pattern over `sessions.project` |
|
||||
| `opts.after` | `string` | ISO lower bound on `started_at` |
|
||||
| `opts.before` | `string` | ISO upper bound on `started_at` |
|
||||
| `opts.limit` | `number` | Max rows, default 50 |
|
||||
| `opts.branch` | `string` | Exact git branch |
|
||||
| `opts.source` | `string` | `"claude"`, `"codex"`, or omitted/all |
|
||||
| `opts.sessionId` | `string` | Exact session ID |
|
||||
| `opts.sessions` | `string[]` | Restrict to session IDs |
|
||||
|
||||
Returns `Array<session_row>`.
|
||||
|
||||
#### `recent(n?)`
|
||||
|
||||
Shorthand for `sessions({ limit: n })`. Default `n` is 10.
|
||||
|
||||
Returns `Array<session_row>`.
|
||||
|
||||
#### `summaries(opts?)`
|
||||
|
||||
Session summary rows ordered by summary `timestamp` descending. Passing a string
|
||||
is treated as `sessionId`; passing a number is treated as `limit`.
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `opts.sessionId` | `string` | Restrict to one session |
|
||||
| `opts.sessions` | `string[]` | Restrict to session IDs |
|
||||
| `opts.project` | `string` | SQL `LIKE` pattern over source session project |
|
||||
| `opts.after` | `string` | ISO lower bound on summary timestamp |
|
||||
| `opts.before` | `string` | ISO upper bound on summary timestamp |
|
||||
| `opts.branch` | `string` | Exact source session branch |
|
||||
| `opts.source` | `string` | Provider filter through joined session |
|
||||
| `opts.limit` | `number` | Max rows, default 100 |
|
||||
|
||||
Returns:
|
||||
|
||||
```js
|
||||
Array<summary_row & { session_title, project }>
|
||||
```
|
||||
|
||||
`summaries.source` is the summary kind, such as `away_summary`; it is not the
|
||||
provider source.
|
||||
|
||||
#### `memories(opts?)`
|
||||
|
||||
Active registered markdown memory records. Passing a string is treated as
|
||||
`sessionId`; passing a number is treated as `limit`.
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `opts.query` | `string` | English recall query over `summary` and `path` |
|
||||
| `opts.project` | `string` | SQL `LIKE` pattern over `memories.project` |
|
||||
| `opts.sessionId` | `string` | Restrict to one source session |
|
||||
| `opts.sessions` | `string[]` | Restrict to source session IDs |
|
||||
| `opts.after` | `string` | ISO lower bound on `created_at` |
|
||||
| `opts.before` | `string` | ISO upper bound on `created_at` |
|
||||
| `opts.branch` | `string` | Exact source session branch |
|
||||
| `opts.source` | `string` | Provider filter through the source session |
|
||||
| `opts.limit` | `number` | Max rows, default 50 |
|
||||
|
||||
Returns:
|
||||
|
||||
```js
|
||||
Array<memory_row & { rank?: number }>
|
||||
```
|
||||
|
||||
Archived memories are omitted. Without `query`, rows are newest first. With
|
||||
`query`, rows are ordered by safe FTS rank first, then `created_at` descending.
|
||||
Lower rank sorts earlier. Translate non-English requests into concise English
|
||||
query terms before calling `memories()`. Read the markdown file at `path` for
|
||||
full content.
|
||||
|
||||
---
|
||||
|
||||
## Structural Expansion Helpers
|
||||
|
||||
#### `trace(uuid)`
|
||||
|
||||
Walk the `parent_uuid` chain from a message to the conversation root.
|
||||
|
||||
Returns `Array<message>` ordered root-first.
|
||||
|
||||
#### `thread(sessionId, opts?)`
|
||||
|
||||
Messages in a session ordered by timestamp.
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `sessionId` | `string` | Session ID |
|
||||
| `opts.includeMeta` | `boolean` | Include injected/control-plane rows, default false |
|
||||
|
||||
Returns `Array<message>`. Use `thread()` as a last resort; prefer targeted
|
||||
search/context or compact SQL projections.
|
||||
|
||||
#### `raw(uuid, opts?)`
|
||||
|
||||
Windowed access to the original JSONL line for one indexed message. Use this
|
||||
when indexed text, tool inputs, or tool results were truncated and you need the
|
||||
raw source.
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `uuid` | `string` | Message UUID |
|
||||
| `opts.offset` | `number` | Character offset into the JSONL line, default 0 |
|
||||
| `opts.limit` | `number` | Max characters, default 10000 |
|
||||
|
||||
Returns:
|
||||
|
||||
```js
|
||||
{ text, totalLength, offset, limit, hasMore } | null
|
||||
```
|
||||
|
||||
`raw()` resolves main-session, subagent, workflow-agent, and Codex JSONL paths
|
||||
from indexed metadata.
|
||||
|
||||
---
|
||||
|
||||
## Agent And Workflow Helpers
|
||||
|
||||
#### `subagents(opts?)`
|
||||
|
||||
Subagent metadata plus message counts. Passing a string is treated as
|
||||
`sessionId`.
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `opts.sessionId` | `string` | Restrict to one session |
|
||||
| `opts.project` | `string` | SQL `LIKE` pattern over source session project |
|
||||
| `opts.source` | `string` | Provider filter |
|
||||
| `opts.limit` | `number` | Max rows, default 100 |
|
||||
|
||||
Returns:
|
||||
|
||||
```js
|
||||
Array<{ ...subagent_row, messageCount }>
|
||||
```
|
||||
|
||||
#### `workflows(opts?)`
|
||||
|
||||
Workflow run rows ordered newest first. Passing a string is treated as
|
||||
`sessionId`.
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `opts.sessionId` | `string` | Restrict to one session |
|
||||
| `opts.project` | `string` | SQL `LIKE` pattern over source session project |
|
||||
| `opts.after` | `string` | ISO lower bound on workflow timestamp |
|
||||
| `opts.before` | `string` | ISO upper bound on workflow timestamp |
|
||||
| `opts.source` | `string` | Provider filter |
|
||||
| `opts.limit` | `number` | Max rows, default 100 |
|
||||
|
||||
Returns `Array<workflow_row>`.
|
||||
|
||||
#### `workflowTree(runId)`
|
||||
|
||||
Lightweight execution tree for one workflow run. It parses `result_json` and
|
||||
adds per-agent message counts. It does not load agent messages.
|
||||
|
||||
Returns:
|
||||
|
||||
```js
|
||||
{ ...workflow_row, result: object | null, agents: Array<{ ...workflow_agent_row, messageCount }> } | null
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Evidence Helpers
|
||||
|
||||
#### `fileHistory(filePath, opts?)`
|
||||
|
||||
Tool calls that touched one file, ordered oldest first. Includes `Read` rows as
|
||||
well as `Edit`/`Write`.
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `filePath` | `string` | Absolute file path |
|
||||
| `opts.after` | `string` | ISO lower bound |
|
||||
| `opts.before` | `string` | ISO upper bound |
|
||||
| `opts.source` | `string` | Provider filter |
|
||||
| `opts.limit` | `number` | Max rows, default 200 |
|
||||
|
||||
Returns:
|
||||
|
||||
```js
|
||||
Array<{
|
||||
toolCall: { id, message_uuid, name, input_json },
|
||||
session: { id, title, project },
|
||||
timestamp
|
||||
}>
|
||||
```
|
||||
|
||||
Use raw SQL with `ORDER BY m.timestamp DESC` when you need newest-first file
|
||||
history.
|
||||
|
||||
#### `failures(opts?)`
|
||||
|
||||
Failed tool results with tool/session context and the next three messages after
|
||||
the failure. Passing a string is treated as `sessionId`.
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `opts.sessionId` | `string` | Restrict to one session |
|
||||
| `opts.project` | `string` | SQL `LIKE` pattern over source session project |
|
||||
| `opts.after` | `string` | ISO lower bound on result message timestamp |
|
||||
| `opts.before` | `string` | ISO upper bound on result message timestamp |
|
||||
| `opts.source` | `string` | Provider filter |
|
||||
| `opts.limit` | `number` | Max rows, default 50 |
|
||||
|
||||
Returns:
|
||||
|
||||
```js
|
||||
Array<{ toolCall, result, session, nextMessages }>
|
||||
```
|
||||
|
||||
Use SQL for precise counts and grouping; treat `failures()` as compact evidence,
|
||||
not a counting primitive.
|
||||
|
||||
---
|
||||
|
||||
## Memory Mutation Helpers
|
||||
|
||||
#### `remember(record)`
|
||||
|
||||
Register a human-approved markdown memory file. Available only in
|
||||
`obelisk --attune` scripts.
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `record.path` | `string` | Existing markdown file path |
|
||||
| `record.summary` | `string` | Required English retrieval summary |
|
||||
| `record.session_id` | `string` | Source session ID, if known |
|
||||
| `record.message_start` | `string` | First relevant source message UUID |
|
||||
| `record.message_end` | `string` | Last relevant source message UUID |
|
||||
| `record.project` | `string` | Project slug override |
|
||||
| `record.anchors` | `array` or JSON `string` | Optional recall anchors |
|
||||
|
||||
Relative paths resolve against the source session `project_path` when
|
||||
`session_id` is provided, otherwise against the runtime cwd. `remember()`
|
||||
validates that `path` exists and is a regular file, rejects obvious CJK text in
|
||||
`summary`, stores the normalized absolute path, and accepts nullable `anchors`.
|
||||
|
||||
Returns:
|
||||
|
||||
```js
|
||||
{ id, path, project, anchors, created_at }
|
||||
```
|
||||
|
||||
#### `forget(record)`
|
||||
|
||||
Archive a human-approved memory record. Available only in
|
||||
`obelisk --attune` scripts.
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `record.id` | `string` | Exact memory ID |
|
||||
| `record.reason` | `string` | Required archive reason |
|
||||
|
||||
`forget()` sets `deleted_at` and `deleted_reason`. It does not delete the
|
||||
markdown file at `path`. Active recall helpers omit archived rows.
|
||||
|
||||
Returns:
|
||||
|
||||
```js
|
||||
{ id, deleted_at, deleted_reason } |
|
||||
{ id, deleted_at, deleted_reason, already_deleted: true }
|
||||
```
|
||||
|
||||
### 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 in-place: archive the old record with `forget()`, then write and
|
||||
register a replacement markdown file with `remember()` under the same approval.
|
||||
@@ -0,0 +1,84 @@
|
||||
# Obelisk Pitfalls
|
||||
|
||||
Use this after a query error, suspicious empty result, over-large output, or
|
||||
unclear helper row shape. For query design, read `retrieval-semantics.md` first.
|
||||
|
||||
## Missing Columns And Wrong Aliases
|
||||
|
||||
Common wrong guesses:
|
||||
|
||||
- Summaries: use `source` and `content`; do not use `summary_type` or `text`.
|
||||
- Tool call name: use `tool_calls.name`. `tool_name` is only an alias in `SELECT tc.name AS tool_name`; `tc.tool_name` is not a column.
|
||||
- Tool call timestamps: `tool_calls` has no timestamp. Join `messages m ON m.uuid = tc.message_uuid`.
|
||||
- Tool result timestamps: `tool_results` has no timestamp. Join `messages m ON m.uuid = tr.message_uuid`.
|
||||
- Workflow agent message counts: `workflowTree()` returns `messageCount` for agents.
|
||||
|
||||
When uncertain, inspect a tiny sample instead of guessing:
|
||||
|
||||
```js
|
||||
const rows = summaries({ limit: 1 });
|
||||
return rows.length ? Object.keys(rows[0]) : [];
|
||||
```
|
||||
|
||||
## FTS5 Syntax Errors
|
||||
|
||||
`search(text)` uses raw FTS5 `MATCH`. Hyphenated terms and punctuation can be
|
||||
parsed as syntax.
|
||||
|
||||
```js
|
||||
// tokenized phrase for FTS
|
||||
search('"workflow script"', { limit: 10 })
|
||||
```
|
||||
|
||||
For literal punctuation, use SQL `LIKE` under the same scope:
|
||||
|
||||
```js
|
||||
sql(`
|
||||
SELECT m.uuid, s.id AS session_id, s.title, substr(m.text,1,180) AS snippet
|
||||
FROM messages m
|
||||
JOIN sessions s ON s.id = m.session_id
|
||||
WHERE s.project LIKE ?
|
||||
AND m.text LIKE ?
|
||||
ORDER BY m.timestamp DESC
|
||||
LIMIT 10
|
||||
`, '%quiet-zero%', '%workflow-script%')
|
||||
```
|
||||
|
||||
## Over-Large Runtime JSON
|
||||
|
||||
If runtime stdout is large, fix the query instead of reading it in chunks.
|
||||
|
||||
- Lower `LIMIT`.
|
||||
- Shorten snippets to 160-240 chars.
|
||||
- Group in SQL/JS and return counts plus sparse examples.
|
||||
- For `fileHistory()`, filter to `Edit`/`Write` before projecting evidence.
|
||||
- For `workflowTree()`, omit `script`, `result_json`, and full agent messages unless explicitly requested.
|
||||
- Use `raw(uuid, { offset, limit })` only after identifying one specific message UUID.
|
||||
|
||||
## Empty Results
|
||||
|
||||
An empty array can be the correct answer for exact scopes or sentinels.
|
||||
|
||||
When the user asks for a scoped project/file/session or exact term:
|
||||
|
||||
1. run the scoped query;
|
||||
2. return `[]` or compact counts;
|
||||
3. say no matching prior result was found;
|
||||
4. do not call `recent()`, all-project `summaries()`, or `thread()` as fallback unless the user asks.
|
||||
|
||||
## Counting From Snippets
|
||||
|
||||
If the user asks "how many", "counts", "top N", or "group by", compute it in
|
||||
SQL or in the query script. Do not infer counts from visible snippets.
|
||||
|
||||
```js
|
||||
sql(`
|
||||
SELECT tc.name AS tool_name, COUNT(*) AS n
|
||||
FROM tool_results tr
|
||||
JOIN tool_calls tc ON tc.id = tr.tool_use_id
|
||||
WHERE tr.is_error = 1
|
||||
GROUP BY tc.name
|
||||
ORDER BY n DESC
|
||||
LIMIT 10
|
||||
`)
|
||||
```
|
||||
@@ -0,0 +1,708 @@
|
||||
# Obelisk Query Patterns
|
||||
|
||||
These are copyable CodeAct patterns for `obelisk --query` scripts plus
|
||||
`--attune` memory mutation patterns. They are not new APIs. Adapt them to the
|
||||
user's scope and return compact evidence.
|
||||
|
||||
Read this before the first query for broad synthesis, progress summaries,
|
||||
design history, weekly/monthly reviews, or questions that ask what the user did,
|
||||
learned, decided, tried, or abandoned. Start with a helper-first pass; use raw
|
||||
`sql()` only when the helper surface cannot express the needed join or
|
||||
aggregation.
|
||||
|
||||
## First Pass: Overview + Recall + Evidence
|
||||
|
||||
Use this for broad synthesis before writing custom SQL. It gives the agent a
|
||||
map, prior notes, and raw session evidence in one bounded result. Then run a
|
||||
faceted detail pass if the first pass reveals useful projects, sessions, files,
|
||||
or terms.
|
||||
|
||||
```js
|
||||
const topic = 'English topic terms translated from the user request';
|
||||
const map = overview({ limit: 6 });
|
||||
const project = map.current.project?.project;
|
||||
const scoped = project ? { project } : {};
|
||||
|
||||
return {
|
||||
query_plan: {
|
||||
mode: 'first_pass',
|
||||
topic,
|
||||
project: project || null,
|
||||
limits: { sessions: 6, memories: 5, search: 8 },
|
||||
},
|
||||
orientation: map.current_project && {
|
||||
project: map.current_project.project,
|
||||
session_total: map.current_project.session_total,
|
||||
sessions: map.current_project.sessions.map(s => ({
|
||||
id: s.id,
|
||||
title: s.title,
|
||||
branch: s.git_branch,
|
||||
ended_at: s.ended_at,
|
||||
})),
|
||||
memory_total: map.current_project.memory_total,
|
||||
memories: map.current_project.memories.map(m => ({
|
||||
id: m.id,
|
||||
path: m.path,
|
||||
anchors: m.anchors,
|
||||
summary: m.summary?.slice(0, 240),
|
||||
})),
|
||||
},
|
||||
prior_memories: memories({ ...scoped, query: topic, limit: 5 }).map(m => ({
|
||||
id: m.id,
|
||||
path: m.path,
|
||||
anchors: m.anchors,
|
||||
session_id: m.session_id,
|
||||
created_at: m.created_at,
|
||||
rank: m.rank,
|
||||
summary: m.summary?.slice(0, 260),
|
||||
})),
|
||||
session_evidence: search(topic.replace(/[-_]/g, ' '), { ...scoped, limit: 8 })
|
||||
.slice(0, 6)
|
||||
.map(h => ({
|
||||
session_id: h.session.id,
|
||||
session_title: h.session.title,
|
||||
uuid: h.message.uuid,
|
||||
timestamp: h.message.timestamp,
|
||||
snippet: h.message.text?.slice(0, 220),
|
||||
})),
|
||||
};
|
||||
```
|
||||
|
||||
## Orient Before Retrieval
|
||||
|
||||
Use `overview()` when the current project or available scopes are unclear. Treat
|
||||
the result as a map, not evidence; follow up with `memories()`, `search()`,
|
||||
helpers, or `sql()` for facts.
|
||||
|
||||
```js
|
||||
const map = overview({ limit: 6 });
|
||||
return {
|
||||
current: map.current,
|
||||
current_project: map.current_project && {
|
||||
project: map.current_project.project,
|
||||
session_total: map.current_project.session_total,
|
||||
sessions: map.current_project.sessions.map(s => ({
|
||||
id: s.id,
|
||||
title: s.title,
|
||||
branch: s.git_branch,
|
||||
ended_at: s.ended_at,
|
||||
})),
|
||||
memory_total: map.current_project.memory_total,
|
||||
memories: map.current_project.memories.map(m => ({
|
||||
id: m.id,
|
||||
path: m.path,
|
||||
anchors: m.anchors,
|
||||
summary: m.summary?.slice(0, 240),
|
||||
})),
|
||||
},
|
||||
projects: map.projects.slice(0, 8),
|
||||
totals: map.totals,
|
||||
};
|
||||
```
|
||||
|
||||
## Bounded Search To Context
|
||||
|
||||
Use `search()` to locate candidates, then expand only the strongest hits.
|
||||
|
||||
```js
|
||||
const hits = search('"runtime query"', { project: '%quiet-zero%', limit: 8 });
|
||||
return hits.slice(0, 5).map(h => {
|
||||
const c = context(h.message.uuid);
|
||||
return {
|
||||
session_id: h.session.id,
|
||||
session_title: h.session.title,
|
||||
uuid: h.message.uuid,
|
||||
timestamp: h.message.timestamp,
|
||||
snippet: h.message.text?.slice(0, 240),
|
||||
parentChain: (c?.parentChain || []).slice(-3).map(m => ({
|
||||
uuid: m.uuid,
|
||||
role: m.role,
|
||||
snippet: m.text?.slice(0, 120),
|
||||
})),
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
## Memory Plus Session Evidence
|
||||
|
||||
Use this when prior conclusions may exist but the answer still depends on raw
|
||||
session evidence. Keep memory as prior notes, not final authority; compare it
|
||||
with session evidence in your final answer when correctness matters.
|
||||
Memory query terms are English even when the user asks in another language.
|
||||
|
||||
```js
|
||||
const project = '%quiet-zero%';
|
||||
const topic = 'markdown memory layer';
|
||||
const ftsTopic = topic.replace(/[-_]/g, ' ');
|
||||
|
||||
const prior_memories = memories({
|
||||
project,
|
||||
query: topic,
|
||||
limit: 5,
|
||||
}).map(m => ({
|
||||
id: m.id,
|
||||
path: m.path,
|
||||
anchors: m.anchors,
|
||||
session_id: m.session_id,
|
||||
message_start: m.message_start,
|
||||
message_end: m.message_end,
|
||||
created_at: m.created_at,
|
||||
summary: m.summary?.slice(0, 260),
|
||||
rank: m.rank,
|
||||
}));
|
||||
|
||||
const session_evidence = search(ftsTopic, { project, limit: 8 })
|
||||
.slice(0, 6)
|
||||
.map(h => ({
|
||||
session_id: h.session.id,
|
||||
session_title: h.session.title,
|
||||
uuid: h.message.uuid,
|
||||
timestamp: h.message.timestamp,
|
||||
snippet: h.message.text?.slice(0, 220),
|
||||
}));
|
||||
|
||||
return {
|
||||
query_plan: {
|
||||
project,
|
||||
topic,
|
||||
memory_limit: 5,
|
||||
session_limit: 8,
|
||||
},
|
||||
prior_memories,
|
||||
session_evidence,
|
||||
};
|
||||
```
|
||||
|
||||
## Attune Approved Memory
|
||||
|
||||
Use this only after the user approves writing memory and the markdown file
|
||||
already exists. `remember()` validates the file and stores a normalized absolute
|
||||
path, so keep the script small and return the registered record.
|
||||
|
||||
Run this script with `obelisk --attune <script>`. The `--attune` runtime
|
||||
exposes only `remember()` and `forget()`, not retrieval helpers.
|
||||
|
||||
```js
|
||||
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: 'SKILL.md' }],
|
||||
summary: [
|
||||
'Decision: Obelisk uses one user-facing entry that queries both memory and raw sessions.',
|
||||
'Memory records are prior notes and must be identified naturally when they influence an answer.',
|
||||
'New memory writes require human confirmation before the markdown file is written and registered.',
|
||||
].join(' '),
|
||||
});
|
||||
```
|
||||
|
||||
## Forget Approved Memory
|
||||
|
||||
Use this only after the user asks to archive an outdated or wrong memory. Identify
|
||||
the exact memory ID in a normal `--query` script first. If one candidate clearly
|
||||
matches the user's request, that request is approval to archive it; if several
|
||||
candidates match, ask which one to forget.
|
||||
|
||||
Run the mutation with `obelisk --attune <script>`:
|
||||
|
||||
```js
|
||||
return forget({
|
||||
id: 'mem-id-to-delete',
|
||||
reason: 'Outdated by newer project guidance.',
|
||||
});
|
||||
```
|
||||
|
||||
`forget()` archives the record. Active recall through `memories()` will omit it,
|
||||
and the markdown file at `path` is left in place.
|
||||
|
||||
## Update Approved Memory
|
||||
|
||||
Use this when the user explicitly corrects an existing memory, or after the
|
||||
agent proposes a replacement and the user approves. An update is one combined
|
||||
operation: archive the old record and register the replacement markdown file.
|
||||
The new markdown file must already exist before running `--attune`.
|
||||
|
||||
```js
|
||||
const archived = forget({
|
||||
id: 'old-memory-id',
|
||||
reason: 'Replaced by updated memory from the current session.',
|
||||
});
|
||||
|
||||
const created = remember({
|
||||
path: '.obelisk/memories/updated-memory.md',
|
||||
session_id: 'current-session-id',
|
||||
message_start: 'first-message-uuid',
|
||||
message_end: 'last-message-uuid',
|
||||
anchors: [{ kind: 'file', path: 'src/path/to/file.ts' }],
|
||||
summary: 'Updated summary: concise English retrieval surface for the replacement memory.',
|
||||
});
|
||||
|
||||
return { archived, created };
|
||||
```
|
||||
|
||||
If the agent only suspects a memory is stale, do not run this pattern yet.
|
||||
Answer from current evidence and ask whether to archive or replace the memory.
|
||||
|
||||
## One-Shot Retrieval For Synthesis
|
||||
|
||||
Use this for conclusion, broad history, failure investigation, or file evolution
|
||||
questions. The goal is to reduce conversation turns: keep intermediate search
|
||||
results inside the query script, then return only a compact task-local evidence
|
||||
view. This does not create stored semantic entities; the agent still reads the
|
||||
evidence and forms the conclusion. Expect 1-2 runtime queries: one broad compact
|
||||
evidence pass, and optionally one targeted detail pass by stable IDs.
|
||||
|
||||
```js
|
||||
const project = '%quiet-zero%';
|
||||
const topic = 'obelisk retrieval semantics';
|
||||
const ftsTopic = topic.replace(/[-_]/g, ' ');
|
||||
const facets = [
|
||||
'summary conclusion',
|
||||
'runtime query script',
|
||||
'failure problem',
|
||||
'file change',
|
||||
];
|
||||
|
||||
const candidates = [];
|
||||
for (const facet of facets) {
|
||||
for (const h of search(`${ftsTopic} ${facet}`, { project, limit: 4 })) {
|
||||
candidates.push({
|
||||
kind: 'message',
|
||||
facet,
|
||||
session_id: h.session.id,
|
||||
session_title: h.session.title,
|
||||
uuid: h.message.uuid,
|
||||
timestamp: h.message.timestamp,
|
||||
snippet: h.message.text?.slice(0, 220),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const s of summaries({ project, limit: 8 })) {
|
||||
if (/obelisk|retrieval|context|summary/i.test(`${s.content || ''} ${s.session_title || ''}`)) {
|
||||
candidates.push({
|
||||
kind: 'summary',
|
||||
facet: 'summary',
|
||||
summary_id: s.id,
|
||||
session_id: s.session_id,
|
||||
session_title: s.session_title,
|
||||
timestamp: s.timestamp,
|
||||
snippet: s.content?.slice(0, 240),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const seen = new Set();
|
||||
const evidence = [];
|
||||
for (const row of candidates.sort((a, b) => String(b.timestamp).localeCompare(String(a.timestamp)))) {
|
||||
const key = row.uuid || row.summary_id || `${row.session_id}:${row.timestamp}:${row.facet}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
evidence.push(row);
|
||||
if (evidence.length >= 16) break;
|
||||
}
|
||||
|
||||
return {
|
||||
query_plan: { project, topic, facets, per_facet_limit: 4, max_evidence: 16 },
|
||||
evidence,
|
||||
omitted: Math.max(0, candidates.length - evidence.length),
|
||||
};
|
||||
```
|
||||
|
||||
## Learned Faceted Detail Pass
|
||||
|
||||
Use this after a broad sweep has identified candidate sessions and vocabulary.
|
||||
Prefer detail facets learned from the first pass over pulling large session
|
||||
windows. Fall back to small filtered windows only when the vocabulary is still
|
||||
unclear, and record that reason in `query_plan`.
|
||||
|
||||
```js
|
||||
const sessionIds = [
|
||||
'first-pass-session-id-a',
|
||||
'first-pass-session-id-b',
|
||||
];
|
||||
|
||||
const learnedFacets = [
|
||||
{ facet: 'architecture comparison', terms: ['ultrawork', 'TaskTree', 'parallel'] },
|
||||
{ facet: 'key judgment', terms: ['ridiculous', 'serial', 'parallel'] },
|
||||
{ facet: 'merge direction', terms: ['replan', 'merge', 'workflow'] },
|
||||
{ facet: 'prompt observation', terms: ['prompt', 'guideline', 'skill'] },
|
||||
];
|
||||
|
||||
const rows = [];
|
||||
for (const { facet, terms } of learnedFacets) {
|
||||
const clauses = terms.map(() => 'm.text LIKE ?').join(' OR ');
|
||||
const params = [
|
||||
...sessionIds,
|
||||
...terms.map(t => `%${t}%`),
|
||||
];
|
||||
rows.push(...sql(`
|
||||
SELECT
|
||||
? AS facet,
|
||||
m.uuid,
|
||||
m.session_id,
|
||||
s.title AS session_title,
|
||||
m.timestamp,
|
||||
substr(m.text, 1, 220) AS snippet
|
||||
FROM messages m
|
||||
JOIN sessions s ON s.id = m.session_id
|
||||
WHERE m.session_id IN (${sessionIds.map(() => '?').join(',')})
|
||||
AND m.text IS NOT NULL
|
||||
AND (${clauses})
|
||||
ORDER BY m.timestamp
|
||||
LIMIT 3
|
||||
`, facet, ...params));
|
||||
}
|
||||
|
||||
const seen = new Set();
|
||||
const evidence = [];
|
||||
for (const row of rows) {
|
||||
if (seen.has(row.uuid)) continue;
|
||||
seen.add(row.uuid);
|
||||
evidence.push(row);
|
||||
if (evidence.length >= 12) break;
|
||||
}
|
||||
|
||||
return {
|
||||
query_plan: {
|
||||
mode: 'learned_faceted_detail',
|
||||
source: 'terms discovered in first pass',
|
||||
session_count: sessionIds.length,
|
||||
facets: learnedFacets.map(f => f.facet),
|
||||
per_facet_limit: 3,
|
||||
},
|
||||
evidence,
|
||||
};
|
||||
```
|
||||
|
||||
## Facet Sweep For Broad History
|
||||
|
||||
Use this only for broad synthesis questions such as "how did X evolve", "what
|
||||
did we do on X", or "what problems happened". Do not use it for concept recall,
|
||||
exact session lookup, exact term recall, or tasks that ask for compact search
|
||||
hits.
|
||||
|
||||
Keep the sweep small: 3-4 facets, `limit: 3` per facet, and at most 12 compact
|
||||
evidence rows.
|
||||
|
||||
```js
|
||||
const name = 'obelisk';
|
||||
const facets = [
|
||||
'runtime CLI script',
|
||||
'schema SQLite FTS',
|
||||
'skill API helper docs',
|
||||
'test failure problem',
|
||||
];
|
||||
|
||||
const rows = [];
|
||||
for (const facet of facets) {
|
||||
for (const h of search(`${name} ${facet}`, { project: '%quiet-zero%', limit: 3 })) {
|
||||
rows.push({ facet, h });
|
||||
}
|
||||
}
|
||||
|
||||
const seen = new Set();
|
||||
return rows
|
||||
.filter(({ h }) => {
|
||||
const key = h.message.uuid || `${h.session.id}:${h.message.timestamp}`;
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
})
|
||||
.slice(0, 12)
|
||||
.map(({ facet, h }) => ({
|
||||
facet,
|
||||
session_id: h.session.id,
|
||||
session_title: h.session.title,
|
||||
project: h.session.project,
|
||||
uuid: h.message.uuid,
|
||||
timestamp: h.message.timestamp,
|
||||
snippet: h.message.text?.slice(0, 180),
|
||||
}));
|
||||
```
|
||||
|
||||
## Summary Rows And Neighbors
|
||||
|
||||
Use `source`, `content`, `session_id`, `project`, and `session_title`.
|
||||
|
||||
```js
|
||||
const rows = summaries({ project: '%quiet-zero%', limit: 8 });
|
||||
return rows.map(s => ({
|
||||
id: s.id,
|
||||
session_id: s.session_id,
|
||||
session_title: s.session_title,
|
||||
project: s.project,
|
||||
source: s.source,
|
||||
timestamp: s.timestamp,
|
||||
snippet: s.content?.slice(0, 240),
|
||||
}));
|
||||
```
|
||||
|
||||
To inspect messages around one summary:
|
||||
|
||||
```js
|
||||
const s = summaries({ project: '%quiet-zero%', limit: 1 })[0];
|
||||
if (!s) return { results: [] };
|
||||
const before = sql(
|
||||
`SELECT uuid, role, timestamp, substr(text,1,200) AS snippet
|
||||
FROM messages
|
||||
WHERE session_id=? AND timestamp<?
|
||||
ORDER BY timestamp DESC LIMIT 3`,
|
||||
s.session_id,
|
||||
s.timestamp
|
||||
);
|
||||
const after = sql(
|
||||
`SELECT uuid, role, timestamp, substr(text,1,200) AS snippet
|
||||
FROM messages
|
||||
WHERE session_id=? AND timestamp>?
|
||||
ORDER BY timestamp ASC LIMIT 3`,
|
||||
s.session_id,
|
||||
s.timestamp
|
||||
);
|
||||
return { summary: s, before, after };
|
||||
```
|
||||
|
||||
## File History Synthesis
|
||||
|
||||
`fileHistory()` contains reads as well as writes and old-to-new rows. For
|
||||
"why/how did this file change", scan a bounded `Edit`/`Write` set first, then
|
||||
return only compact evidence. Do not return 20 long snippets; keep runtime JSON
|
||||
small enough that the final answer, not the query output, carries the prose.
|
||||
|
||||
```js
|
||||
const rows = fileHistory('/absolute/path/to/file', { limit: 80 });
|
||||
const writes = rows.filter(r => ['Edit', 'Write'].includes(r.toolCall?.name));
|
||||
const reads = rows.filter(r => r.toolCall?.name === 'Read');
|
||||
const targetTerms = ['summaries', 'failures', 'raw'];
|
||||
|
||||
const bySession = new Map();
|
||||
for (const r of writes) {
|
||||
let input = {};
|
||||
try { input = JSON.parse(r.toolCall.input_json || '{}'); } catch {}
|
||||
const delta = String(input.new_string || input.content || input.old_string || '');
|
||||
const snippet = delta.slice(0, 220);
|
||||
const sid = r.session.id;
|
||||
const group = bySession.get(sid) || {
|
||||
session_id: sid,
|
||||
session_title: r.session.title,
|
||||
project: r.session.project,
|
||||
write_edit_count: 0,
|
||||
first_timestamp: r.timestamp,
|
||||
last_timestamp: r.timestamp,
|
||||
evidence: [],
|
||||
};
|
||||
group.write_edit_count++;
|
||||
group.first_timestamp = group.first_timestamp < r.timestamp ? group.first_timestamp : r.timestamp;
|
||||
group.last_timestamp = group.last_timestamp > r.timestamp ? group.last_timestamp : r.timestamp;
|
||||
if (group.evidence.length < 2) {
|
||||
group.evidence.push({
|
||||
tool: r.toolCall.name,
|
||||
tool_id: r.toolCall.id,
|
||||
timestamp: r.timestamp,
|
||||
mentions: targetTerms.filter(k => delta.toLowerCase().includes(k)),
|
||||
snippet,
|
||||
});
|
||||
}
|
||||
bySession.set(sid, group);
|
||||
}
|
||||
|
||||
const sessions = [...bySession.values()].slice(0, 6);
|
||||
const returnedEvidence = sessions.reduce((n, s) => n + s.evidence.length, 0);
|
||||
return {
|
||||
counts: { reads: reads.length, writes_edits: writes.length },
|
||||
sessions,
|
||||
omitted_write_edit_rows: Math.max(0, writes.length - returnedEvidence),
|
||||
};
|
||||
```
|
||||
|
||||
## Failed Tool Counts
|
||||
|
||||
For precise counts, aggregate in SQL. Do not hand-count long result rows in the
|
||||
final answer.
|
||||
|
||||
```js
|
||||
const counts = sql(`
|
||||
SELECT
|
||||
tc.name AS tool_name,
|
||||
COUNT(*) AS failure_count,
|
||||
MAX(m.timestamp) AS last_failure_at
|
||||
FROM tool_results tr
|
||||
JOIN tool_calls tc ON tc.id = tr.tool_use_id
|
||||
JOIN messages m ON m.uuid = tr.message_uuid
|
||||
JOIN sessions s ON s.id = tr.session_id
|
||||
WHERE tr.is_error = 1
|
||||
AND s.project LIKE ?
|
||||
GROUP BY tc.name
|
||||
ORDER BY failure_count DESC, last_failure_at DESC
|
||||
LIMIT 20
|
||||
`, '%quiet-zero%');
|
||||
|
||||
const examples = sql(`
|
||||
SELECT
|
||||
tr.tool_use_id,
|
||||
tc.name AS tool_name,
|
||||
m.timestamp,
|
||||
s.id AS session_id,
|
||||
s.title AS session_title,
|
||||
substr(tr.content, 1, 180) AS error_snippet
|
||||
FROM tool_results tr
|
||||
JOIN tool_calls tc ON tc.id = tr.tool_use_id
|
||||
JOIN messages m ON m.uuid = tr.message_uuid
|
||||
JOIN sessions s ON s.id = tr.session_id
|
||||
WHERE tr.is_error = 1
|
||||
AND s.project LIKE ?
|
||||
ORDER BY m.timestamp DESC
|
||||
LIMIT 8
|
||||
`, '%quiet-zero%');
|
||||
|
||||
return { counts, examples };
|
||||
```
|
||||
|
||||
## Failure Investigation Groups
|
||||
|
||||
For questions like "recent failed tool calls", "which tasks failed", or "group
|
||||
failures by task/session", group structurally and return sparse examples. Use
|
||||
SQL for counts; treat `failures()` as an evidence helper, not a precise counter.
|
||||
|
||||
```js
|
||||
const project = '%quiet-zero%';
|
||||
|
||||
const groups = sql(`
|
||||
SELECT
|
||||
s.id AS session_id,
|
||||
s.title AS session_title,
|
||||
s.project,
|
||||
COUNT(*) AS failure_count,
|
||||
MAX(m.timestamp) AS last_failure_at
|
||||
FROM tool_results tr
|
||||
JOIN tool_calls tc ON tc.id = tr.tool_use_id
|
||||
JOIN messages m ON m.uuid = tr.message_uuid
|
||||
JOIN sessions s ON s.id = tr.session_id
|
||||
WHERE tr.is_error = 1
|
||||
AND s.project LIKE ?
|
||||
GROUP BY s.id
|
||||
ORDER BY last_failure_at DESC
|
||||
LIMIT 10
|
||||
`, project);
|
||||
|
||||
const examples = sql(`
|
||||
SELECT
|
||||
tr.tool_use_id AS tool_call_id,
|
||||
tc.name AS tool_name,
|
||||
s.id AS session_id,
|
||||
m.timestamp,
|
||||
substr(tr.content, 1, 180) AS error_snippet
|
||||
FROM tool_results tr
|
||||
JOIN tool_calls tc ON tc.id = tr.tool_use_id
|
||||
JOIN messages m ON m.uuid = tr.message_uuid
|
||||
JOIN sessions s ON s.id = tr.session_id
|
||||
WHERE tr.is_error = 1
|
||||
AND s.project LIKE ?
|
||||
ORDER BY m.timestamp DESC
|
||||
LIMIT 12
|
||||
`, project);
|
||||
|
||||
return { groups, examples };
|
||||
```
|
||||
|
||||
## Workflow Tree Compact View
|
||||
|
||||
Find the run with `workflows()` under scope, then project `workflowTree()` into
|
||||
compact fields. Do not return raw `script`, `result_json`, or the full tree.
|
||||
|
||||
```js
|
||||
const runs = workflows({ project: '%quiet-zero%', limit: 30 });
|
||||
const target = runs.find(w =>
|
||||
/session[-_ ]journal/i.test(`${w.workflow_name || ''} ${w.task_id || ''} ${w.run_id || ''}`)
|
||||
);
|
||||
if (!target) {
|
||||
return {
|
||||
found: false,
|
||||
candidates: runs.slice(0, 8).map(w => ({
|
||||
run_id: w.run_id,
|
||||
workflow_name: w.workflow_name,
|
||||
timestamp: w.timestamp,
|
||||
agent_count: w.agent_count,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
const tree = workflowTree(target.run_id);
|
||||
return {
|
||||
run_id: target.run_id,
|
||||
workflow_name: target.workflow_name,
|
||||
status: tree?.status ?? target.status,
|
||||
timestamp: tree?.timestamp ?? target.timestamp,
|
||||
agent_count: tree?.agent_count ?? tree?.agents?.length ?? target.agent_count,
|
||||
agents: (tree?.agents || []).map(a => ({
|
||||
agent_id: a.agent_id,
|
||||
phase: a.phase,
|
||||
label: a.label,
|
||||
state: a.state,
|
||||
tokens: a.tokens,
|
||||
messageCount: a.messageCount,
|
||||
})),
|
||||
};
|
||||
```
|
||||
|
||||
## Subagent Metadata Recall
|
||||
|
||||
Use `subagents()` for metadata. Do not expand transcripts unless the user asks.
|
||||
|
||||
```js
|
||||
const rows = subagents({ project: '%quiet-zero%', limit: 50 });
|
||||
return rows
|
||||
.filter(r => /obelisk/i.test(`${r.description || ''} ${r.agent_type || ''}`))
|
||||
.map(r => ({
|
||||
agent_id: r.agent_id,
|
||||
agent_type: r.agent_type,
|
||||
description: r.description,
|
||||
session_id: r.session_id,
|
||||
messageCount: r.messageCount,
|
||||
total_tokens: r.total_tokens,
|
||||
}));
|
||||
```
|
||||
|
||||
## Empty Result Without Fallback
|
||||
|
||||
If the user asks for an exact sentinel, scoped project, or exact file, an empty
|
||||
result is valid. Report it; do not broaden automatically.
|
||||
|
||||
```js
|
||||
const needle = 'obelisk-impossible-sentinel-20260602';
|
||||
const hits = search(`"${needle.replace(/-/g, ' ')}"`, { limit: 10 });
|
||||
const real = hits.filter(h => {
|
||||
const scope = `${h.session?.project || ''} ${h.message?.cwd || ''}`;
|
||||
return !/SkillOpt[-/. ]outputs|obelisk_train|obelisk-eval/i.test(scope);
|
||||
});
|
||||
return real.map(h => ({
|
||||
session_id: h.session.id,
|
||||
session_title: h.session.title,
|
||||
project: h.session.project,
|
||||
uuid: h.message.uuid,
|
||||
snippet: h.message.text?.slice(0, 200),
|
||||
}));
|
||||
```
|
||||
|
||||
## Raw Window
|
||||
|
||||
Use `raw()` only after identifying a specific message UUID.
|
||||
|
||||
```js
|
||||
const row = sql(`
|
||||
SELECT uuid, length(text) AS indexed_len
|
||||
FROM messages
|
||||
WHERE length(text) >= 10000
|
||||
LIMIT 1
|
||||
`)[0];
|
||||
if (!row) return null;
|
||||
const first = raw(row.uuid, { offset: 0, limit: 4000 });
|
||||
return {
|
||||
uuid: row.uuid,
|
||||
indexed_len: row.indexed_len,
|
||||
totalLength: first?.totalLength,
|
||||
hasMore: first?.hasMore,
|
||||
text: first?.text?.slice(0, 500),
|
||||
};
|
||||
```
|
||||
@@ -0,0 +1,9 @@
|
||||
# Obelisk Recap Retrieval Patterns
|
||||
|
||||
Compatibility pointer.
|
||||
|
||||
The `/obelisk recap` flow now starts at `references/recap/overview.md`.
|
||||
Do not use this as an all-in-one retrieval document. The current flow is
|
||||
card-by-card: read the overview, then for each card read its `patternN-*.md`,
|
||||
retrieve that card's evidence, read its `writingN-*.md`, and update the JSON
|
||||
before moving on.
|
||||
@@ -0,0 +1,8 @@
|
||||
# Obelisk Recap Writing
|
||||
|
||||
Compatibility pointer.
|
||||
|
||||
The `/obelisk recap` writing contract now lives in the per-card writing files
|
||||
under `references/recap/`, coordinated by `references/recap/overview.md`.
|
||||
Read the overview first. Then use each per-card writing file immediately after
|
||||
that card's retrieval pattern, rather than loading one large writing prompt.
|
||||
@@ -0,0 +1,74 @@
|
||||
# Obelisk Recap Overview
|
||||
|
||||
Use this only when the first word after `/obelisk` is `recap`. Everything after
|
||||
`recap` is the target period or style hint.
|
||||
|
||||
## Highest Priority: Phase Loop
|
||||
|
||||
This workflow is sequential. Do not preload all recap files. Do not gather all
|
||||
evidence first and write all cards at the end.
|
||||
|
||||
Follow this loop exactly:
|
||||
|
||||
1. Resolve the target period from the user's phrase.
|
||||
2. Run only a tiny orientation pass such as `overview({ limit: 6 })`.
|
||||
3. For Card 1, read `pattern1-cover.md`.
|
||||
4. Retrieve only Card 1 evidence.
|
||||
5. Read `writing1-cover.md`.
|
||||
6. Update/write the JSON for Card 1 now.
|
||||
7. Only after the JSON is updated, move to Card 2 and repeat.
|
||||
|
||||
Card order:
|
||||
|
||||
| card | retrieve | write |
|
||||
|---|---|---|
|
||||
| 1 cover | `pattern1-cover.md` | `writing1-cover.md` |
|
||||
| 2 thinking | `pattern2-thinking.md` | `writing2-thinking.md` |
|
||||
| 3 vibe | `pattern3-vibe.md` | `writing3-vibe.md` |
|
||||
| 4 workflow | `pattern4-workflow.md` | `writing4-workflow.md` |
|
||||
| 5 closing | `pattern5-closing.md` | `writing5-closing.md` |
|
||||
|
||||
The per-card files own retrieval details, JSON field duties, and card-specific
|
||||
taste. Do not move those concerns back into this file.
|
||||
|
||||
## Period Targets
|
||||
|
||||
- `this week`, `last week`: calendar week in the user's runtime timezone.
|
||||
- `this month`, `last month`: calendar month in the user's runtime timezone.
|
||||
|
||||
Do not infer timezone from examples, UTC suffixes, or file timestamps when
|
||||
runtime/session timezone is available.
|
||||
|
||||
## Overall Deck Taste
|
||||
|
||||
This is a Spotify Wrapped-like set of personal share cards: concise, designed,
|
||||
slightly playful, and built to make the user's work feel seen.
|
||||
|
||||
Do not criticize the user. Do not scold, diagnose, rank their personality, or
|
||||
turn friction into a performance review.
|
||||
|
||||
Use designed English chrome where it feels like card UI: week/month labels,
|
||||
archetype labels, compact stats, verdict seals, and signoffs. Preserve the
|
||||
user's own language for prompts, quotes, catchphrases, and reactions. This is
|
||||
not a translation task.
|
||||
|
||||
The deck should feel like a small artifact from someone who noticed the week,
|
||||
not a report generated from a database.
|
||||
|
||||
## Archetypes
|
||||
|
||||
Choose one dominant archetype from the period's dominant attention, not from the
|
||||
current recap-generation session.
|
||||
|
||||
| archetype | when it fits | tone baseline |
|
||||
|---|---|---|
|
||||
| `architect` | structure, boundaries, schema, systems | matter-of-fact structural pride |
|
||||
| `debugger` | symptoms, false positives, root-cause loops | wry and bug-comfortable |
|
||||
| `shipper` | dense implementation cadence | energetic but not breathless |
|
||||
| `curator` | organization, memory, docs, refinement | reflective and precise |
|
||||
| `director` | workflows, subagents, orchestration | observant from a slight remove |
|
||||
| `cartographer` | moving boundaries and redrawing maps | patient and surveyor-like |
|
||||
| `wanderer` | many projects without one center | gentle, exploratory |
|
||||
|
||||
If two fit, pick the one that describes what the user spent more thinking time
|
||||
on, not what shipped.
|
||||
@@ -0,0 +1,36 @@
|
||||
# Card 1 Cover Retrieval
|
||||
|
||||
Goal: choose the recap's dominant claim, persona, activity shape, and compact
|
||||
footer. The cover is not a topic inventory; it is one glanceable claim about
|
||||
what the period felt like.
|
||||
|
||||
Use the period from `overview.md`. Start from `overview({ limit: 6 })`, then
|
||||
look at in-period sessions, summaries, memories, and any obvious project scope.
|
||||
If the user asked for a project, keep that scope; otherwise prefer the current
|
||||
project only when the evidence makes it the clear center.
|
||||
|
||||
Prefer helpers first. If you need custom SQL for activity, token/message counts,
|
||||
or source-session scope, read `references/schema.md` before writing the SQL.
|
||||
|
||||
Retrieve:
|
||||
|
||||
- dominant claim: one thing that defined the period, supported by raw evidence;
|
||||
- persona: which archetype best matches the user's attention;
|
||||
- source sessions and memories used by this cover claim;
|
||||
- activity: weekly day intensities or monthly day intensities when supported;
|
||||
- footer: compact public metric such as sessions, messages, or tokens.
|
||||
|
||||
Avoid:
|
||||
|
||||
- a claim that lists three topics;
|
||||
- an archetype chosen from the recap-generation session itself;
|
||||
- footer caveats like excluded projects, exact SQL filters, or long session names;
|
||||
- making the cover a workflow metric when the week was really about a decision.
|
||||
|
||||
Read this card's writing file immediately after the cover evidence is stable:
|
||||
`references/recap/writing1-cover.md`. Then update the JSON fields
|
||||
`period`, `source`, `metrics`, `persona`, and the first `cards[]` entry.
|
||||
Do not read `pattern2-thinking.md` until this JSON update is done.
|
||||
|
||||
Stop when the cover has one evidence-backed dominant claim, one chosen persona,
|
||||
one metric scope, and at least one `evidence` anchor.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Card 2 Thinking Retrieval
|
||||
|
||||
Goal: find turning points. This card is not a project timeline and not an implementation log. It is the record of what changed in the user's mind.
|
||||
|
||||
Retrieve 3-6 turns. A turn needs both sides:
|
||||
|
||||
- the user question, friction, doubt, or request that started the turn;
|
||||
- the later decision, reframing, finding, or constraint that replaced the earlier
|
||||
state.
|
||||
|
||||
Useful searches:
|
||||
|
||||
- user questions in the period: "为什么", "是不是", "怎么", "我觉得", "不应该";
|
||||
- places where the user corrected the direction and then approved a new frame;
|
||||
- summaries that name decisions, followed by `context()` or `thread()` for the
|
||||
user's actual words;
|
||||
- memory records only as hints; raw session evidence must provide the prompt and
|
||||
turn.
|
||||
|
||||
Prefer helpers first. If you need custom SQL for message windows or user-turn
|
||||
counts, read `references/schema.md` before writing the SQL.
|
||||
|
||||
Do not use workflow names, feature names, or agent task labels as prompts when
|
||||
the user had their own wording. Do not use counts, "5 rounds", "13 agents", or
|
||||
implementation effort as turns unless that count is the turn itself.
|
||||
|
||||
Read this card's writing file immediately after the turns are chosen:
|
||||
`references/recap/writing2-thinking.md`. Then update the JSON `thinking_path`
|
||||
card and add evidence for each item.
|
||||
Do not read `pattern3-vibe.md` until this JSON update is done.
|
||||
|
||||
Stop when each item has a source-language prompt label, a short changed-state
|
||||
prompt, turn, and an `evidence` anchor.
|
||||
@@ -0,0 +1,30 @@
|
||||
# Card 3 Vibe Retrieval
|
||||
|
||||
Goal: find small human signals in visible user messages. Vibe is not a correction log, not bracketed runtime text, and not a psychological profile.
|
||||
|
||||
Look for:
|
||||
|
||||
- catchphrases and repeated tiny reactions;
|
||||
- unusually blunt praise or rejection;
|
||||
- late-night disbelief, jokes, or rituals;
|
||||
- one quotable sentence that captures the period's character.
|
||||
|
||||
Only count visible user messages. Helper APIs omit meta by default, but custom
|
||||
SQL for phrase counts must filter user text with `COALESCE(m.is_meta,0)=0` and
|
||||
`m.content_type='text'`. Do not count tool results, injected command envelopes,
|
||||
UI labels, or bracketed runtime strings.
|
||||
|
||||
Useful retrieval:
|
||||
|
||||
- targeted phrase counts after you notice a likely catchphrase;
|
||||
- `thread(sessionId)` around high-energy moments;
|
||||
- `search()` for exact phrases, then `context()` for timing;
|
||||
- a bounded SQL count only after reading `references/schema.md`.
|
||||
|
||||
Read this card's writing file immediately after you have the small user signals:
|
||||
`references/recap/writing3-vibe.md`. Then update the JSON `vibe` card and add
|
||||
evidence for every quote, count, and timestamp.
|
||||
Do not read `pattern4-workflow.md` until this JSON update is done.
|
||||
|
||||
Stop when every observation is either exact user words or a tiny label backed by
|
||||
exact user words.
|
||||
@@ -0,0 +1,41 @@
|
||||
# Card 4 Workflow Retrieval
|
||||
|
||||
Goal: find actual workflow runs and how the user received them. Card 4 is about
|
||||
orchestration as experienced by the user, not an agent performance table.
|
||||
|
||||
Workflow rows have their own `workflows.timestamp`. During this card's retrieval,
|
||||
call `workflows({ project: projectLike, after, before })` before concluding the
|
||||
period had zero workflows.
|
||||
|
||||
Prefer helpers first. If you need custom SQL for workflow joins, timestamps, or
|
||||
message reactions, read `references/schema.md` before writing the SQL.
|
||||
|
||||
Do not derive workflow counts only from `sessions({ after, before })`: long
|
||||
sessions can start before the period and still contain workflow runs inside the
|
||||
period. Do not scope workflow lookup by exact `project_path`; nested cwd values
|
||||
can belong to the same Claude project slug.
|
||||
|
||||
For each candidate workflow:
|
||||
|
||||
- get the actual workflow_name from `workflows()` or `workflowTree()`;
|
||||
- collect run id, timestamp, project, agent count, and compact result for stats
|
||||
and evidence only;
|
||||
- search the parent session for the user message immediately following the workflow completion;
|
||||
- use that user reaction as `items[].reaction`.
|
||||
|
||||
Rank rows by the strength of the user reaction, not by agent count, workflow
|
||||
size, duration, or implementation importance. A small workflow with "完美" is a
|
||||
better row than a large workflow with no visible response.
|
||||
|
||||
Do not use architecture topics, memory-system milestones, app modules, or recap
|
||||
feature work as workflow rows unless they are actual workflow_name values.
|
||||
Do not make a row for a workflow with no visible user reaction; keep it only in
|
||||
`stats`, `metrics`, or `evidence`.
|
||||
|
||||
Read this card's writing file immediately after workflow evidence is stable:
|
||||
`references/recap/writing4-workflow.md`. Then update the JSON `workflow` card,
|
||||
top-level workflow metrics, and source session ids for workflow evidence.
|
||||
Do not read `pattern5-closing.md` until this JSON update is done.
|
||||
|
||||
Stop when every displayed row has an actual workflow name and a visible user
|
||||
reaction.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Card 5 Closing Retrieval
|
||||
|
||||
Goal: close with a small personal receipt. Use the same period and source scope
|
||||
as the recap, or explicitly record a wider metric in `evidence`.
|
||||
|
||||
Retrieve:
|
||||
|
||||
- one consistent metric that can stand alone, such as streak, active days,
|
||||
sessions, messages, or workflows;
|
||||
- one or two compact receipts;
|
||||
- most said phrase, only if a real repeated user phrase is supported;
|
||||
- signoff material from the period's mood, not a second summary.
|
||||
|
||||
For phrase counts, count only non-meta visible user text. For streaks and active
|
||||
days, define whether the scope is all Obelisk data, the current project, or the
|
||||
selected evidence sessions. Keep the scope consistent with the cover footer
|
||||
unless the evidence explicitly says otherwise.
|
||||
|
||||
Prefer helpers first. If you need custom SQL for phrase counts, active days, or
|
||||
streaks, read `references/schema.md` before writing the SQL.
|
||||
|
||||
Avoid:
|
||||
|
||||
- naked numbers without units;
|
||||
- project report bullets;
|
||||
- internal session names;
|
||||
- token audits;
|
||||
- slogans, advice, or next-action commands.
|
||||
|
||||
Read this card's writing file immediately after the closing receipt is chosen:
|
||||
`references/recap/writing5-closing.md`. Then update the JSON `closing` card and
|
||||
add evidence for counts and phrases.
|
||||
This is the final card; save the completed JSON before replying.
|
||||
|
||||
Stop when the closing can end the deck without explaining the whole week again.
|
||||
@@ -0,0 +1,83 @@
|
||||
# Card 1 Cover Writing
|
||||
|
||||
The cover should be readable in one glance: badge, persona, one plain claim,
|
||||
activity, footer. Before writing, say the claim to the user in a chat bubble.
|
||||
If it sounds like a topic list or report heading, shrink it.
|
||||
|
||||
## Mock taste anchor
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "cover",
|
||||
"badge": "Week 24",
|
||||
"title": "The Architect",
|
||||
"claim": "从零设计了一个完整的 memory 系统。",
|
||||
"activity": [0.85, 0.92, 0.72, 0.45, 0.88, 0.30, 0],
|
||||
"footer": "12 sessions · 2.4M tokens"
|
||||
}
|
||||
```
|
||||
|
||||
This works because `从零设计了一个完整的 memory 系统。` is one plain claim, in
|
||||
the user's language, and can be read in one breath. `The Architect` is English
|
||||
chrome; it gives the card a designed surface without translating the user's
|
||||
actual work.
|
||||
|
||||
## JSON Shape
|
||||
|
||||
```ts
|
||||
type CoverCard = {
|
||||
type: "cover";
|
||||
badge: string;
|
||||
title: string;
|
||||
claim: string;
|
||||
activity: number[];
|
||||
footer: string;
|
||||
evidence_refs?: string[];
|
||||
};
|
||||
```
|
||||
|
||||
Field duties:
|
||||
|
||||
- `badge`: compact period chrome, such as `Week 24`.
|
||||
- `title`: persona label, usually `The Architect`, `The Debugger`, etc.
|
||||
- `claim`: one plain claim; not a topic list, project inventory, colon-led
|
||||
tagline, or clever English that hides the user's language.
|
||||
- `activity`: period intensity values from retrieval.
|
||||
- `footer`: public metric line with no internal filter notes.
|
||||
|
||||
After writing, check that `persona.claim` and `cover.claim` tell the same
|
||||
story, and attach `evidence_refs` to the claim or metric if it is surprisingly
|
||||
specific.
|
||||
|
||||
## First JSON Write
|
||||
|
||||
After Card 1, create or update the recap JSON file. Do this before reading
|
||||
`pattern2-thinking.md`.
|
||||
|
||||
Use this top-level shape:
|
||||
|
||||
```ts
|
||||
type Recap = {
|
||||
schema_version: "obelisk.recap.v1";
|
||||
kind: "weekly" | "monthly";
|
||||
generated_at: string;
|
||||
period: { label: string; start: string; end: string; timezone: string };
|
||||
source: { project?: string; session_ids: string[]; memory_ids?: string[] };
|
||||
metrics: {
|
||||
sessions?: number;
|
||||
messages?: number;
|
||||
tokens?: number;
|
||||
active_days?: number[];
|
||||
streak_days?: number;
|
||||
workflows?: number;
|
||||
workflow_agents?: number;
|
||||
corrections?: number;
|
||||
};
|
||||
persona: { archetype: string; title: string; claim: string; tone: string };
|
||||
cards: [CoverCard, { type: "thinking_path" }, { type: "vibe" }, { type: "workflow" }, { type: "closing" }];
|
||||
evidence?: Array<{ id: string; session_id?: string; message_uuid?: string; memory_id?: string; summary?: string }>;
|
||||
};
|
||||
```
|
||||
|
||||
For app handoff, write JSON under `~/.obelisk/recap/`. Weekly filenames are
|
||||
`recap-{YYYY}-W{WW}.json`; monthly filenames are `recap-{YYYY}-{MM}.json`.
|
||||
@@ -0,0 +1,49 @@
|
||||
# Card 2 Thinking Writing
|
||||
|
||||
Thinking Path should feel like a few bends in the user's reasoning, not a
|
||||
weekly changelog. Before writing, test each row by asking: "what changed here?"
|
||||
|
||||
## Mock taste anchor
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "thinking_path",
|
||||
"title": "Five questions, five turns.",
|
||||
"items": [
|
||||
{ "day": "Mon", "prompt": "为什么要把 session 编译成 wiki?", "turn": "raw SQLite, no wiki" },
|
||||
{ "day": "Tue", "prompt": "buildWhere 是什么", "turn": "unified filter opts, not DSL" },
|
||||
{ "day": "Wed", "prompt": "failures() 90% 误报", "turn": "is_error in JSONL" },
|
||||
{ "day": "Thu", "prompt": "memory 层需要清理机制吗", "turn": "soft-delete, human-only" },
|
||||
{ "day": "Fri", "prompt": "热力图不选中默认显示本月", "turn": "GitHub-style activity timeline" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The prompts stay close to the user's words. Each turn is a short decision
|
||||
fragment, not a full explanation.
|
||||
|
||||
## JSON Shape
|
||||
|
||||
```ts
|
||||
type ThinkingPathCard = {
|
||||
type: "thinking_path";
|
||||
title: string;
|
||||
items: Array<{
|
||||
day: string;
|
||||
prompt: string;
|
||||
turn: string;
|
||||
evidence_refs?: string[];
|
||||
}>;
|
||||
};
|
||||
```
|
||||
|
||||
Field duties:
|
||||
|
||||
- `title`: designed deck line, not `本周路径`, not a research-paper heading.
|
||||
- `prompt`: user's compact question, friction, or task. Use source language.
|
||||
- `turn`: short decision fragment, finding, or shift; usually under 10 words.
|
||||
Compact English fragments are allowed when they work as designed chrome.
|
||||
|
||||
After writing, remove any row whose prompt is a workflow name or whose turn
|
||||
describes implementation rather than changed thinking.
|
||||
Update the JSON now before reading `pattern3-vibe.md`.
|
||||
@@ -0,0 +1,73 @@
|
||||
# Card 3 Vibe Writing
|
||||
|
||||
Vibe is affectionate observation. It should make the user recognize themselves
|
||||
without feeling evaluated. Before writing, remove anything that reads like a
|
||||
correction audit, behavior label, diagnosis, or complaint ledger.
|
||||
|
||||
## Mock taste anchor
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "vibe",
|
||||
"title": "A short character study.",
|
||||
"voice_lines": [
|
||||
{ "label": "catchphrase", "text": "这太丑了", "count": 4 },
|
||||
{ "label": "highest praise", "text": "可以" },
|
||||
{ "label": "late night", "text": "你在干什么", "time": "02:47 AM" }
|
||||
],
|
||||
"meter": {
|
||||
"label": "patience",
|
||||
"value": 0.78,
|
||||
"caption": "saint"
|
||||
},
|
||||
"quote": {
|
||||
"text": "若无必要,勿增实体。",
|
||||
"caption": "your most philosophical moment"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The humor comes from exact small lines. `可以` is funnier and truer than
|
||||
"approval signal".
|
||||
|
||||
## JSON Shape
|
||||
|
||||
```ts
|
||||
type VibeCard = {
|
||||
type: "vibe";
|
||||
title: string;
|
||||
voice_lines: Array<{
|
||||
label: string;
|
||||
text: string;
|
||||
count?: number;
|
||||
time?: string;
|
||||
evidence_refs?: string[];
|
||||
}>;
|
||||
meter?: {
|
||||
label: string;
|
||||
value: number;
|
||||
caption: string;
|
||||
};
|
||||
quote?: {
|
||||
text: string;
|
||||
caption?: string;
|
||||
evidence_refs?: string[];
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
Field duties:
|
||||
|
||||
- `title`: light character-study line, not a scorecard.
|
||||
- `voice_lines[].text`: exact user words; no paraphrase, translation,
|
||||
ellipsized half-quote, meta text, or correction log.
|
||||
- `voice_lines[].label`: designed chrome can be English; the quoted user text
|
||||
stays in source language.
|
||||
- `meter`: meter is not a diagnosis. Keep the caption one or two words and
|
||||
affectionate, never punitive.
|
||||
- `quote.text`: one exact user phrase or sentence.
|
||||
|
||||
Do not use `[Request interrupted by user]`, tool output, injected context, or
|
||||
UI status text as vibe. After writing, verify every `voice_lines[].text` and
|
||||
`quote.text` can be traced to a non-meta user message.
|
||||
Update the JSON now before reading `pattern4-workflow.md`.
|
||||
@@ -0,0 +1,68 @@
|
||||
# Card 4 Workflow Writing
|
||||
|
||||
Workflow is the orchestration card. It should show the strongest few workflow
|
||||
runs and the user's reaction to them. Before writing, remove any row whose
|
||||
reaction is not traceable to a visible user reaction.
|
||||
|
||||
## Mock taste anchor
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "workflow",
|
||||
"title": "Three workflows. Forty-two agents.",
|
||||
"deck": "你召唤了机器军团。结果各有不同。",
|
||||
"stats": "3 workflows · 42 agents",
|
||||
"items": [
|
||||
{ "name": "hono-plugin-review", "reaction": "完美" },
|
||||
{ "name": "vue-migration", "reaction": "你这页面完全和之前的不一样…" },
|
||||
{ "name": "split-render-js", "reaction": "可以" }
|
||||
],
|
||||
"verdict": "Mostly tolerated."
|
||||
}
|
||||
```
|
||||
|
||||
The row reactions are user reactions. The title carries the metric; the verdict
|
||||
is a small English seal.
|
||||
|
||||
## JSON Shape
|
||||
|
||||
```ts
|
||||
type WorkflowCard = {
|
||||
type: "workflow";
|
||||
title: string;
|
||||
deck?: string;
|
||||
stats?: string;
|
||||
items: Array<{
|
||||
name: string;
|
||||
reaction: string;
|
||||
evidence_refs?: string[];
|
||||
}>;
|
||||
verdict: string;
|
||||
};
|
||||
```
|
||||
|
||||
Field duties:
|
||||
|
||||
- `title`: human story line or compact metric line.
|
||||
- `deck`: optional second line; do not repeat stats mechanically.
|
||||
- `stats`: compact count line.
|
||||
- `items[].name`: actual workflow name, command name, or run-id prefix.
|
||||
- `items[].reaction`: exact or lightly trimmed user reaction. Preserve source
|
||||
language. No feature description, implementation summary, agent count,
|
||||
duration, "framework switch", "modularization", "theming landed", or other
|
||||
internal progress label.
|
||||
- `verdict`: compact seal based on the row reactions, often 3-6 words.
|
||||
|
||||
Agent counts belong only in `title` or `stats`, never in `items[].reaction`.
|
||||
These row values are invalid because they are implementation labels, not user
|
||||
reactions:
|
||||
|
||||
- `13 agents, the big build` is invalid.
|
||||
- `9 agents, framework switch` is invalid.
|
||||
- `6 agents, modularization` is invalid.
|
||||
- `theming landed` is invalid.
|
||||
|
||||
If no user reaction exists, omit the row rather than write an implementation result.
|
||||
After writing, check that every row name maps to retrieval evidence and every
|
||||
reaction can be read as quoted user verdict text.
|
||||
Update the JSON now before reading `pattern5-closing.md`.
|
||||
@@ -0,0 +1,54 @@
|
||||
# Card 5 Closing Writing
|
||||
|
||||
Closing is a receipt, not a second summary. Before writing, read the headline
|
||||
alone. If it does not mean anything without the rest of the card, add the unit
|
||||
or choose a better line.
|
||||
|
||||
## Mock taste anchor
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "closing",
|
||||
"headline": "19 days",
|
||||
"receipts": ["847 messages exchanged", "12 corrections · 47 approvals"],
|
||||
"most_said_phrase": "好的开始做吧",
|
||||
"signoff": "See you next week."
|
||||
}
|
||||
```
|
||||
|
||||
This works because `19 days` has a unit, the receipts feel like a small receipt,
|
||||
and `See you next week.` is a quiet goodbye instead of a slogan.
|
||||
|
||||
## JSON Shape
|
||||
|
||||
```ts
|
||||
type ClosingCard = {
|
||||
type: "closing";
|
||||
headline: string;
|
||||
receipts: string[];
|
||||
most_said_phrase?: string;
|
||||
signoff: string;
|
||||
evidence_refs?: string[];
|
||||
};
|
||||
```
|
||||
|
||||
Field duties:
|
||||
|
||||
- `headline`: compact stat or phrase with its unit; not a naked number.
|
||||
- `receipts`: at most two `receipts`, compact and personal.
|
||||
- `most_said_phrase`: complete phrase the user actually said, or omit it.
|
||||
- `signoff`: short and earned; quiet goodbye, not advice or a brand slogan.
|
||||
English signoff chrome such as `See you next week.` is allowed.
|
||||
|
||||
After writing, remove internal scope notes from visible fields and put them in
|
||||
`evidence`. The final card should feel like the deck ending, not the report
|
||||
continuing.
|
||||
|
||||
Final save rules:
|
||||
|
||||
- The file contains only the JSON object: no Markdown fence, no prose.
|
||||
- Keep exactly five cards in this order: cover, thinking_path, vibe, workflow,
|
||||
closing.
|
||||
- Keep private SQL, raw tool output, secrets, long paths, and source caveats out
|
||||
of visible card text; put traceability in `evidence`.
|
||||
- After saving, reply briefly with the saved path and important evidence caveats.
|
||||
@@ -0,0 +1,165 @@
|
||||
# Obelisk Retrieval Semantics
|
||||
|
||||
Read this before designing a non-trivial query. This is the query design frame;
|
||||
`pitfalls.md` is only the debug checklist.
|
||||
|
||||
## Four Principles
|
||||
|
||||
### Scope First
|
||||
|
||||
Classify the user's request before choosing tools.
|
||||
|
||||
| User signal | Locator mode | Start with | Avoid first |
|
||||
|-------------|--------------|------------|-------------|
|
||||
| unclear project/session landscape | orientation | `overview()` | treating overview rows as evidence |
|
||||
| project name/path, session, cwd, file, time range | scope | `sessions()`, exact SQL on `project_path`, `sessionId`, `fileHistory()` | broad FTS |
|
||||
| workflow, subagent, tool call, summary, edit | artifact | `workflows()`, `subagents()`, `summaries()`, `tool_calls`, `tool_results` | all-session search |
|
||||
| concept, conclusion, design history, vague memory | semantic | `memories({ query })`, `search()`, summaries, bounded facet sweep | session dumps |
|
||||
|
||||
`overview()` is a navigation map: current cwd/project if knowable, global
|
||||
project counts, and recent current-project session/memory entry points. Use it
|
||||
when scope is unclear, then query the memory or raw session layer for evidence.
|
||||
It does not guess the current session.
|
||||
|
||||
For a new task, the first pass normally starts with `overview({ limit: 6 })`
|
||||
unless the user gave an exact session ID, message UUID, or absolute file path.
|
||||
Broad synthesis and progress-summary tasks should start from
|
||||
`references/query-patterns.md`, not raw SQL.
|
||||
|
||||
One-shot retrieval is not all-shot retrieval. A query script may perform
|
||||
multiple steps, but the first locator should be the narrowest semantic fit. If a
|
||||
scope locator finds the relevant project/session/file, do not also run broad FTS
|
||||
unless scoped evidence is insufficient and `query_plan` says why.
|
||||
|
||||
Project-like fields are distinct:
|
||||
|
||||
- `sessions.project`: provider-normalized project slug.
|
||||
- `memories.project`: stored project slug copied onto registered memory records.
|
||||
- `sessions.project_path`: absolute session path derived from message `cwd` when available; slug decoding is only a fallback.
|
||||
- `messages.cwd`: working directory at message time.
|
||||
- `sessions.source` / `messages.source`: transcript provider, currently `claude` or `codex`.
|
||||
- helper `project`: SQL `LIKE` over `sessions.project`, not exact membership.
|
||||
- helper `source`: optional provider filter. Omit it unless provenance matters.
|
||||
|
||||
For exact project membership, prefer helper filters or a scoped first pass when
|
||||
they are expressive enough; use `sql()` with `s.project = ?` or
|
||||
`s.project_path = ?` when you need exact membership across a join or
|
||||
aggregation. Empty or tiny scoped results are valid results; do not broaden
|
||||
unless the user asks or your `query_plan` explicitly marks a fallback.
|
||||
|
||||
### Plan Before Probe
|
||||
|
||||
For conclusion, broad history, failure investigation, or file evolution tasks,
|
||||
prefer a retrieval script over interactive probing.
|
||||
|
||||
Good shape:
|
||||
|
||||
1. locate candidates with scope/artifact/semantic locators;
|
||||
2. expand only selected hits;
|
||||
3. dedupe and group in the script;
|
||||
4. return compact evidence rows plus counts and limits.
|
||||
|
||||
If a second detail pass is needed, derive filters or facets from the first pass:
|
||||
candidate sessions, discovered vocabulary, files, tools, timestamps, or
|
||||
decisions. Prefer a learned faceted detail pass over `LIMIT 25` session windows.
|
||||
If vocabulary is still unclear, use a small filtered window and say so in
|
||||
`query_plan`.
|
||||
|
||||
### Structure Before Text
|
||||
|
||||
Use the database shape before asking the model to read text. This means
|
||||
structured helpers and compact JS shaping first; raw SQL only when it expresses
|
||||
the needed join, grouping, or exact schema-level check better than helpers.
|
||||
|
||||
- Count and aggregate in SQL or JS (`GROUP BY`, `COUNT`, `MAX`, `ORDER BY`, `LIMIT`).
|
||||
- Join metadata from the owner table instead of inventing fields.
|
||||
- Project compact rows; do not return whole sessions, complete workflow trees, full raw messages, or entire tool results.
|
||||
- Keep synthesis runtime JSON around 10k-12k chars when possible.
|
||||
- For recent failures, aggregate by session/task and return sparse examples.
|
||||
- For file evolution, filter `fileHistory()` to `Edit`/`Write`, group by session or phase, and return short deltas.
|
||||
|
||||
Ordering and context are semantic:
|
||||
|
||||
- `sessions()`, `summaries()`, `workflows()`, and `failures()` are newest first.
|
||||
- `memories()` without `query` is newest first; `memories({ query })` is FTS-ranked over memory `summary`/`path`, with lower rank sorting earlier.
|
||||
- `fileHistory()` is oldest first.
|
||||
- `search().context` is temporal neighbors in one session, not causal context.
|
||||
- `context(uuid)` and `trace(uuid)` are for parent-chain/causal expansion.
|
||||
|
||||
### Evidence Before Conclusion
|
||||
|
||||
Obelisk's raw session layer stores original structure, not precompiled claims:
|
||||
sessions, messages, summaries, tool calls/results, files, subagents, workflows,
|
||||
parent chains, and raw JSONL windows. The memory layer can store
|
||||
human-approved markdown conclusions, but treat them as prior notes to compare
|
||||
against raw evidence when correctness matters.
|
||||
|
||||
For semantic questions, build a task-local evidence view:
|
||||
|
||||
```js
|
||||
{
|
||||
query_plan: { mode, scope, facets, limits },
|
||||
prior_memories: [
|
||||
{ id, path, anchors, session_id, created_at, summary }
|
||||
],
|
||||
evidence: [
|
||||
{ type, id, session_id, timestamp, content_type, is_meta, facet, snippet }
|
||||
],
|
||||
omitted: 0
|
||||
}
|
||||
```
|
||||
|
||||
For message evidence, preserve `content_type` when projecting snippets.
|
||||
`text` can support user-visible claims; `thinking` is only trace/debug context;
|
||||
`tool_use` means follow `tool_calls` for structured details; `tool_result`
|
||||
means follow `tool_results` for structured output. Mixed or unfamiliar message
|
||||
surfaces remain `unknown`.
|
||||
|
||||
Preserve `is_meta` separately from `content_type`. Default message evidence
|
||||
should exclude `is_meta=1` rows because they are transcript control-plane
|
||||
content, not ordinary user intent or assistant conclusions. Include them only
|
||||
when investigating injected caveats, command envelopes, or transcript structure.
|
||||
When writing raw SQL for ordinary conversation evidence, add
|
||||
`COALESCE(m.is_meta,0)=0` to message filters unless meta rows are the subject of
|
||||
the investigation.
|
||||
|
||||
Memory recall is English-indexed: translate non-English user requests into
|
||||
concise English query terms before calling `memories({ query })`. Memory
|
||||
summaries registered with `remember()` are also English, regardless of the
|
||||
conversation language.
|
||||
`memories({ query })` uses safe FTS5 tokenization over memory `summary` and
|
||||
`path`, so hyphens and punctuation do not need raw `MATCH` escaping.
|
||||
`memories()` returns active memories only. For raw SQL memory recall, include
|
||||
`deleted_at IS NULL`; archived memory records are management/audit data.
|
||||
|
||||
The agent may decide whether to use, ignore, or verify a recalled memory for the
|
||||
current answer without user approval because no persistent state changes. If a
|
||||
user explicitly says a memory is wrong, outdated, should be forgotten, or should
|
||||
be replaced, that request is approval to mutate the exact matching memory. If
|
||||
the agent discovers the conflict without an explicit user request, it should
|
||||
answer from current evidence and ask before archiving or replacing the memory.
|
||||
|
||||
Then synthesize the conclusion in the final answer. Do not pretend the raw
|
||||
evidence view is itself a stored Obelisk entity.
|
||||
|
||||
After synthesis, check whether the conclusion should become a memory. Offer to
|
||||
write one when the result is durable, likely to help future sessions, and not
|
||||
already covered by `prior_memories`. Good candidates include design decisions,
|
||||
project conventions, abandoned alternatives, repeated failure causes, workflow
|
||||
patterns, and conclusions synthesized across multiple raw evidence points. Do
|
||||
not propose memory for one-off lookups, uncertain findings, or duplicate
|
||||
coverage. The offer is only a proposal: write the markdown file and run
|
||||
`--attune` only after user approval.
|
||||
|
||||
Memory updates are archive-plus-write, not in-place edits: run `forget()` on the
|
||||
old record and `remember()` the replacement markdown file under the same user
|
||||
approval.
|
||||
|
||||
## Text Search Semantics
|
||||
|
||||
`search(text)` passes text to SQLite FTS5 `MATCH`.
|
||||
|
||||
- Hyphens tokenize: for `workflow-script`, use `"workflow script"` or SQL `LIKE` for literal punctuation.
|
||||
- Special characters may produce FTS syntax errors; simplify or quote the FTS query under the same scope.
|
||||
- Exact phrase, token search, and literal punctuation are different semantics.
|
||||
- Results are ordered by `ORDER BY rank`; lower rank sorts earlier. Prefer returned order over "closer to zero" comparisons.
|
||||
@@ -0,0 +1,328 @@
|
||||
# Obelisk -- Raw SQL Quick Reference
|
||||
|
||||
Read this before writing non-trivial `sql()` queries. It is a compact field and
|
||||
join map for raw SQL, not the full helper API manual.
|
||||
|
||||
- Canonical executable DDL: [`packages/core/src/schema.sql`](https://github.com/tommy0103/obelisk/blob/main/packages/core/src/schema.sql) in the CLI source repository (not duplicated in this docs-only skill)
|
||||
- Helper signatures and return shapes: `references/api-reference.md`
|
||||
- Query recipes and synthesis patterns: `references/query-patterns.md`
|
||||
- FTS, alias, ordering, and compactness traps: `references/pitfalls.md`
|
||||
|
||||
Database path: `~/.obelisk/obelisk.sqlite`. Older `~/.claude/obelisk.sqlite`
|
||||
databases are copied forward on first open when the new database does not
|
||||
exist.
|
||||
|
||||
## Source Model
|
||||
|
||||
Obelisk stores Claude Code and Codex transcripts in the same schema.
|
||||
|
||||
- Claude rows use `source='claude'`.
|
||||
- Codex rows use `source='codex'`; root session and message IDs are prefixed
|
||||
with `codex:`.
|
||||
- Omit `source` filters unless provider provenance matters.
|
||||
- Codex child threads are represented through `subagents`; Codex may not have
|
||||
Claude-style workflow rows.
|
||||
|
||||
## Scope Fields
|
||||
|
||||
Use the narrowest scope before text search.
|
||||
|
||||
| Field | Meaning | Raw SQL note |
|
||||
| --- | --- | --- |
|
||||
| `sessions.project` | Provider-normalized project slug | Use `LIKE ?` for fuzzy project filters |
|
||||
| `sessions.project_path` | Absolute project path inferred from cwd | Use for exact local project identity |
|
||||
| `messages.cwd` | Working directory at message time | Useful when a session spans directories |
|
||||
| `sessions.source` / `messages.source` | Transcript provider | Use only when provider matters |
|
||||
| `messages.is_meta` | Injected/control-plane transcript material | Ordinary evidence should filter it out |
|
||||
|
||||
For ordinary conversation evidence in raw SQL, add:
|
||||
|
||||
```sql
|
||||
COALESCE(m.is_meta, 0) = 0
|
||||
```
|
||||
|
||||
Do not add that filter when investigating injected context, command envelopes,
|
||||
or transcript structure.
|
||||
|
||||
## Tables
|
||||
|
||||
### `sessions`
|
||||
|
||||
One row per root session.
|
||||
|
||||
| Column | Meaning |
|
||||
| --- | --- |
|
||||
| `id` | Session ID (`codex:<thread-id>` for Codex roots) |
|
||||
| `title` | AI/session title |
|
||||
| `project` | Provider-normalized project slug |
|
||||
| `project_path` | Absolute project path when known |
|
||||
| `started_at`, `ended_at` | ISO timestamps |
|
||||
| `git_branch` | Branch at session time |
|
||||
| `version` | Provider CLI/app version |
|
||||
| `message_count` | Indexed user + assistant messages |
|
||||
| `jsonl_path` | Source JSONL path |
|
||||
| `source` | `claude` or `codex` |
|
||||
|
||||
### `messages`
|
||||
|
||||
Core evidence table.
|
||||
|
||||
| Column | Meaning |
|
||||
| --- | --- |
|
||||
| `uuid` | Message ID |
|
||||
| `session_id` | FK to `sessions.id` |
|
||||
| `type`, `role` | User/assistant role fields |
|
||||
| `parent_uuid` | Conversation tree parent |
|
||||
| `timestamp` | ISO timestamp |
|
||||
| `text` | Extracted text, truncated to 10k chars |
|
||||
| `content_type` | `text`, `thinking`, `tool_use`, `tool_result`, or `unknown` |
|
||||
| `is_meta` | 1 for injected/control-plane messages |
|
||||
| `model` | Assistant model name |
|
||||
| `is_sidechain` | Retry/branch marker |
|
||||
| `agent_id` | Subagent/workflow agent ID |
|
||||
| `input_tokens`, `output_tokens` | Assistant token usage |
|
||||
| `cwd` | Working directory at message time |
|
||||
| `skill` | Skill that generated the response, if known |
|
||||
| `turn_duration_ms` | Wall-clock duration for the turn |
|
||||
| `source` | `claude` or `codex` |
|
||||
|
||||
`content_type='tool_use'` is only a marker. Tool-call details live in
|
||||
`tool_calls`. `content_type='tool_result'` marks provider-emitted tool-result
|
||||
messages; structured tool-result rows live in `tool_results`.
|
||||
|
||||
### `tool_calls`
|
||||
|
||||
One row per assistant tool invocation.
|
||||
|
||||
| Column | Meaning |
|
||||
| --- | --- |
|
||||
| `id` | Tool-use ID |
|
||||
| `message_uuid` | Assistant message containing the call |
|
||||
| `session_id` | Denormalized session ID |
|
||||
| `name` | Tool name (`Read`, `Edit`, `Bash`, etc.) |
|
||||
| `input_json` | JSON-serialized input, truncated to 10k chars |
|
||||
| `file_path` | Extracted file path for file tools |
|
||||
|
||||
`tool_calls` does not have timestamps. Join through `messages`.
|
||||
|
||||
### `tool_results`
|
||||
|
||||
One row per tool result.
|
||||
|
||||
| Column | Meaning |
|
||||
| --- | --- |
|
||||
| `tool_use_id` | FK to `tool_calls.id` |
|
||||
| `message_uuid` | User/tool-result message carrying the result |
|
||||
| `session_id` | Denormalized session ID |
|
||||
| `content` | Result text, truncated to 10k chars |
|
||||
| `file_path` | Tool result file path metadata, if any |
|
||||
| `is_error` | 1 when the provider marks the result as an error |
|
||||
|
||||
`tool_results` does not have timestamps. Join through `messages`.
|
||||
|
||||
### `summaries`
|
||||
|
||||
Session summary rows.
|
||||
|
||||
| Column | Meaning |
|
||||
| --- | --- |
|
||||
| `id` | Summary ID |
|
||||
| `session_id` | FK to `sessions.id` |
|
||||
| `timestamp` | Summary timestamp |
|
||||
| `source` | Summary kind, such as `away_summary`; not provider source |
|
||||
| `content` | Summary text |
|
||||
|
||||
### `subagents`
|
||||
|
||||
Metadata for non-workflow subagent spawns.
|
||||
|
||||
| Column | Meaning |
|
||||
| --- | --- |
|
||||
| `agent_id` | Subagent ID |
|
||||
| `session_id` | Parent session |
|
||||
| `parent_tool_use_id` | Tool call that spawned the subagent |
|
||||
| `agent_type` | Agent type label |
|
||||
| `description` | Assigned task |
|
||||
| `duration_ms` | Wall-clock duration |
|
||||
| `total_tokens` | Sum of indexed agent tokens |
|
||||
|
||||
### `workflows`
|
||||
|
||||
Workflow execution records.
|
||||
|
||||
| Column | Meaning |
|
||||
| --- | --- |
|
||||
| `run_id` | Workflow run ID |
|
||||
| `session_id` | Parent session |
|
||||
| `task_id` | Task identifier |
|
||||
| `script` | Workflow script content, truncated |
|
||||
| `result_json` | JSON-serialized workflow result |
|
||||
| `timestamp` | Execution timestamp |
|
||||
| `agent_count` | Number of workflow agents |
|
||||
| `duration_ms`, `total_tokens` | Aggregate run cost |
|
||||
| `status` | Run status |
|
||||
| `workflow_name` | Name from workflow metadata |
|
||||
|
||||
### `workflow_agents`
|
||||
|
||||
Individual agents inside a workflow run.
|
||||
|
||||
| Column | Meaning |
|
||||
| --- | --- |
|
||||
| `agent_id` | Workflow agent ID |
|
||||
| `run_id` | FK to `workflows.run_id` |
|
||||
| `session_id` | Parent session |
|
||||
| `agent_type`, `description` | Agent task metadata |
|
||||
| `phase`, `label` | Workflow positioning |
|
||||
| `model`, `state` | Runtime state |
|
||||
| `duration_ms`, `tokens`, `tool_calls` | Per-agent cost |
|
||||
|
||||
### `memories`
|
||||
|
||||
Human-approved markdown memory records. The markdown file at `path` is the
|
||||
durable memory; `summary` is the compact retrieval surface.
|
||||
|
||||
| Column | Meaning |
|
||||
| --- | --- |
|
||||
| `id` | Memory ID |
|
||||
| `session_id` | Source session, if known |
|
||||
| `project` | Project slug for scoped recall |
|
||||
| `message_start`, `message_end` | Source message UUID range |
|
||||
| `path` | Normalized absolute markdown path |
|
||||
| `anchors` | Optional JSON array of recall anchors |
|
||||
| `summary` | English retrieval summary |
|
||||
| `created_at` | Registration timestamp |
|
||||
| `deleted_at` | Archive timestamp |
|
||||
| `deleted_reason` | Archive reason |
|
||||
|
||||
Active memory means `deleted_at IS NULL`. Recall helpers omit archived rows.
|
||||
When using raw SQL for memory recall, include `memories.deleted_at IS NULL`.
|
||||
|
||||
### `index_state`
|
||||
|
||||
Indexer progress and sentinel state.
|
||||
|
||||
| Column | Meaning |
|
||||
| --- | --- |
|
||||
| `jsonl_path` | Source path or synthetic sentinel key |
|
||||
| `mtime` | Last indexed mtime |
|
||||
| `lines_processed` | Incremental line cursor |
|
||||
|
||||
Sentinel keys include `__last_build__`, `__app_heartbeat__`,
|
||||
`__app_last_successful_build__`, `__indexer_owner_app__`, and
|
||||
`__last_source_mtime__`.
|
||||
|
||||
### FTS Tables
|
||||
|
||||
| Table | Search surface | Use |
|
||||
| --- | --- | --- |
|
||||
| `messages_fts` | `messages.text` | Usually through `search()` |
|
||||
| `memories_fts` | `memories.path`, `memories.summary` | Usually through `memories({ query })` |
|
||||
|
||||
Prefer helpers for FTS. Raw `MATCH` syntax is easy to get wrong; see
|
||||
`references/pitfalls.md` before debugging FTS behavior.
|
||||
|
||||
## 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
|
||||
messages.agent_id --> workflow_agents.agent_id
|
||||
tool_calls.id <-- tool_results.tool_use_id
|
||||
workflows.run_id <-- workflow_agents.run_id
|
||||
```
|
||||
|
||||
## Safe SQL Joins
|
||||
|
||||
Tool calls with timestamps:
|
||||
|
||||
```sql
|
||||
SELECT tc.id, tc.name, tc.file_path, m.timestamp, s.title
|
||||
FROM tool_calls tc
|
||||
JOIN messages m ON m.uuid = tc.message_uuid
|
||||
JOIN sessions s ON s.id = tc.session_id
|
||||
WHERE s.project LIKE ?
|
||||
ORDER BY m.timestamp DESC
|
||||
LIMIT 20;
|
||||
```
|
||||
|
||||
Tool failures with timestamps:
|
||||
|
||||
```sql
|
||||
SELECT tr.tool_use_id, tc.name, m.timestamp, substr(tr.content, 1, 200) AS error
|
||||
FROM tool_results tr
|
||||
JOIN tool_calls tc ON tc.id = tr.tool_use_id
|
||||
JOIN messages m ON m.uuid = tr.message_uuid
|
||||
WHERE tr.is_error = 1
|
||||
ORDER BY m.timestamp DESC
|
||||
LIMIT 20;
|
||||
```
|
||||
|
||||
Ordinary message evidence:
|
||||
|
||||
```sql
|
||||
SELECT m.uuid, m.role, m.timestamp, substr(m.text, 1, 220) AS snippet
|
||||
FROM messages m
|
||||
JOIN sessions s ON s.id = m.session_id
|
||||
WHERE s.project LIKE ?
|
||||
AND COALESCE(m.is_meta, 0) = 0
|
||||
ORDER BY m.timestamp DESC
|
||||
LIMIT 20;
|
||||
```
|
||||
|
||||
Active memories:
|
||||
|
||||
```sql
|
||||
SELECT id, path, anchors, summary, session_id, created_at
|
||||
FROM memories
|
||||
WHERE project LIKE ?
|
||||
AND deleted_at IS NULL
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 20;
|
||||
```
|
||||
|
||||
## Indexes
|
||||
|
||||
Common indexed filters:
|
||||
|
||||
- `messages(session_id)`
|
||||
- `messages(agent_id)`
|
||||
- `messages(session_id, timestamp)`
|
||||
- `sessions(source)`
|
||||
- `messages(source)`
|
||||
- `tool_calls(session_id, name)`
|
||||
- `tool_calls(file_path)`
|
||||
- `subagents(session_id)`
|
||||
- `workflows(session_id)`
|
||||
- `workflow_agents(run_id)`
|
||||
- `summaries(session_id)`
|
||||
- `memories(project)`
|
||||
- `memories(session_id)`
|
||||
- `memories(created_at)`
|
||||
|
||||
## Raw SQL Pitfalls
|
||||
|
||||
- Start with helpers. Use raw `sql()` for exact joins, grouping, aggregation, or
|
||||
fields helpers do not expose.
|
||||
- `sql()` accepts only read-only `SELECT`/`WITH`; use `--attune` for memory
|
||||
mutation.
|
||||
- `tool_calls` and `tool_results` do not have timestamps. Join `messages`.
|
||||
- For normal user/assistant evidence, filter `COALESCE(m.is_meta, 0) = 0`.
|
||||
- `summaries.source` is a summary kind, not provider provenance. Provider
|
||||
source is on `sessions.source` and `messages.source`.
|
||||
- `sessions.project` is a slug/fuzzy scope; `sessions.project_path` is the
|
||||
absolute path when known; `messages.cwd` is per-message working directory.
|
||||
- Memory rows are archived with `deleted_at`; do not recall archived memories.
|
||||
- Indexed text and JSON fields are truncated to 10k chars. Use `raw()` from
|
||||
`references/api-reference.md` when a specific message needs the original JSONL
|
||||
line.
|
||||
- Prefer SQL-side `COUNT`, `GROUP BY`, `MAX`, `ORDER BY`, and `LIMIT` over
|
||||
returning large row sets and hand-counting in the final answer.
|
||||
Reference in New Issue
Block a user