Usage events
The append-only record of metered work, kept for operational accounting and product insight.
zero-memory records every unit of metered work — an LLM extraction, an
embedding batch, an MCP tool call — as a row in an append-only table,
public.usage_events. The table is the deployment's own operational
accounting ledger: it stays inside your database, and it never contains
memory content.
Why it exists
Usage history is unrecoverable: if events are not written from day one, the data simply never existed. A single append-only table, captured from the start, serves two needs at once — operational accounting (how much work ran, for whom, in what units) and product insight (which tools and paths are actually used) — without shipping anything to an external analytics service.
How it works
The table
Each event is one row:
| Column | Meaning |
|---|---|
occurred_at | When the work happened |
user_id | Whom the work was for |
agent_name | The agent principal, when the caller is an agent |
event_type | What kind of work (see below) |
quantity | How much of it (defaults to 1) |
unit | count or tokens |
metadata | Small structured context — normally counters and identifiers; recall queries are the bounded exception described below |
request_id | Correlation id shared with logs and the audit log |
The table is append-only and indexed by time and by (user_id, occurred_at).
Row-level security is enabled with no policies — a deliberate deny-all:
only the server's service role can access the table directly. End users never
query raw rows; owner-scoped dashboard/activity RPCs expose only the caller's
derived view.
Event types
| Event type | Emitted when | Unit |
|---|---|---|
llm_extraction | A metered LLM call completes — ingest extraction, translation, or background judging (hygiene, usefulness, benchmark); metadata carries the input/output token split, the model id, and a purpose tag | tokens |
embedding | An embedding batch runs; quantity is the number of texts | count |
mcp_tool_call | Any MCP tool is dispatched; metadata carries the tool name and bounded result counters. Read tools add the scope-usage audit trail: read_mode (explicit, all, hint, session, or open), the session's attached project at call time, and a per-scope count of what was returned | count |
ingest_chunk | The ingest path accepts a chunk | count |
session_briefing | A briefing build_context call is delivered | tokens |
recall_used | A recall outcome is attributed: a later remember supersedes or derives from a recalled memory, the usefulness judge credits it, or a challenge records the negative counterpart (valence: misled) | count |
Emission is centralized on purpose: MCP tool calls are counted once at the tool dispatcher, not inside every tool, so adding a tool never requires adding instrumentation.
Recording is fire-and-forget
Writes go through a small domain port (UsageRecorder) whose calls are never
awaited on the critical path. If the ledger write fails, the failure is
logged and the primary operation — the recall, the remember, the ingest —
completes normally. Accounting must never break the product.
Attribution
user_id comes from the authenticated request context. Background work (for
example, watcher passes) runs outside any request but always knows whose
corpus it touched, so it passes the subject explicitly — otherwise the ledger
and any check against it would describe different things. agent_name is set
when the actor is an agent principal, and
request_id ties the event to the same request in the logs — see
Observability.
How to use it
Query the table directly with operator (service-role) access:
- Volume by type — group by
event_typeandunitover a time window to see extraction token spend, embedding volume, and tool-call counts. - Per-user accounting — the
(user_id, occurred_at)index makes per-person rollups cheap. - Tracing one request — filter by
request_idto see every unit of work a single request produced, alongside its log lines and audit entries. - Scope-usage audit — group read-tool rows by
read_mode, or unnest the per-scope return counts, to answer "are sessions reading pinned to their project, or degrading to everything visible?" The fields are recorded at the source, so the audit is aGROUP BYinstead of a fragile reconstruction.
One thing deliberately not in this ledger: refused writes. A refused
remember emits no usage event — it is counted by the
mcp_write_refused_total Prometheus counter, labelled by refusal reason
(see Observability).
Higher-level questions — what the memory actually contributed, not just what
it cost — are covered by Value metering,
which builds on this ledger's recall_used signal.
Design notes
- Own table over external analytics. A hosted analytics service would mean a premature dependency and data leaving the deployment. One append-only table covers both accounting and funnel analysis, and keeps a self-hosted installation fully self-contained.
- Memory content never enters the ledger. Metadata is normally restricted
to counters and identifiers — token counts, model ids, tool names, and
returned memory ids. The deliberate exception is the user's own
recallquery, truncated to 200 characters so the activity feed can explain an empty search. It is stored exactly as it was sent, which is also exactly what was searched — nothing rewrites a query, so one string covers both. Treat usage retention as operational metadata retention, not as a content-free guarantee. - Deny-all RLS as a pattern. Enabling row-level security while creating no policies is the standard pattern here for operational tables: nothing short of the service role can touch them, and the posture is explicit in the schema rather than implied by omission.