Repository navigation
Token report: read the existing usage ledger into per-turn numbers and a reproducible prompt split #799
Description
The measurement already exists; the report does not
This issue is the measurable half of #796, split out because it changes no product behaviour and needs no credentials or CI, so it can be done now instead of waiting on the owner's decisions about which savings to take.
What is already on main (fd205c40):
core/src/host/usage.jsis a token ledger, one row per billed attempt:tokensOf()(:44-49) keepsinput,cacheRead,cacheWrite,outputapart,foldCalls()(:53) folds Session events into attempts, and the SQLite schema (:142-152,PRAGMA user_version = 2) iscalls(key, session, time, provider, model, kind, live, input, cache_read, cache_write, output)plusowners(session, owner, name)andkinds(session, kind). It lives at$DSH_HOME/usage.sqlite(install(),:228-230).core/dev/llm-tap.mjsis a proxy that logs one JSON line per call with the provider's ownusage/tokensand aprefixdiff (sameMessages,firstDiff) against the previous call of the same Bot, and it can dump each request body (<id>-<kind>-<who>.json).core/src/client/usage-page.js/usage-data.jsrender per hour × owner × model × kind sums (usage-data.js:3-6).
What is missing: nothing reads that ledger outside the UI. There is no per-turn distribution, no growth curve, no split of the prompt into the parts #796 asks about, and no report anyone can reproduce.
What this host's ledger already says
Read read-only from a copy of usage.sqlite in this workspace (2026-10-20T07:34Z → 2026-10-22T22:05Z, 16,399 calls):
| Bot | calls | prompt (input+cacheRead+cacheWrite) | fresh input | cacheRead | output |
|---|---|---|---|---|---|
| Dev | 3,183 | 439,134,269 | 8,787,133 | 430,347,136 | 1,688,487 |
| Reviewer | 2,856 | 387,835,783 | 9,565,575 | 378,270,208 | 1,649,123 |
| Chief | 1,964 | 236,697,658 | 13,328,314 | 223,369,344 | 1,302,937 |
| Dev2 | 1,313 | 193,004,783 | 4,278,255 | 188,726,528 | 725,002 |
| Maintainer | 1,415 | 183,857,209 | 2,932,025 | 180,925,184 | 797,996 |
Per-call prompt: p50 129,231, p90 210,614, p99 244,788, max 255,554. Across all calls, cache reads are 96% of the prompt.
That last number is why the report has to keep the columns apart instead of printing one "tokens" figure: a cache read and a fresh input token do not cost the same, and #796 cannot tell a real saving from a change that only looks cheaper while they are added together. It also points the investigation at history growth (every turn re-reads the whole conversation) rather than at the group notice, which is what people assume.
Scope
core/dev/token-report.mjs, a CLI over the ledger — same place ascore/dev/llm-tap.mjs:--db <path>(default$DSH_HOME/usage.sqlite),--from,--to,--tz,--bot,--top.- Prints per Bot: calls, fresh input, cacheRead, cacheWrite, output, and prompt as a separate column; per-call prompt p50/p90/p99; the top Sessions by prompt; per-day totals.
- Prints, per Session, the prompt delta from one call to the next. These deltas are exact — calls in a Session are appended, so the difference is what that turn added — and they are the growth curve
#796asks for.
- The composition (
fixed overhead / history / tool results / task): one ablated replay per category, throughcore/dev/llm-tap.mjswith a dumped body as the input, each variant a real provider call, and the category's cost is the provider-reported prompt difference. The report states the sample size. Anything not measured that way is labelledderivedwith its formula; a character share may be shown as a character share and must not be printed as a token count. docs/reports/2026-10-23-token-spend.md: the table, the method, the exact commands, the provenance of every column, and the sample sizes.docs/subsystems/ds-bot-core.md: its frontmatter ownscore/dev/(arch-docs checks this), so it gets a short paragraph naming the new script.
Out of scope
- Any change to what is sent to the model: no prompt, notice, roster, memory or tool-output edits.
#796carries those, and mixing them into this PR is how a measurement becomes uncomparable. - Any change under
core/src/, any workflow change, any new metrics framework.#791defines the RSI baseline metrics and#357is the Nightly smoothness dashboard; if one of them lands a schema while this is in flight, express these numbers in it rather than starting a third. #645(budgets, quiet hours) stays where it is.
Done when
node core/dev/token-report.mjs --db <fixture> --from … --to …prints the tables above and exits 0; on a missing--dbit exits non-zero naming the path instead of printing an empty table.- A test builds a fixture ledger in the schema above and asserts the report's rows equal hand-computed sums, including with a cache-read-heavy and a cache-read-free Bot, and with
ownersrows missing for some Sessions (an unowned Session must appear under its Session id, not disappear). - Cache reads never get folded into fresh input; a test fails if they are.
- The committed report's every number carries its provenance, and a test fails if a row has none.
npm run verifypasses;node scripts/ds-bot-patches.mjs checkpasses if anything underpatches/moves.
Kill conditions (each must turn a test red)
- Drop
cache_readfrom the group-by → the reconciliation test fails. - Sum
cache_readintoinput→ the separation test fails. - Remove the missing-
--dbguard → the missing-DB test fails. - Drop the
ownersjoin → unowned Sessions vanish → the fixture test fails.
Activity
Birfy commentedon Oct 23, 2026
agent-ready, with the acceptance written into the body so it can be checked rather than argued.Why this can start now: every input is already on this machine — the ledger (
$DSH_HOME/usage.sqlite, schema incore/src/host/usage.js:142-152) and the tap (core/dev/llm-tap.mjs) — so it needs no secret, no CI, no provider call for the totals, and the Actions billing lock does not block the work. Only the ablated replays for the category split cost calls, and they are small and counted.Two things I will hold a PR to:
#796.providermeans it came out ofusage;derivedmeans it has a formula and a sample size next to it. A character count printed as a token count is the one thing that makes this report worthless.docs/subsystems/ds-bot-core.mdownscore/dev/in its frontmatter, so arch-docs will ask for it — add the paragraph in the same PR.Birfy commentedon Oct 23, 2026
Written-by: Dev3
Claiming this one. Branch
fix/799-token-report, frommainatfd205c40; PR #802, tipd2766978.Scope exactly as triaged:
core/dev/token-report.mjsreads the existing ledger (no new metrics framework), the report isdocs/reports/2026-10-23-token-spend.md,ds-bot-core.mdnames the script because it ownscore/dev/, andcore/test/token-report.test.jsasserts hand-computed sums on a fixture ledger plus the missing---dbexit and the report's provenance column. Fresh input, cache read, cache write and their sum stay in separate columns, and the growth curve splits at a compaction instead of reading it as a negative delta.The numbers, from a read-only copy of this host's ledger (2026-10-20T07:34Z..10-23T02:06Z, 16,407 calls): per-call prompt p50 129,242 / p90 210,628 / p99 244,885; prompt 2,117,885,549 of which 96.4% is cache read; median prompt growth per turn +804 tokens (p90 +3,021, 683 compaction boundaries excluded). The composition split is reported as
not measured, not estimated: it needs dumped request bodies and real provider replays, and this host has neither.No optimisation in this PR —
#796decides which saving to take; this is the number it is compared against.feat(dev): read the usage ledger into a token report (ThinkFlowLab#802)