Summary: On 0.9.23 the dashboard's Overview headline for 7 Days sat ~27% below the sum of the Daily Activity rows printed directly beneath it (27,346 calls in the headline vs 32,608 summed from its own visible day rows). The two panels read different sources by design — Overview takes every historical day from the durable daily cache, Daily Activity / By Model parse the live session logs — and five cached days had been frozen at a third to two-thirds of what their session logs actually contain.
The logs were never missing. Re-parsing those dates today returns the full numbers, and the previous cache generation still on disk (daily-cache.v17.json) holds them correctly. Once a day is written into the durable cache and the watermark passes it, nothing ever looks at it again — so a single bad derivation is permanent.
Environment
- codeburn 0.9.23 (Homebrew CLI), macOS 26 / darwin 25.6.0
- providers: claude, codex, grok, pi, cline-cli, warp
~/.claude/projects 1.3G, ~/.codex/sessions 2.5G, daily cache v29 with 222 days
What the cache held vs. what the logs contain
Calls only (the cost columns diverge in the same proportion):
| day |
cached (Overview) |
live logs (Daily Activity, and a re-parse today) |
| 2026-08-24 |
149 |
1,123 |
| 2026-08-25 |
257 |
1,650 |
| 2026-08-26 |
892 |
2,429 |
| 2026-08-27 |
2,174 |
4,458 |
| 2026-08-28 |
1,893 |
3,191 |
daily-cache.v17.json, written before the upgrade, has the correct figures for all five (e.g. 2026-08-26 claude 2,363 calls, against 826 in v29). Single-day and 6-month parses today both return the live column, so this is not a parser gap — the durable cache is simply holding a stale under-read.
Root cause: two individually reasonable rules that combine badly
mergeDayEntries lets a smaller fresh slice replace a larger baseline slice whenever the day is inside the settle window — isPartialSurvival() (src/daily-cache.ts:1033) only rescues the baseline for date < today - SETTLE_DAYS (7). This is deliberate and covered: "recent days stay authoritative: a shrink inside the settle window is honored", tests/daily-cache-carry-forward.test.ts:852.
ensureCacheHydrated never revisits a day at or before lastComputedDate — the gap parse starts at the watermark + 1 (src/daily-cache.ts:1393), so a day is derived once and then final.
The window in which a fresh derivation is trusted unconditionally is exactly the window that is never re-derived. If any one complete-marked derivation under-reads a day while that day is still inside the settle window, the undercount is written, the watermark moves past it a day or two later, and no subsequent run ever re-examines it.
The upgrade path is what exposed it here. With no daily-cache.v29.json present, adoptOlderDailyCaches() unions the older files (v17 among them), stamps complete: false, and the same run does a full re-derive. That re-derive under-read Claude on the five days above — all inside the settle window at the time — so the truncated slices won and the correct v17 slices were discarded. The other providers on those days produced no fresh slice at all, so they were carried, which is why the days ended up flagged carried: true.
I want to be straight about the limit of the diagnosis: I cannot reproduce the under-read itself. A parse of those dates today, narrow or wide, returns the right numbers. Whatever made that one derivation partial (it ran during an upgrade, on a large corpus, and sessionComplete() evidently returned true) was transient. This report is about the second half — that a transient under-read becomes permanent state with no path back.
Second-order symptom: the footnote asserts the opposite of the truth
Because those days took carried slices, the Overview footnote read includes $… preserved from expired session logs, and the amount was exactly the sum of the five frozen days. So the UI advertised the corrupted days as preserved history whose sources were gone, while every one of their transcripts was intact on disk. After the repair below, that footnote drops to $0.00.
Deterministic repro (no real corpus needed)
Against a cache marked complete through yesterday, with a day 3 days old holding a full slice:
- Run
ensureCacheHydrated with a complete parse that returns a truncated slice for that day → the day is overwritten with the truncated numbers. This step is intended behavior (rule 1).
- Run
ensureCacheHydrated again with a parse that returns the correct, full slice → the day stays truncated, because the watermark is current and no parse covers it. Every later run does the same.
Step 2 is the bug.
Workaround for anyone hitting this
Set "complete": false in ~/.cache/codeburn/daily-cache.v29.json and run codeburn once. That takes the re-derive path, where today's correct parse wins per (date, provider) and days whose sources really are gone stay carried. Verified here: all five days healed to match a fresh parse exactly, and the carried footnote went to $0.00. Do not delete the file instead — MIN_SUPPORTED_VERSION = 28 means an older generation cannot be loaded back, so a delete throws away every carried day whose sources have since expired.
Proposed fix (flagging before writing much, per CONTRIBUTING)
Re-derive the unsettled tail rather than freezing it: start the gap parse at min(lastComputedDate + 1, today - SETTLE_DAYS). That leaves every merge rule untouched — a fresh derivation still wins inside the settle window, days past it stay frozen and still get isPartialSurvival protection, and a partial parse still cannot shrink anything — but it makes the window self-healing instead of write-once, so the same transient failure costs one launch instead of forever. The cache write needs a no-op guard alongside it so the extra pass doesn't republish the file on every run (cf. #1032).
Happy to send that as a PR if the approach looks right, or to take it another way if you'd rather the settle window not be re-parsed on every launch — the alternative I considered is recording per-day whether the derivation that sealed it was source-complete, and re-deriving only suspect days, which is a schema bump.
Summary: On 0.9.23 the dashboard's Overview headline for
7 Dayssat ~27% below the sum of the Daily Activity rows printed directly beneath it (27,346 calls in the headline vs 32,608 summed from its own visible day rows). The two panels read different sources by design — Overview takes every historical day from the durable daily cache, Daily Activity / By Model parse the live session logs — and five cached days had been frozen at a third to two-thirds of what their session logs actually contain.The logs were never missing. Re-parsing those dates today returns the full numbers, and the previous cache generation still on disk (
daily-cache.v17.json) holds them correctly. Once a day is written into the durable cache and the watermark passes it, nothing ever looks at it again — so a single bad derivation is permanent.Environment
~/.claude/projects1.3G,~/.codex/sessions2.5G, daily cache v29 with 222 daysWhat the cache held vs. what the logs contain
Calls only (the cost columns diverge in the same proportion):
daily-cache.v17.json, written before the upgrade, has the correct figures for all five (e.g. 2026-08-26 claude 2,363 calls, against 826 in v29). Single-day and 6-month parses today both return the live column, so this is not a parser gap — the durable cache is simply holding a stale under-read.Root cause: two individually reasonable rules that combine badly
mergeDayEntrieslets a smaller fresh slice replace a larger baseline slice whenever the day is inside the settle window —isPartialSurvival()(src/daily-cache.ts:1033) only rescues the baseline fordate < today - SETTLE_DAYS(7). This is deliberate and covered: "recent days stay authoritative: a shrink inside the settle window is honored",tests/daily-cache-carry-forward.test.ts:852.ensureCacheHydratednever revisits a day at or beforelastComputedDate— the gap parse starts at the watermark + 1 (src/daily-cache.ts:1393), so a day is derived once and then final.The window in which a fresh derivation is trusted unconditionally is exactly the window that is never re-derived. If any one complete-marked derivation under-reads a day while that day is still inside the settle window, the undercount is written, the watermark moves past it a day or two later, and no subsequent run ever re-examines it.
The upgrade path is what exposed it here. With no
daily-cache.v29.jsonpresent,adoptOlderDailyCaches()unions the older files (v17 among them), stampscomplete: false, and the same run does a full re-derive. That re-derive under-read Claude on the five days above — all inside the settle window at the time — so the truncated slices won and the correct v17 slices were discarded. The other providers on those days produced no fresh slice at all, so they were carried, which is why the days ended up flaggedcarried: true.I want to be straight about the limit of the diagnosis: I cannot reproduce the under-read itself. A parse of those dates today, narrow or wide, returns the right numbers. Whatever made that one derivation partial (it ran during an upgrade, on a large corpus, and
sessionComplete()evidently returned true) was transient. This report is about the second half — that a transient under-read becomes permanent state with no path back.Second-order symptom: the footnote asserts the opposite of the truth
Because those days took carried slices, the Overview footnote read
includes $… preserved from expired session logs, and the amount was exactly the sum of the five frozen days. So the UI advertised the corrupted days as preserved history whose sources were gone, while every one of their transcripts was intact on disk. After the repair below, that footnote drops to $0.00.Deterministic repro (no real corpus needed)
Against a cache marked complete through yesterday, with a day 3 days old holding a full slice:
ensureCacheHydratedwith a complete parse that returns a truncated slice for that day → the day is overwritten with the truncated numbers. This step is intended behavior (rule 1).ensureCacheHydratedagain with a parse that returns the correct, full slice → the day stays truncated, because the watermark is current and no parse covers it. Every later run does the same.Step 2 is the bug.
Workaround for anyone hitting this
Set
"complete": falsein~/.cache/codeburn/daily-cache.v29.jsonand run codeburn once. That takes the re-derive path, where today's correct parse wins per (date, provider) and days whose sources really are gone stay carried. Verified here: all five days healed to match a fresh parse exactly, and the carried footnote went to $0.00. Do not delete the file instead —MIN_SUPPORTED_VERSION = 28means an older generation cannot be loaded back, so a delete throws away every carried day whose sources have since expired.Proposed fix (flagging before writing much, per CONTRIBUTING)
Re-derive the unsettled tail rather than freezing it: start the gap parse at
min(lastComputedDate + 1, today - SETTLE_DAYS). That leaves every merge rule untouched — a fresh derivation still wins inside the settle window, days past it stay frozen and still getisPartialSurvivalprotection, and a partial parse still cannot shrink anything — but it makes the window self-healing instead of write-once, so the same transient failure costs one launch instead of forever. The cache write needs a no-op guard alongside it so the extra pass doesn't republish the file on every run (cf. #1032).Happy to send that as a PR if the approach looks right, or to take it another way if you'd rather the settle window not be re-parsed on every launch — the alternative I considered is recording per-day whether the derivation that sealed it was source-complete, and re-deriving only suspect days, which is a schema bump.