Assay stores a few kinds of row. Labels hang on them. They do not invent a new unit.

The units

UnitWhat it isTypical id
SpanOne metered eventminted on capture
TurnOne sealed transcript lineminted on capture
TaskOne unit of intent (often one human turn)task_id
Session / sittingOne harness conversationsession_idthe spine
EpisodeA range of turns inside one sitting, with one primary on the treean objective window
There is no per-turn objective label. Join a turn to an episode by range at read time. principal is who directed. It is a dimension. It is never the join. Joins use session_id, then task_id, then the exact span.

The span

Written once. No update. No delete. It carries:
  • Attribution — principal, session, task, optional parent span, optional tool name, optional project (seating: where the harness was pointed)
  • Model — provider, model, lane (frontier / offload / local)
  • Tokens by phase — input, cache read, cache write, output, reasoning, tool I/O
  • Cost — integer micro-USD when priced; absent when not
  • Producer ref — the idempotency key
Project is seating, not product. Where the session was opened is not what it built.

The turn

One conversation line: human, agent, reasoning, or tool. Almost every column is readable without the key: time, session, sequence, kind, tool, counts, the link to the span. Exactly one column is sealed: the message text. A shredded principal keeps the rows. The text is gone. Surfaces say [shredded], not an empty message.

Overlays

Overlays are extra rows. They point at a span or a sitting. They never edit the original.
OverlayClaim
ClassificationSpan work-type (mechanical, tooling, routing, reasoning, generation)
ValuationA value score on a span
Cost correctionA later, versioned cost — original stays
Outcome attributionThis outcome was produced by this spend
Product attributionPath mentions in the sitting’s own documents
Session annotationType — kind of work
Objective windowEpisode — this stretch of turns was for this tree node
Objective touchThis sitting also affected that node — no dollars
Scope windowWrites landed in repo:<slug>
Each overlay is idempotent on its own key. Running a pass twice does not double-count.

Outcomes

A task_outcome is a result: something happened, optionally with a declared dollar value. ROI joins outcomes to spend through the attribution overlay. Missing declared value means ROI is undefined, not −100%.

How a dollar finds a sentence

  1. Capture writes spans and turns for the same sitting.
  2. A turn may carry span_ref to the span that paid for it.
  3. assay turns and the session page print cost on that join.
  4. Labels on the sitting (type, episode, product) group those dollars.
If a sitting has spend and no transcript, you still have cost. You do not have words. If a sitting has words and no price (unknown model), you still have the conversation. Totals keep that hole visible.