Skip to content

daily-cache: never finalize history against a degraded session parse - #856

Draft
avs-io wants to merge 1 commit into
getagentseal:mainfrom
avs-io:fix/daily-cache-degraded-completeness
Draft

daily-cache: never finalize history against a degraded session parse#856
avs-io wants to merge 1 commit into
getagentseal:mainfrom
avs-io:fix/daily-cache-degraded-completeness

Conversation

@avs-io

@avs-io avs-io commented Jul 29, 2026

Copy link
Copy Markdown
Member

What

Stop the daily cache from being finalized against a session parse that did not cover the days it is finalizing.

  • parser.ts: a read-only parse now reports a complete hydration only when nothing changed under the snapshot it served.
  • daily-cache.ts: only a complete parse may advance lastComputedDate, on both the gap-fill and full re-derive paths.
  • daily-cache.ts: a cache whose lastComputedDate is newer than its newest populated day has the watermark pulled back to that day, so the ordinary gap parse re-derives the tail instead of trusting the marker. A cache with no days at all is exempt, so a machine with no history can still finalize.

Why

ensureCacheHydrated already takes a sessionComplete callback, and usage-aggregator.ts already wires isSessionHydrationComplete into it. The gate is in place; the signal feeding it was inaccurate.

runParse set sessionHydrationComplete = true at the end of every run, including the read-only runs taken when the refresh lock times out, is unavailable, or the publication fence is lost. In read-only mode a changed file is served at its stale fingerprint and a file with no cache entry is skipped entirely, so such a run can legitimately return nothing for recent days while reporting a complete hydration. The backfill then writes complete: true and advances lastComputedDate to yesterday. Since gapStart is lastComputedDate + 1, the days the parse never covered fall behind the watermark and are never revisited.

Observed in the field as a daily-cache.v15.json marked complete with lastComputedDate: 2026-07-28 whose newest entry was 2026-07-25; three days of history were missing from the Trend view until the cache was rebuilt by hand.

The watermark pull-back also heals caches already on disk in that state, so no cache-version bump is needed and existing consistent caches take an identical path.

Why the signal is "served stale", not "was read-only"

Marking every read-only run incomplete would be one line instead of six, but complete !== true triggers a full 365-day re-derive on the next run. Anyone running the menubar alongside the CLI contends for the refresh lock routinely, and a filesystem where the lock is permanently unavailable would re-derive forever. A read-only run over an unchanged snapshot is equivalent to a full parse and stays trusted; only one that actually staled or skipped a file is treated as partial.

Preservation

Every path here only adds or re-derives days; none can drop one. Partial parses merge with the baseline winning, the re-derived range is strictly after the newest cached day, and retention still anchors on yesterdayStr (the real calendar edge), so holding the watermark cannot evict anything. A carried day whose session files are gone is asserted present, with its original accounting, across the degraded, healed, and pull-back paths.

Testing

New tests/daily-cache-degraded-completeness.test.ts (6 tests): a degraded parse publishes neither complete: true nor an advanced watermark on either path; a later healthy run rebuilds the missed days; an internally inconsistent complete cache is re-derived; a carried day survives all of the above; an empty cache still finalizes without re-parsing.

tests/parser-cache-refresh-timeout.test.ts gains cases on the existing timed-out-lock harness: a stale served snapshot and a snapshot missing a session file both report an incomplete hydration; an unchanged snapshot still reports complete.

All confirmed failing before the change. Full suite 2473 passed, tsc --noEmit clean. The existing 39 carry-forward and 22 daily-cache tests pass unchanged.

Out of scope

A read-only session parse (served when the cache refresh lock times out or
is unavailable) reported itself as a complete hydration, so the daily
backfill published `complete: true` and advanced `lastComputedDate` to
yesterday over days the parse never covered. Because gapStart is
lastComputedDate + 1, those days were never looked at again — observed as a
cache marked complete at 2026-07-28 whose newest entry was 2026-07-25.

- parser: a read-only run reports a complete hydration only when nothing
  changed under the snapshot it served (a skipped or staled file makes it
  partial).
- daily-cache: only a complete parse may advance `lastComputedDate`, on both
  the gap and the full re-derive paths.
- daily-cache: a cache whose watermark outruns its newest populated day has
  its watermark pulled back to that day, so the ordinary gap parse re-derives
  the tail instead of trusting the marker.

Days are only ever added or re-derived, never dropped; the preservation bias
is unchanged and covered by test.
@iamtoruk
iamtoruk force-pushed the fix/daily-cache-degraded-completeness branch from 7e7c934 to 213f4cb Compare July 30, 2026 00:39

@iamtoruk iamtoruk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix is real and load-bearing — reproduced the field bug end-to-end on a 58-day corpus through the real parser: on main, four contended runs finalize the cache with two days permanently lost; here the watermark holds and the healed run recovers them. Gate placement in daily-cache.ts is right (baseline-wins merge, retention anchoring intact), the three "complete" concepts stay properly distinct, healthy-corpus output is byte-identical to main (cache hashes matched across three corpus shapes), and the port to the extraction branch is path-only.

One change requested before merge — performance, not correctness:

The signal is date-blind, and that costs ~2× per invocation in two common healthy states. Measured on 1,425–1,450 session files: idle-tail cache (newest day 4 days ago, nothing missing) 63ms → 124ms; sustained contention with only today's transcript growing 53ms → 115ms. Mechanism: any cache whose newest day ≠ yesterday gets a watermark pull-back + full gap re-parse + cache rewrite on every run, and a contended run's complete:false sends the next run down the 365-day re-derive path (4/4 and 7/7 consecutive contended runs re-derived in my probes) — during active work, today's file is always growing, so this is the normal case, on the desktop poll path. Two cheap mitigations, either suffices: date-scope the flag (don't set it when the stale/skipped file can't carry a turn dated ≤ yesterday — both mtimes ≥ local midnight with straddle slack), and/or on the gap path hold the watermark but leave complete alone (recovery still works via the pull-back, and the next run re-derives a 2-day gap instead of 365 days).

Description corrections (cheap, worth doing in the same push):

  • The title reads as if the degraded-parse class is closed; the signal covers one mode of ~five. Confirmed unflagged by probe: permission-denied config dirs (collectJsonlFiles' readdir().catch(() => []) turns EACCES into a silent empty scan that finalizes undercounted history — the biggest remaining hole, deserving its own issue), per-file failed:true markers, the if (!fp) continue skips (a one-liner fix inside this PR's own signal), and network providers in read-only mode. Suggest retitling to the mode actually covered and filing the EACCES swallow as a follow-up.
  • "All confirmed failing before the change": 3 of the 9 new tests pass on main.
  • The "Preservation" section should note that days whose source files expire during the degraded window are still lost — the guard delays the write, it doesn't capture the data. (The deeper fix, if ever wanted: let read-only runs parse uncached files in memory without publishing.)
  • A permanently unavailable lock now means permanent 365-day re-derive per run rather than wrong finalization — right trade, but the body argues it can't happen; it can, and it self-heals on the first uncontended run. Worth stating.

Also: branch is 5 commits behind main (clean merge per merge-tree, unrelated regions) — please rebase. The test suite's single red (cli-durable-totals.test.ts:182) is the known pre-existing flake, identical on both trees.

The synthetic gate tests are fine as unit coverage, but the two halves (parser signal → daily-cache gate) are never integration-tested together — happy to share my real-parser degraded→healthy probe as a starting point; a case asserting the flag is NOT set when only today's file grew would also lock in the perf fix.

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.

2 participants