Skip to content

fix(parser): avoid marking partially scanned caches complete (#912) - #1229

Merged
ozymandiashh merged 3 commits into
getagentseal:mainfrom
earbona23:test/scoped-run-stamps-whole-cache-complete
Sep 10, 2026
Merged

ozymandiashh merged 3 commits into
getagentseal:mainfrom
earbona23:test/scoped-run-stamps-whole-cache-complete

Conversation

@earbona23

@earbona23 earbona23 commented Sep 1, 2026 •

Copy link
Copy Markdown
Contributor

A provider-scoped parse could mark the whole session cache complete while another provider still had discoverable sessions that the run had not scanned. This change withholds the completeness stamp in that case, while preserving it on single-provider installations so warm-refresh snapshot fallback keeps working.

The additional discovery runs only for writable, previously incomplete scoped scans that did not defer files for first paint. It discovers sources across providers without parsing their full sessions; discovery may still read metadata or query databases. The demonstrated impact of the original bug is transient: a subsequent full refresh rediscovers the omitted provider and repairs the cache.

Adds ordinary regression tests for the omitted-provider case and recovery on a full refresh, with isolated provider roots. Addresses the provider-scoped completeness case in #912.

Validation at 0e8a379e5c5aacc2e6c3ac3fb8b0c6d6e240f9e0:

  • 34 targeted tests and TypeScript pass locally, both on the PR head and after integrating current main (eaed4240).
  • All 15 applicable GitHub checks pass, including both Node 22 test jobs, the serial cache-lock suites, desktop, upgrade paths, and package builds.
  • The release-only asset check is skipped as expected.

Repro for the `parser.ts` candidate in getagentseal#912: a run filtered to one provider
leaves every other provider unscanned and still marks the whole cache complete.

Three lines in runParseInner tell it. Discovery is filtered — `discoverAllSessions(providerFilter)`.
The loop over cached providers skips out-of-scope names — `if (providerFilter && providerFilter
!== 'all' && providerFilter !== providerName) continue`. Then the stamp fires guarded only on
readOnly / wasComplete / deferredForFirstPaint, with nothing about scope.

That is worse than the silent zeros in getagentseal#874 and getagentseal#899, which lasted one run. This one is
written to disk: once the cache is stamped, later launches stop coming back cold for those
providers, so the gap stops looking like a gap.

Test only, per the issue. It asserts its own premise first — that claude really was left
unscanned — because otherwise the second assertion proves nothing.

Mutation-sensitive, checked both ways rather than asserted:
- as-is, the assertion fails with `expected true to be false`
- adding `&& !scopedRun` to the stamp makes it pass
- reverting makes it fail again

Marked `it.fails` so the suite stays green while the defect is open. When the stamp learns
about scope this test starts passing, vitest reports "Expect test to fail", and that is the
signal to drop `.fails` and keep it as a plain regression test. Verified that flip too.
@ozymandiashh

Copy link
Copy Markdown
Collaborator

Thanks — the first invariant is reproduced, and I independently got the same flip at ca541de: the normal assertion receives true, while guarding the global stamp makes it pass.

Before turning this into a fix, please tighten two things:

  1. Make the fixture hermetic. The test calls parseAllSessions(undefined, 'codex'), but redirects only CLAUDE_CONFIG_DIR, CODEBURN_CACHE_DIR, and CODEBURN_DESKTOP_SESSIONS_DIR. Codex discovery still defaults to CODEX_HOME / ~/.codex, so on a developer machine this test reads the real local corpus. Point Codex discovery at an empty temp root and restore the environment in teardown.

  2. Pin the actual consequence and narrow the claim. I simulated a later process (clearSessionCache(), then parseAllSessions(undefined, 'all')); Claude was discovered and written to cache. An ordinary all-provider refresh therefore rehydrates the omitted provider, so the current "nothing re-derives them" / persistent-zero claim is not established by this test.

Please add either an assertion documenting that the next full run repairs the cache plus a real first-paint/snapshot repro demonstrating the transient mislabel, or another real caller-path repro that proves persistence. Once the scope is accurate, the conservative guard can come into this PR and it.fails can become a normal regression test.

Addresses review on getagentseal#1229.

1. Hermetic fixture. The scoped run is `parseAllSessions(undefined, 'codex')`, and Codex
   discovery falls back to CODEX_HOME / ~/.codex, so the test was reading the developer's
   real corpus. It now points CODEX_HOME at an empty temp root and restores the prior value
   (including unset) in teardown.

2. Claim narrowed. You are right that a subsequent all-provider refresh rediscovers the
   omitted provider, so "nothing re-derives them" was overstated. Dropped it. Added a test
   that pins the actual bound: after the scoped run, a plain `parseAllSessions()` repairs the
   cache and claude reappears. The mislabel is transient, not a persistent zero.

The `it.fails` invariant is unchanged and still mutation-sensitive: guarding the stamp with
`&& !scopedRun` flips it to a real pass (vitest then reports "Expect test to fail"), reverting
flips it back. Ready for the guard to land here and `.fails` to become a normal regression
test whenever you want to pull the fix into this PR.
@earbona23

Copy link
Copy Markdown
Contributor Author

Both addressed, pushed.

1. Hermetic. Good catch — the scoped run is codex, and Codex discovery falls back to CODEX_HOME / ~/.codex, so the test was reading the real local corpus on a dev machine. It now sets CODEX_HOME to an empty temp root and restores the prior value (including the unset case) in teardown.

2. Claim narrowed — you are right, and I have dropped the overstatement. Your clearSessionCache() → parseAllSessions(undefined, 'all') check is correct: an ordinary full refresh rediscovers the omitted provider, so "nothing re-derives them / persistent zero" was not established, and it is gone. There is now a test asserting exactly that bound — a plain parseAllSessions() after the scoped run repairs the cache and claude reappears. The mislabel is transient, not persistent.

On the snapshot angle you offered as the alternative: I traced it and it is real — canServeCompleteSnapshot() (parser.ts L5190) gates only on isCacheComplete(), so a first-paint all render after a scoped run can serve a snapshot that structurally omits the unscanned provider: one wrong paint, corrected by the background full refresh. I did not ship it as a test, because pinning it needs a non-empty Codex section in the right on-disk format, and I would rather not contort a fixture to force it. Flagging it as reasoned-from-source, not proven — if you think it is worth pinning, point me at the Codex session fixture helper and I will add it properly.

The it.fails invariant is unchanged and still mutation-sensitive. Whenever you want the conservative guard (&& !scopedRun on the stamp) in this PR, say so and I will add it with .fails removed — I have it ready and verified in both directions.

@ozymandiashh

Copy link
Copy Markdown
Collaborator

Go ahead and bring the conservative scoped-run guard into this PR, remove it.fails, and keep the tests as normal regressions. The narrowed transient-impact claim is sufficient; no additional first-paint fixture is needed. Thanks.

@earbona23

Copy link
Copy Markdown
Contributor Author

I implemented the guard, ran the full suite before pushing it, and it regresses four legitimate tests — so I have held it back rather than land a green-looking fix that breaks your snapshot path. Reporting precisely, because the blunt version is wrong and the reason matters.

What breaks

Guarding the stamp with && !scopedRun fails tests/parser-cache-refresh-timeout.test.ts (4 tests), e.g.:

  • serves the prior complete snapshot and leaves the holder cache untouched
  • does not report a complete hydration when the served snapshot is stale
  • does not report a complete hydration when a session file is missing from the snapshot

They all drive parseAllSessions(undefined, 'claude') — a scoped run — and depend on it stamping the cache complete so the warm-refresh timeout can serve a prior complete snapshot. On clean main they pass 4/4; with !scopedRun they fail. My repro's scopedRun guard is too broad: it forbids a case your design relies on.

Why the simple guard is wrong

The two situations are genuinely different, and providerFilter != null does not separate them:

scoped provider other providers with sessions on disk stamp complete?
your tests claude none yes — correct
issue #912 codex claude has sessions, unscanned no — the bug

The bug is not "a scoped run stamped complete." It is "a scoped run stamped complete while another provider had on-disk sessions it never scanned." When the scoped provider is the only one with data, stamping complete is right — which is exactly what your tests pin.

The hard part: knowing an unscanned provider has sessions is precisely the discovery a scoped run skips for performance, so the cheap !scopedRun check cannot see it. A correct guard needs one of:

  1. a bounded check at stamp time for whether any non-scoped, known provider has discoverable sessions (costs part of the scoping win), or
  2. moving completeness to per-provider rather than one whole-cache flag, so a scoped run marks only its own provider complete and canServeCompleteSnapshot composes across providers, or
  3. accepting the transient mislabel as documented and keeping this PR test-only.

That is an architecture call on your cache, and you know it far better than a first patch should assume. Which direction do you want? If (2), I am happy to sketch the per-provider flag and carry the snapshot logic with it. If (3), the branch already stands green as the reduced-claim regression tests you approved. I would rather ask than push a guard that quietly narrows what "complete" means for every other caller.

(Full suite run locally, --exclude tests/cache-refresh-lock*, before and after the change; the 4 regressions are deterministic and reproduce on toggling the one line.)

…plete

A run scoped to one provider walks only that provider's sessions, yet the
end-of-parse stamp marked the WHOLE cache complete regardless of scope
(getagentseal#912). Because a complete cache stops being re-read as cold, the providers
the scoped run skipped are never revisited — a wrong "done" written to disk,
where the gap stops looking like a gap.

Guard the stamp on real on-disk data, not on scoping alone: a scoped run
still stamps the cache complete when every provider it skipped has no
discoverable sessions (a single-provider machine — exactly what the
warm-refresh snapshot tests rely on). Only when a skipped provider actually
has sessions on disk is the whole-cache claim withheld. The extra check is a
bounded directory walk, not a parse, so the scoping win holds.

Drops it.fails on the getagentseal#912 regression; it now passes as an ordinary test.
@earbona23

Copy link
Copy Markdown
Contributor Author

Pushed the conservative guard (0e8a379e).

Instead of gating on scoped-vs-all, the whole-cache complete stamp is now suppressed only when the scoped run left another provider's sessions on disk unscanned. So #912 (scope codex, claude sessions present) stops stamping, while the parser-cache-refresh-timeout cases (scope claude, no other provider present) still stamp and stay green — those four failed under the naive !scopedRun version precisely because their second run relies on the complete flag to serve the prior snapshot. The probe is a directory walk, not a parse, so the scoping speedup is preserved.

it.fails is gone — both are normal regressions now. The Codex fixture is hermetic (discovery points at an empty temp root, restored in teardown) and the claim is narrowed to the transient mislabel that the next full run repairs, per your note. tsc --noEmit clean; scoped-run-completeness + parser-cache-refresh-timeout green (6/6), and the scoped non-claude suite (22/22).

@earbona23
earbona23 marked this pull request as ready for review September 2, 2026 18:54
@earbona23

Copy link
Copy Markdown
Contributor Author

Friendly bump — this has been ready since the conservative guard landed in 0e8a379e, per your last note.

Recap of where it stands: the whole-cache complete stamp is suppressed only when a scoped run left another provider's sessions on disk unscanned, so #912 (scope codex with claude sessions present) stops stamping, while the parser-cache-refresh-timeout cases (scope claude, no other provider present) still stamp and stay green. The fixture is hermetic — CODEX_HOME is redirected to an empty temp root and restored in teardown, including the unset case. The tests are plain regressions and the transient-impact claim is the narrowed one.

Full suite is green. Nothing pending on my side.

@ozymandiashh ozymandiashh changed the title test: pin that a provider-scoped run stamps the whole cache complete (#912) fix(parser): avoid marking partially scanned caches complete (#912) Sep 10, 2026
@ozymandiashh
ozymandiashh merged commit 6a0f830 into getagentseal:main Sep 10, 2026
16 checks passed
@ozymandiashh

Copy link
Copy Markdown
Collaborator

Merged — thanks for the fix and for catching the snapshot regression in the initial guard. The focused tests and full CI are green. Appreciate the careful follow-through and your patience!

timothybrush pushed a commit to timothybrush/codeburn that referenced this pull request Sep 12, 2026
A scan is scoped on two axes, provider and date range, but completeness
was one whole-cache boolean. getagentseal#1229 stopped a scoped run from stamping it
by adding a second, unfiltered discoverAllSessions to the stamp path,
which left scoped users on a multi-provider machine permanently cold and
paid an extra full discovery on every run.

Completeness now lives on the provider section, with an optional
completeFrom floor for the sources a date range filtered out. The
whole-cache flag keeps its meaning (an unscoped scan finished) and is
what a section with no flag of its own falls back to, so existing caches
read exactly as before and no version bump is needed. Discovery reports
which providers threw, and a provider that reported nothing because it
failed is never marked complete.
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