Data & storage
Query API#
The read surface is a declared contract called q/1. It is not a loose
collection of endpoints: every shape, its arguments, its defaults and its limits
are registered, and the HTTP API, the daemon proxy, the MCP server and the cloud
relay all serve the same registry.
That is why an agent asking over MCP and a script calling curl get identical
rows.
Evolution rule#
Inside q/1, evolution is additive only. New shapes and new optional
arguments may appear. Renaming or removing a shape, an argument, or a response
field requires a new contract version.
Live shapes#
| Shape | Arguments | Returns |
|---|---|---|
sessions | agent_id, since, until, limit (100, max 2000) | One row per session with start/end, event count, cost |
events | session_id, agent_id, event_type, since, until, limit (200, max 5000) | Raw event rows, newest first |
transcript | session_id required, limit (500, max 5000) | Events scoped to one session |
aggregates | agent_id, since, until | Per-day rollup of events, tokens and cost |
health | — | Store health: engine, size, ring depth, flush age |
spans | trace_id, session_id, agent_type, since, until, limit (200, max 2000) | OTel span rows |
traces | session_id, agent_type, since, until, limit (100, max 1000) | One row per trace with aggregate span stats |
external_calls | session_id, since, until, limit (200, max 2000) | Non-LLM API calls captured by the interceptor |
models | runtime, since, until, limit (1000, max 10000) | Per-model daily token and cost rollup |
runtimes | since, until, limit (1000, max 10000) | Per-runtime daily activity and cost rollup |
rollup_sessions | runtime, limit (200, max 2000) | Materialised per-session summary: title, status, totals, stuck flag |
search | q required, model, status, since, until, limit (50, max 500) | Full-text search over session titles and eval reasons |
agent_graph | since, until, limit (500, max 2000) | Spawn graph: nodes plus spawn edges |
Arguments are strictly coerced: anything not in a shape's allowed-key set is dropped, and limits are clamped to their range rather than erroring. An agent asking for a million events gets the cap.
Trust classes#
Every shape is classified, and the classification governs what may leave the machine in plaintext:
| Class | Meaning |
|---|---|
plaintext | Aggregate counters or metadata the server may see in cleartext |
e2e | Session and content-bearing payloads. These only ever leave the machine AES-256-GCM encrypted, and must never appear on a plaintext push list. |
aggregates, health, models, runtimes and agent_graph are plaintext.
Everything content-bearing — sessions, events, transcript, spans,
traces, external_calls, rollup_sessions, search — is e2e.
Calling it — HTTP#
Convenience routes:
curl -s 'localhost:8900/api/local/sessions?limit=20' | jq '.rows'
curl -s 'localhost:8900/api/local/events?event_type=error' | jq '.rows'
curl -s 'localhost:8900/api/local/transcript/<session_id>' | jq '.rows'
curl -s localhost:8900/api/local/aggregates | jq '.rows'
curl -s localhost:8900/api/local/health | jq
curl -s 'localhost:8900/api/local/spans?session_id=<sid>' | jq '.rows'
curl -s 'localhost:8900/api/local/traces?limit=20' | jq '.rows'
curl -s 'localhost:8900/api/local/external-calls' | jq '.rows'
curl -s 'localhost:8900/api/local/search?q=migration' | jq '.rows'
curl -s 'localhost:8900/api/local/agent-graph' | jqThe generic form, which reaches every shape:
curl -s localhost:8900/api/local/query -X POST \
-H 'content-type: application/json' \
-d '{"shape":"rollup_sessions","args":{"runtime":"claude_code","limit":50}}' | jqAn unknown shape returns 400 with the list of allowed shapes, which makes the
API self-describing:
curl -s localhost:8900/api/local/query -X POST \
-H 'content-type: application/json' -d '{"shape":"nope"}' | jq '.allowed_shapes'Calling it — the daemon directly#
The dashboard is a convenience. The daemon serves the same contract:
PORT=$(jq -r .port ~/.clawmetry/local_query.json)
TOK=$(jq -r .token ~/.clawmetry/local_query.json)
curl -s "http://127.0.0.1:$PORT/api/local/query" \
-H "Authorization: Bearer $TOK" \
-H 'content-type: application/json' \
-d '{"shape":"sessions","args":{"limit":5}}' | jqThis works with no dashboard running at all, which makes it the right path for a monitoring script.
Calling it — MCP#
Five of these shapes are exposed as MCP tools so an agent can query them directly. → MCP tool reference
Planned shapes#
The registry also declares shapes that are targets rather than served surfaces:
usage, session, brain, approvals and glance. A planned shape is a
declared intent; shipping one flips its registry entry, and a drift test enforces
both directions so the documentation cannot claim a method the code does not
serve.
Practical recipes#
# yesterday's errors, grouped by tool
curl -s 'localhost:8900/api/local/events?event_type=tool_result&limit=5000' \
| jq '[.rows[] | select(.data.is_error) | .tool_name] | group_by(.) | map({tool: .[0], n: length}) | sort_by(-.n)'
# the ten most expensive sessions this month
curl -s localhost:8900/api/local/query -X POST -H 'content-type: application/json' \
-d '{"shape":"rollup_sessions","args":{"limit":2000}}' \
| jq '[.rows[] | select(.cost_usd != null)] | sort_by(-.cost_usd) | .[:10] | map({title, cost_usd, runtime})'
# per-runtime cost for the last 30 days
curl -s localhost:8900/api/local/runtimes | jq \
'[.rows[]] | group_by(.runtime) | map({runtime: .[0].runtime, cost: (map(.cost_usd) | add)})'