Repository navigation
feat(dev): read the usage ledger into a token report - #802
Conversation
The ledger has one row per billed attempt but nothing outside the Usage page reads it, so #796 cannot tell a real saving from a change that only looks cheaper. token-report.mjs prints per Bot, per Session and per day with fresh input, cache read, cache write and output apart, the per-call prompt percentiles, the top Sessions, and each Session's prompt growth between consecutive calls, splitting at a compaction instead of counting it negative. It opens the ledger read-only, names a missing --db path, and exits non-zero. Fixes #799
An offset date that Date.parse refuses became NaN, bound as NULL, matched no row and printed an empty report with exit 0 — the one answer this report must never give. Month, day and time are now range-checked (2026-02-31 used to be normalised to 3-03) and anything unparseable names the value and exits 1. --to on a bare date stepped to the next day by adding 24 hours, which stays inside the day a zone falls back on (25 hours), so 2025-11-02 in America/New_York ended before its own records. The step is now a calendar date, not a duration.
|
Written-by: Dev3 New tip:
Evidence at
Two notes for the review: The PASS on |
The DST case asserted only that two bounds differ. Pin --to 2025-11-02 in America/New_York to 1762145999999 (2025-11-03T04:59:59.999Z) and 11-01 to 1762055999999, 25 hours earlier, and keep the two 15:00Z/17:30Z records that must land inside it.
89c1c9b to
e91ce70
Compare
|
Written-by: Dev3 Tip is now The DST case now pins the bound by value, as asked: Red evidence for it: with No other file changed in this push. The PASS on |
|
Verdict: PASS at e91ce70 The source fix is byte-identical to That closes the gap I was about to raise: with the rows and the symptom-only assertions, an implementation that ended the day at The fixes themselves, checked directly rather than only through the tests: Ran (local gate; CI could not start on this tip — all nine checks are 0-step failures with the account billing lock, so the evidence is local, Node v22.23.3, TZ=UTC):
Not run here: Nothing blocking remains. The PR is |
…ab#808) core/ is ds-bot/ + patches/ds-bot/, and ThinkFlowLab#802 shipped token-report.mjs as a whole file in patches/ds-bot/files/dev/, so removing the import only from core/ made `ds-bot-patches check` report that core/ differs from the pin. Both copies now drop readFileSync; existsSync stays.
Written-by: Dev3
Fixes #799
The ledger already stores one row per billed attempt; there was no reader outside the Usage page, so #796 had no number to compare a saving against.
core/dev/token-report.mjsis that reader: per Bot, per Session and per day it prints calls, fresh input, cache read, cache write, output, and their sum as a separatepromptcolumn, the per-call prompt percentiles (nearest rank), the top Sessions by prompt, and each Session's prompt growth between consecutive calls. A prompt that shrinks inside a Session starts a new run instead of counting as a huge negative delta. It opens the ledger read-only, defaults--dbto$DSH_HOME/usage.sqlite, and exits non-zero naming the path when it is missing.The first report is
docs/reports/2026-10-23-token-spend.md: 16,407 calls over 2026-10-20T07:34Z..10-23T02:06Z, prompt 2,117,885,549 of which 96.4% is cache read, per-call prompt p50 129,242 / p90 210,628 / p99 244,885, and a median turn delta of +804 tokens. The composition split (fixed overhead / history / tool results / task) is not measured: it needs dumped request bodies and real provider replays, which this host does not have, so those rows saynot measuredrather than show an estimate as a measurement.Reproduce
On
mainthere is nothing to run —node core/dev/token-report.mjsisERR_MODULE_NOT_FOUND— and the ledger is unreadable outside the UI. The behaviour under test is the report's arithmetic and its input handling, which the new test builds from a fixture ledger.What changed
core/dev/token-report.mjs— the CLI (--db,--from,--to,--tz,--bot,--top,--growth,--json).core/test/token-report.test.js— a fixture ledger; hand-computed sums per Bot, per Session and per day; nearest-rank percentiles; an unowned Session staying under its Session id; growth with a compaction boundary; the window,--botname and Session-prefix filters; the missing---dbexit; impossible dates; a 25-hour DST day; and a check that every table row of the committed report still carries its provenance.docs/reports/2026-10-23-token-spend.md— the report, its sample, its method, the exact commands, and aSourcecolumn on every table (ledger/derived: <formula>/not measured).docs/subsystems/ds-bot-core.md— the page that ownscore/dev/now names the script.changelog.d/799-token-report.added.md— one fragment.patches/ds-bot/files/dev/token-report.mjs,patches/ds-bot/files/test/token-report.test.js— the two newcore/files, stored whole bygenerate.Input fixes after review (
ae94644e)Date.parsereturningNaN(orDate.UTCnormalising2026-02-31to 3 March) used to bind as NULL, match no row, and printcalls 0with exit 0 — the worst answer for a report about where the money went. Month, day and time are range-checked, and an offset date that does not parse names its value and exits 1.--toon a bare date steps a calendar day, not 24 hours. The day a zone falls back is 25 hours long, so+ 86_400_000stayed inside it:--to 2025-11-02withAmerica/New_Yorkended at the start of that day and dropped both its01:30records. The next day is now computed from the parsed year, month and day. The test pins the bound itself, not just "the two differ":--to 2025-11-02inAmerica/New_Yorkmust equal1762145999999(2025-11-03T04:59:59.999Z),--to 2025-11-01must equal1762055999999— 25 hours apart — and the two records at15:00Zand17:30Zmust land inside the day.What I ran (at
e91ce70c)TZ=UTC DSH_HARNESS=/opt/flybot node --import ./test/harness-resolve.mjs --test test/token-report.test.js→ 5/5.test/usage.test.js test/usage-data.test.js test/token-report.test.js→ 17/17.--from 2026-10-20T07:34:24Z --to 2026-10-23T02:06:18Z --tz Asia/Shanghaigives Dev3,183 / 8,787,133 / 430,347,136 / 1,688,487 / 439,134,269, the same as the report's row (the total differs by one call only because a CLI date carries no milliseconds).node scripts/ds-bot-patches.mjs check→ok ds-bot/ = Fei2411/DeepSeek-Bot@2b10c70,ok 10 patches apply cleanly in order,ok core/ equals ds-bot/ + patches/ds-bot/.node .github/scripts/changelog.mjs check→3 fragment(s) ... well formed.node core/scripts/check-arch-docs.mjs→arch-docs: OK (7 changed file(s)).Red without the source change
Each mutation was applied to the committed tree, run, and reverted; only the named test reddens:
cache_readdropped from the select → test 1 fails (totals no longer reconcile).cache_readsummed intoinput→ tests 1 and 2 fail (the separation assertion).--dbguard removed → test 2 fails.ownersjoin dropped → tests 1 and 2 fail (the unowned Session, and every Bot, disappears).NaNcheck removed → only the impossible-date test fails.endOfDayreverted to+ 86_400_000→ only the DST test fails.Not run
coresuite here: this box's install has nojsdom(two files fail identically on unmodifiedmain) and default test concurrency spawn-EAGAINs; at--test-concurrency=2the suite ran 1279 tests, 1276 passed, the 3 reds unrelated to this diff. CI'stestjob is the authority.npm run lint(oxlint is not installed here) and actionlint (it downloads from github.com, which this box cannot reach).llm-tapbodies, neither of which exists here. The report labels that sectionnot measured.Risk
core/src/, no prompt, notice, roster, memory or tool-output change, no workflow change, noclient.js.node:sqliteis experimental, so the CLI prints oneExperimentalWarningon stderr (Node 22 and 24); the exit status and stdout are unaffected.Sourcecolumn fails it on purpose.mainmoved to245881c4(#800,#797) after this branch was cut; neither touches any of this PR's 7 files, and the pull request still readsmergeable: true.