Data & storage
Event schema#
Twenty-seven runtimes, one event shape. Everything downstream — the dashboard, the rollups, the detectors, the MCP tools, the cloud snapshot — reads this and nothing else.
An event#
{
"id": "…",
"session_id": "claude_code:1f2e3d…",
"agent_id": "…",
"ts": 1788745200.123,
"event_type": "tool_call",
"role": "assistant",
"tool_name": "Bash",
"model": "claude-sonnet-5",
"cost_usd": 0.0041,
"data": { "…": "runtime-specific detail" }
}| Field | Notes |
|---|---|
id | Deterministic. Re-ingesting the same source is a primary-key no-op, never a duplicate. |
session_id | Namespaced by runtime — see below |
agent_id | Which agent, where the runtime distinguishes them |
ts | Epoch seconds, float |
event_type | See the table below |
role | user, assistant, system, tool |
tool_name | Present on tool events |
model | Present on model calls. This is what makes cost derivable. |
cost_usd | Derived at ingest, or the vendor's figure. null when genuinely unknown. |
data | The runtime's own detail, preserved rather than flattened away |
Event types#
| Type | Meaning |
|---|---|
session.started | A session began |
message | A user or assistant turn |
prompt.submitted | A user prompt was submitted |
model.completed | A model call finished, with usage |
tool_call | A tool was invoked |
tool_result | A tool returned, success or failure |
error | The runtime recorded a failure |
thinking | A reasoning block, where persisted |
compaction | Context was compacted |
session.ended | A session ended |
Runtimes emit richer vocabularies of their own; those are preserved in data
and mapped onto these for anything cross-runtime. A detector that asks "was this
a tool call" gets a consistent answer regardless of whether the runtime called
it function_call, toolRequest, ToolCall or tool.execution_start.
data is not flattened#
The runtime's own payload is kept. That is what makes the Runtime extras tab possible — Grok's upload manifest, Cline's rejected tool calls, Kimi's context-window occupancy — none of which fits a unified schema and all of which is worth keeping.
The daemon adds data.extra.runtime on ingest, so an event always knows which
runtime produced it even after it has been through a rollup.
Session identity#
Session ids are namespaced by runtime:
claude_code:1f2e3d4c-…
codex:019e28c2-…
cursor:composer-…
pi-<uuid>
n8n-<execution_id>Three consequences worth knowing:
- Filtering by runtime genuinely scopes a query rather than filtering rows
client-side, which is what keeps the runtime filter cheap on a large store.
- Ids do not collide across runtimes even when the underlying values would.
- Synthesised child sessions carry a composite id —
<parent>::<tool_call_id> for a PicoClaw delegation, or
<runtime>:vmusage:<session> for a hosted-VM usage row — so a synthetic
session is identifiable as one.
Spans#
OpenTelemetry-shaped spans are stored alongside events, for tracing and the agent graph. The mapping from a Claude Code transcript is the clearest example:
| Source | Span |
|---|---|
| Assistant turn | llm.call |
tool_use block | tool.<name>, child |
tool_use named Task | agent.spawn, child |
thinking block | thinking, kind INTERNAL |
Spans also arrive directly over OTLP.
Timestamps#
Every runtime is normalised to epoch seconds as a float. The sources vary enormously — ISO 8601 with and without a zone, epoch seconds, epoch milliseconds, epoch microseconds, local time with no zone at all, Go-trimmed RFC 3339 fractions — and each adapter converts.
Where a record genuinely carries no timestamp, the adapter falls back to file mtime and offsets successive messages slightly so stream order is preserved.
A session with no timestamp
A NULL timestamp renders as "just now", forever. Adapters are written to avoid
producing one; if you see a session that is perpetually current, that is the
shape of the bug.
Cost fields#
| Field | Meaning |
|---|---|
cost_usd | The figure, or null |
cost_status | exact, estimated, partial, tokens_only, unavailable |
spend_basis | What produced the figure |
cost_usd: null with cost_status: "unavailable" means the data is not there.
It does not mean zero. → How cost is computed
Adding your own events#
The ingest API accepts this shape directly, so anything you push participates in the same rollups, detectors and views as a first-class runtime.