Skip to content

Token report: read the existing usage ledger into per-turn numbers and a reproducible prompt split #799

Closed

Description

@Birfy

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.js is a token ledger, one row per billed attempt: tokensOf() (:44-49) keeps input, cacheRead, cacheWrite, output apart, foldCalls() (:53) folds Session events into attempts, and the SQLite schema (:142-152, PRAGMA user_version = 2) is calls(key, session, time, provider, model, kind, live, input, cache_read, cache_write, output) plus owners(session, owner, name) and kinds(session, kind). It lives at $DSH_HOME/usage.sqlite (install(), :228-230).
  • core/dev/llm-tap.mjs is a proxy that logs one JSON line per call with the provider's own usage/tokens and a prefix diff (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.js render 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

  1. core/dev/token-report.mjs, a CLI over the ledger — same place as core/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 #796 asks for.
  2. The composition (fixed overhead / history / tool results / task): one ablated replay per category, through core/dev/llm-tap.mjs with 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 labelled derived with its formula; a character share may be shown as a character share and must not be printed as a token count.
  3. docs/reports/2026-10-23-token-spend.md: the table, the method, the exact commands, the provenance of every column, and the sample sizes.
  4. docs/subsystems/ds-bot-core.md: its frontmatter owns core/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. #796 carries 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. #791 defines the RSI baseline metrics and #357 is 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 --db it 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 owners rows 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 verify passes; node scripts/ds-bot-patches.mjs check passes if anything under patches/ moves.

Kill conditions (each must turn a test red)

  • Drop cache_read from the group-by → the reconciliation test fails.
  • Sum cache_read into input → the separation test fails.
  • Remove the missing---db guard → the missing-DB test fails.
  • Drop the owners join → unowned Sessions vanish → the fixture test fails.

Activity

  1. added
    enhancementNew feature or request
    agent-readyMaintainer-approved spec: an agent may take it. Only maintainers set this; never in a template
    area:automationScheduled tasks and automation
    on Oct 23, 2026
  2. Birfy commented on Oct 23, 2026

    @Birfy
    CollaboratorAuthor

    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 in core/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:

    1. No behaviour change in the same diff. If the PR also edits prompts, the notice, the roster or tool-output handling, it cannot be compared to the baseline it just measured. That work belongs to #796.
    2. Provenance on every number. provider means it came out of usage; derived means 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.md owns core/dev/ in its frontmatter, so arch-docs will ask for it — add the paragraph in the same PR.

  3. Birfy commented on Oct 23, 2026

    @Birfy
    CollaboratorAuthor

    Written-by: Dev3

    Claiming this one. Branch fix/799-token-report, from main at fd205c40; PR #802, tip d2766978.

    Scope exactly as triaged: core/dev/token-report.mjs reads the existing ledger (no new metrics framework), the report is docs/reports/2026-10-23-token-spend.md, ds-bot-core.md names the script because it owns core/dev/, and core/test/token-report.test.js asserts hand-computed sums on a fixture ledger plus the missing---db exit 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 — #796 decides which saving to take; this is the number it is compared against.

  4. added
    agent-workingAn agent has claimed this issue (the claim comment names the branch)
    and removed
    agent-readyMaintainer-approved spec: an agent may take it. Only maintainers set this; never in a template
    on Oct 23, 2026
  5. added a commit that references this issue on Oct 23, 2026
    21ee37d
  6. removed
    agent-workingAn agent has claimed this issue (the claim comment names the branch)
    on Oct 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:automationScheduled tasks and automationenhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions