Skip to content

bug: an under-read day inside the settle window is frozen forever — Overview headline stays below the sum of its own Daily Activity rows #1217

Description

@therickfactr

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

  1. 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.
  2. 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:

  1. 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).
  2. 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 insteadMIN_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.

Activity

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions