Skip to content

perf(telemetry): the ledger is kept by day, so an ingest costs what it adds - #1031

Merged
blafourcade merged 17 commits into
nextfrom
fix/ledger-by-day
Oct 15, 2026
Merged

blafourcade merged 17 commits into
nextfrom
fix/ledger-by-day

Conversation

@blafourcade

Copy link
Copy Markdown
Contributor

🎯 What & why

One ingested call used to cost the whole ledger: every partition read and refolded, and its month rewritten (#1001). The ledger is now kept by UTC day, and an ingest reads, folds and rewrites only the days around its calls. One call into a 265k-call ledger: ~1.65 s → 58-64 ms, flat as history grows.

🛠️ How it works

  • Partitions ledger/<YYYY-MM-DD>.jsonl replace <YYYY-MM>.jsonl. The mechanism is unchanged: canonical order, a changed partition rewritten whole by rename, one call per line (usage-ledger-adapter.ts).
  • Ingest window: each call's day plus the days on each side (stored-usage.ts:43, ledger-upkeep.ts:15).
    • Measured read-only on a real history: 262,631 calls. A call's snapshots are 0.3 h apart at most, 3 straddle midnight, none spans a day.
  • Report settles (ledger-upkeep.ts:30, under the lock at report-usage-use-case.ts:61). It already reads everything. When it finds a call in two places, a leftover monthly file or a damaged line, it rewrites those partitions canonically.
    • After any report, the bytes are identical whatever the history of ingests.
    • Between reports, a call seen outside the window would sit twice, never observed in practice. Fold-at-read still counts it once.
  • Migration: monthly files are still read. The first ingest touching a month splits it into days and removes the month last (usage-ledger-adapter.ts:102), so a crash mid-split loses nothing.
  • Times: an at without a zone or outside four-digit years is now unusable, so a day never depends on the machine's TZ. Incoming calls like that count as undated; stored lines like that are dropped as damage. Claude always writes Z: 1.83M local timestamps checked, none without it.
  • Alternative set aside: append-only plus compaction. About 0.1 s faster, but a compaction rule, torn-line handling and a fold in every reader. Frame: contract.md.

🧪 How to verify

  • cd cli && pnpm test: 6495 passed.
  • pnpm test:mutation:telemetry: 96.14 (floor 96).
  • Determinism: ledger-upkeep.property.integration.test.ts. Any grouping or order of ingests, from monthly files or not, with damage, gives canonical bytes after a settle, and a second settle is a no-op. The reviewer also swept 5 seeds × 1000 runs and replayed 3,617 crash states of a split; all converged.
  • node scripts/check-claude-tokens-end-to-end.cjs (and --attended): 2 calls, 393 tokens, all on task demo, ticket T-1.
  • Review rounds: review.md.

⚠️ Heads-up

  • report still reads the whole ledger (~2.8 s on a 265k-call month). That is out of scope here.
  • First ingest after upgrade splits the current month once: 1.6-1.7 s at 265k calls.
  • IngestResult.skippedLedgerLines now counts damage inside the ingest's window. The report's figure covers everything.
  • If AIDD_TELEMETRY_DIR points at ledger/ itself, forget would list day files as files from the earlier version. This is a pre-existing oddity.

🔗 Linked issue

Closes #1001

✅ I certify

  • I DO CERTIFY I READ EACH LINE OF THE PULL REQUEST BECAUSE I AM A SOFTWARE ENGINEER, NOT A AI PUPPY.

🤖 Generated with Claude Code

blafourcade and others added 17 commits October 15, 2026 09:35
…indow

One ingested call now loads, folds and writes only its calls' days and
the day on each side, plus any monthly partition those days fall in,
which it splits into days. Damage and duplicates in the window are
rewritten with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
The report rewrites, folded, every partition that is damaged, monthly,
or holds a call another one holds, so the bytes on disk are those of
one ingest of the same calls whatever order the ingests ran in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Pins that forget removes daily and monthly partitions as the ledger,
never as an earlier day file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Kills the two mutants the telemetry run left in the ledger adapter: an
unanchored monthly name, and a duplicate inside one day going unseen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
dayOf now gives no day for a time without an explicit zone (Z or
+-hh:mm), or whose UTC day falls outside four-digit years. Such a call
is counted undated at ingest, and such a ledger line is counted and
dropped as not a record, instead of landing in a file DAILY never reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
The report settles the ledger, so a line that is not a record is gone
after it, as after an ingest. Both now say so in the same words.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
…nged nothing

A day's key never equals a month's, so filtering names to monthly files
before matching them against the touched months was redundant: the
equivalent mutant the review saw survive.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Once on disk and order-independent bytes hold after a report settles the
ledger; an ingest reads its window plus a month it splits; save removes a
named day it holds no records for rather than writing it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Adds torn lines in a monthly file and in a stray day, calls already held
in a monthly file read again, 200 runs, an assertion that the bytes are
the canonical partitionByDay(foldUsage(calls)) with each call once, and
that a second settle finds nothing to do and changes nothing. Red when
settle ignores damaged partitions or save keeps an emptied day.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
The four-digit check on the computed day was a ten-character regex on a
ten-character slice, so its anchors were equivalent mutants; it is now
a sign check. Reading a stored time goes through dayOf alone. Tests pin
the leading anchor and a zoned time naming no real day.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
7ff1dee read a stored time through dayOf(String(at)), so a time held
in a one-element array coerced to a zoned string and was accepted with a
non-text at. The text check is restored, superseding that commit's
"goes through dayOf alone"; the new refusal case also kills its mutant,
so the check is no longer equivalent. The zone pattern's closing anchor
is kept and says why no test can observe it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
@blafourcade
blafourcade marked this pull request as ready for review October 15, 2026 14:43
@blafourcade
blafourcade requested a review from a team as a code owner October 15, 2026 14:43
@blafourcade
blafourcade merged commit fcb6022 into next Oct 15, 2026
61 checks passed
@aidd-bot aidd-bot Bot mentioned this pull request Oct 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant