Value metering
Instrumentation that measures what memory contributes — briefing metering and recall-hit attribution.
Value metering is the instrumentation layer that records evidence of memory being useful: how large each briefing is, whether it came back empty, exactly which memories each read returned, and which of those the work went on to use. It extends the usage events record with result-aware emission points, and it is what powers the usefulness metrics on the insights dashboard — briefing hit rate, top surfaced facts, and estimated tokens saved.
What usage events cannot answer
Usage events answer "how often is each tool called". They cannot answer the questions users actually care about.
Briefing hit rate is one. A session-start briefing is delivered as an ordinary
build_context call, so on the server it is indistinguishable from a
mid-session call, and there is nothing to compute a hit rate from. Top
surfaced facts is another: the generic tool-call metering point fires before a
tool executes and sees only the tool name, so the result — the list of memory
ids that were returned — is structurally out of reach. Tokens saved is the
third. Estimating it requires knowing the briefing's size in tokens, which is
otherwise never recorded.
Usage events are also unrecoverable: a day without these events permanently loses that day's history. Value metering therefore runs from the start, accumulating correct events even before any dashboard consumes them.
The emission points
Briefing metering
The build_context contract carries an optional briefing?: boolean input
(default false). The session-start hook sets it to true; ordinary
mid-session calls omit it. This is a deliberate, explicit signal in the
contract itself rather than an inference from transport headers, so the
contract stays self-describing.
When briefing is true, the build_context handler emits a
session_briefing usage event after the result is known:
unitistokensandquantityis the estimated token size of the delivered briefing pack, from a fixed per-row estimate;metadatarecordstopic_len,returned(the number of memories in the pack), andempty(returned === 0), the raw material for briefing hit rate;briefing_kindrecords which briefing this was, the session-start unfold or the task-aware pack, so hit rate is measurable per kind rather than averaged across two different questions;conversation_idis present when the caller supplied one. A hook-delivered briefing runs in its own connection, so without it that briefing could not be joined to the rest of the same conversation's activity.
The mcp_tool_call event for build_context is still emitted, once. There
is no double counting of value: session_briefing measures usefulness,
mcp_tool_call measures call volume.
Clients that predate the flag simply never set it. No event is written and nothing breaks; degradation is soft.
Recall-hit attribution
recall, build_context and remember are result-attributed: instead of
being counted before they run, their single mcp_tool_call event is emitted
once the result is known, so it can carry that result. The pre-execution
counter deliberately skips them, which is what keeps the count at one row per
call.
For a read, the event carries the surfaced ids and the scope-usage trail:
{
"tool": "recall",
"returned_ids": ["mem_..."],
"returned": 3,
"read_mode": "session",
"returned_by_scope": { "proj.usr_....api": 3 }
}read_mode records how the read set was chosen — an explicit scopes
argument, a project hint, the session's attachment, a deliberate
all-visible-scopes read, or an open (unattached) degrade — and
returned_by_scope counts what each scope contributed. session_scope is
captured before any attach the call itself performs, so the row shows what
the session knew when it asked. Together they answer the scope-attachment
question at the heart of project isolation: did this read run pinned, or did
it silently widen? A hint-pinned briefing also attaches the session as a
side effect, and that transition is visible in these rows.
returned_ids contains only memory identifiers, never content. Empty results
are recorded too (returned: 0, returned_ids: []), because a miss is as
much signal as a hit. A read that fails still gets its row, flagged
error: true and carrying no counters, so failures are visible in the
activity feed without polluting hit-and-miss aggregates. Open-loop ids are
left out of returned_ids on purpose: loops are lifecycle reminders rather
than knowledge, and counting them would skew the metrics the ids feed.
For a write, the event carries the supersede candidates the response surfaced
(similar_ids). That is what lets the
session receipt report the rediscovery memory
prevented — facts the store already held that a session was about to write
from scratch — without inventing a new event type for it. An ordinary write
with nothing similar stays a bare tool row.
"Top surfaced facts" is then a plain aggregation: unnest returned_ids across
a period and count how often each id appears. This surfaced count is a
deliberate simplification. The true signal would be "this memory changed the
outcome of the work", which requires an execution-success feedback loop.
Surfaced count is an honest proxy and is labeled as such wherever it is shown.
Recall-outcome attribution
Surfacing is not use. A separate event type, recall_used, records one row
per memory whose outcome is known, through three channels.
The in-band channel fires when a later remember supersedes or derives from a
recalled memory. That is an explicit reference by the agent that had the fact
in hand, so the row is affirmative (useful: true). The challenge channel is
its deterministic negative counterpart: the caller reports a recalled memory
as wrong or stale, and the row is tagged valence: misled. Everything that
filters on usefulness ignores those rows, while the reinforcement and
stale-suspect rollups look for exactly them.
The judge channel is a background pass that reads the conversation and rules on the memories the session recalled, filling in the coverage the first two cannot reach. These rows carry the verdict and a confidence; see Recall quality.
Attribution
Two rules keep the ledger describing what actually happened.
Work is attributed to whoever it was for, not to whoever ran it. Background passes run outside any request, so there is no ambient user to read, but upkeep of a corpus is always done for its owner, and the pass names that owner explicitly on the event. Translation follows the same rule: a translated recall or briefing is metered against the owner it ran for. Without this, a large class of work would land with no subject at all and any per-person view would disagree with reality.
Calls made on a user's own provider key are marked and left out. A deployment
can be configured so that a user's own provider key runs their own calls.
Those events are still recorded, since the work happened, but they carry an
own_key marker, and the rollups that total the deployment's own token volume
skip them. The rows outlive the key, so without the marker work the deployment
never performed would start counting against the person who performed it
themselves.
Invariants
No memory content enters metering. Event metadata carries identifiers,
counters and flags, and memory content never enters the metering tables. The
one deliberate exception is the user's own recall query, kept to 200
characters so the activity feed can explain an empty search. It is metered
verbatim, and since nothing rewrites a query it is also what the search ran
on. See usage events.
Recording is fire-and-forget: it is never in the critical path, so if the
metering adapter fails, recall, build_context, and the briefing still
succeed. There are no direct reads either. The events table is deny-all under
row-level security, and users see aggregates only through the dashboard's
owner-scoped queries.
What clients need to set
Nothing needs to be enabled; metering is on by default. If you use the
standard session-start briefing hook, it already
passes briefing: true. If you build your own client integration, set
briefing: true on the one build_context call that delivers your
session-start pack, and leave it unset everywhere else; see
client adapters. A briefing
delivered for a specific task rather than at session start should also set
briefing_kind: 'task', so the two do not average into one hit rate; an
omitted kind is recorded as session.
The results surface on the insights dashboard: recall hits over a period, top surfaced facts, briefing hit rate, and estimated tokens saved.
One row, one flag, two event types
Hits go in an array in metadata rather than one row per hit. A dedicated per-hit event type would be cleaner to group by, but multiplies row volume by the result size of every recall. One row per call with an id array keeps volume flat, and unnesting an array in Postgres is cheap.
The briefing signal is an explicit flag, not header inference. Detecting a session start from transport provenance would work, but it hides semantics outside the contract. An explicit boolean keeps the contract the single source of truth for what a call means.
Surfacing and use are separate events. Folding "was it used" into the tool-call row would mean rewriting a row that has already been written, in a table whose whole value comes from being append-only. A second event type keeps every fact immutable and lets the three outcome channels arrive whenever they can: one during the call, one on a challenge, one long after the session ended.