Token Accounting: Replay for Decisions, Projection for Display
Two meters, each its own job—compaction decisions never trust the UI number. Core source: packages/llm/token-meter/src/usage-projection.ts.
projectedTokens formula answers the size of the next request; and why occupancy-percent non-atomicity is a design decision—complete with a defense written into the docs.
Play first, then talk. Left gauge is replay measure()—compaction reads it. Right is UI projection projectedTokens—the status line shows it. A conversation advances: big tool output, compaction, model switch, new request. You'll see the gauges stay close most of the time, then openly diverge on the model switch—and look justified doing it.
ctx.tokenMeter.measure() at their own request boundary—where both values are available together—not read this projection..agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.zh.md line 35, original quoteUp front: the two numbers answer different questions. Compaction needs “how big would a request be right now”—accurate, may be expensive. The UI status line only needs occupancy for the user—cheap, durable, available immediately after reconnect; decimal precision doesn't matter. DSH simply builds separate roads and makes neither compromise.
Display numbers must be cheap—rough is fine.
The decision path is replay. Each ctx.tokenMeter.measure() call folds the durable log's current tail into an immutable snapshot: if the latest successful request's provider usage matches the current request envelope and isn't below its full heuristic anchor, use it as the anchor; surface (model-visible surface) changes after that are re-priced with signed deltas; with no reusable anchor, price the whole thing with a fixed heuristic.
The cost is plain: O(surface) per call—so only decision-makers like compaction call it at their own request boundary.
Source:docs/subsystems/token-meter.zh.md definitions of the two baseline kinds (usage anchor / estimated heuristic anchor).
The display path is projection. Ordinary durable session projection state with two last-writer-wins fields: pressureTokens—prompt-side size from the latest request report, input plus cache read/write, excluding output (usage-projection.ts lines 70–72); and contextWindow from the latest request/context log record. Numerator and denominator write separately—never one atomic observation.
pressureTokens alone is awkward: it only updates when a request reports usage, sits still during Turn streaming, and can't see compaction. Compaction replaces a big surface stretch but the status-line number doesn't move—users think compaction did nothing. So fold also carries a running surface total; what's published is the sample plus signed surface change after it. Source comments state the intent plainly: occupancy answers for the next request rather than the last one (usage-projection.ts lines 150–161 comments). Demo step 5 is that effect: compaction just hit disk, no new request yet, and the projection already dropped.
One timing detail: usage samples are stamped BEFORE the same event joins surface—so an assistant/message anchors the surface its own request saw, and the delta start doesn't skew.
Numerator and denominator aren't an atomic pairOn a model switch, the new contextWindow takes effect immediately while pressureTokens is still the previous route's sample. Occupancy is approximate until the next request reports usage. Docs call this a tradeoff, not a bug.
pressureTokens counts prompt side onlyScope is inputTokens plus cache read/write, excluding output. It describes how big the outbound request is—a different projection unit from billing total tokenUsage. Don't mix them.
Decision path doesn't read the projectionAgent Note, verbatim: nothing in the harness decides from occupancy percent—compaction reads measure() directly. However pretty the UI number is, it never enters a decision function's argument list.
The whole outward projection view is these 7 lines. The third spread is the lesson's punchline: pressureTokens + surfaceTokens - sampledSurfaceTokens—sample plus net surface change after sampling, floored with Math.max(0, …). If either source field is missing, that output simply doesn't appear.
view: ({ contextWindow, pressureTokens, surfaceTokens, sampledSurfaceTokens }) => ({
...contextWindow === undefined ? {} : { contextWindow },
...pressureTokens === undefined ? {} : { pressureTokens },
...pressureTokens === undefined || sampledSurfaceTokens === undefined
? {}
: { projectedTokens: Math.max(0, pressureTokens + surfaceTokens - sampledSurfaceTokens) },
}),
packages/llm/token-meter/src/usage-projection.ts, verified on 2026-08-13. Code blocks keep the original source text.Two edge cases fall out of this code. First, provider returns no usage: projection-side pressureTokens stays absent, spreads two and three never appear, UI shows no occupancy (Agent Note: show occupancy only when pressure and capacity are both known); decision side is unaffected—measure() falls back to an estimated heuristic anchor and still returns a number. Second, non-atomic occupancy divergence—the doc's defense already pops in demo step 6, from Agent Note lines 29–35, titled “Context occupancy is approximate, and that is the decision.”
A dedicated pure-function crate is the single source of truth: bytes/4 heuristic plus derived display math—/context, /session-info, auto-compact gates, preflight overflow checks, and every client renderer use it. Decision and display share one number by nature—never fight.
The cost: decisions also get only a coarse heuristic—4 bytes = 1 token (BYTES_PER_TOKEN = 4), one image counts 765. The crate's module comment calls itself the single source of truth for bytes/4 heuristics and derived display math. Thresholds use u32 integer percents with saturating multiply; details and source evidence are in the verified on-site lesson Token usage rate & threshold boundaries. Real provider usage doesn't enter occupancy on this path.
Source: grok-build-main crates/codegen/xai-token-estimation/src/lib.rs lines 3–19, verified 2026-08-13.
Sub-Agent progress stores token counts in two fields: input_tokens is the API's per-turn cumulative, keeping only the latest; output_tokens is per-turn delta, summed. Adding both raw double-counts input and inflates the display (manuscript ch. 6 citing restored-src ProgressTracker, study/chapters/06-task-system.md lines 169–181).
Compaction thresholds are buffer constants—13000 reserved for autocompact, 3000 for manual /compact (manuscript ch. 3 citing autoCompact.ts). Directionally, display and decision also have separate accounts—but in verified public materials we haven't seen an explicit formula that makes projection answer the next request like this.
Side by side, all three answer the same question: who uses the occupancy number, and who pays when it's wrong. Grok chose a forever-consistent coarse number; Claude Code patched display counting against double-count; DSH fully separates the two consumers and admits in docs that the display copy is approximate.
Hand-walk a divergence scene
Compaction just hit disk; surface shrank from 92k to 8k; no new request yet; the user immediately switches the model from a 100k window to 200k. Answer four numbers now: UI status-line numerator and denominator—which fields; if compaction calls measure() right now, roughly what total and which anchor kind. Finally use Agent Note line 35's original sentence to explain why the two sets may differ, and what a consumer that needs precision should do.