Follow-up to PR #2631 (#2571), from the read-only verification pass on its round-2 fixes. None blocks that merge; all are post-logout-only edges of the new session epoch, tracked here for one small follow-up PR.
1. MEDIUM: the map clear inverts the summary-side guard the same way it inverted the detail guard, and the epoch does not cover the list path. applyBackgroundListSnapshot (captureStore.ts ~184-193) keeps a locally newer row only when latestSummaryGenerationById.get(id) ?? 0 exceeds the snapshot's observed generation; after resetForLogout() clears that map the read is 0, so an older in-flight background list snapshot wins. Scenario: a batch triage poll is active; a detail read moves an item to a terminal status via upsertSummary (generation N above the in-flight snapshot's observed generation); the user logs out; between the shell watcher's resetForLogout() and the Inbox unmount's cancelBatchTriagePolling(), the pre-logout list response lands with isCurrent() still true and items regresses the row. The window is sub-millisecond, the route is already leaving, and the next Inbox mount replaces items through the scope-replacement seam, so it is not user-visible today; but the pre-existing spec at captureStore.spec.ts ~2763-2796 asserts the regressed behaviour as intended ("nothing stale-high is left to pin the row"), so the two halves of one guard now carry opposite post-logout contracts. Fix: the list snapshot path captures the session epoch like the detail path and drops a snapshot issued before the reset; retarget that spec.
2. LOW: onRefreshed fires for reads the epoch dropped (captureStore.ts ~651), so refreshedDetailIds in the batch poll can claim an id was reconciled while detailById still holds pre-batch state; post-logout only.
3. LOW: fetchDetail's drop is unreported to callers (three drop paths, no boolean, unlike fetchItems); selectItemById returns true with an empty detail and the Legacy panel shows "Unable to load capture detail." for a read that succeeded; the new path is post-logout only, the other two are pre-existing.
4. LOW: the docstring says every detail read captures the epoch, but peekDetail writes detailById through cacheDetail with no epoch and no generation compare (its only guard is the hash re-check); cacheSummary: false keeps it out of items. Qualify the wording or add the capture.
5. LOW: captureStore.spec.ts ~2560 installs a persistent mockImplementation returning never-resolving promises; the suite's only reset is vi.clearAllMocks(), so a test added below it that triggers an unconfigured detail read hangs instead of receiving undefined. Replace with per-test mockReturnValueOnce or restore the implementation in that test's teardown.
Refs #2571, PR #2631, PR #2567.
Follow-up to PR #2631 (#2571), from the read-only verification pass on its round-2 fixes. None blocks that merge; all are post-logout-only edges of the new session epoch, tracked here for one small follow-up PR.
1. MEDIUM: the map clear inverts the summary-side guard the same way it inverted the detail guard, and the epoch does not cover the list path.
applyBackgroundListSnapshot(captureStore.ts~184-193) keeps a locally newer row only whenlatestSummaryGenerationById.get(id) ?? 0exceeds the snapshot's observed generation; afterresetForLogout()clears that map the read is 0, so an older in-flight background list snapshot wins. Scenario: a batch triage poll is active; a detail read moves an item to a terminal status viaupsertSummary(generation N above the in-flight snapshot's observed generation); the user logs out; between the shell watcher'sresetForLogout()and the Inbox unmount'scancelBatchTriagePolling(), the pre-logout list response lands withisCurrent()still true anditemsregresses the row. The window is sub-millisecond, the route is already leaving, and the next Inbox mount replacesitemsthrough the scope-replacement seam, so it is not user-visible today; but the pre-existing spec atcaptureStore.spec.ts~2763-2796 asserts the regressed behaviour as intended ("nothing stale-high is left to pin the row"), so the two halves of one guard now carry opposite post-logout contracts. Fix: the list snapshot path captures the session epoch like the detail path and drops a snapshot issued before the reset; retarget that spec.2. LOW:
onRefreshedfires for reads the epoch dropped (captureStore.ts~651), sorefreshedDetailIdsin the batch poll can claim an id was reconciled whiledetailByIdstill holds pre-batch state; post-logout only.3. LOW:
fetchDetail's drop is unreported to callers (three drop paths, no boolean, unlikefetchItems);selectItemByIdreturns true with an empty detail and the Legacy panel shows "Unable to load capture detail." for a read that succeeded; the new path is post-logout only, the other two are pre-existing.4. LOW: the docstring says every detail read captures the epoch, but
peekDetailwritesdetailByIdthroughcacheDetailwith no epoch and no generation compare (its only guard is the hash re-check);cacheSummary: falsekeeps it out ofitems. Qualify the wording or add the capture.5. LOW:
captureStore.spec.ts~2560 installs a persistentmockImplementationreturning never-resolving promises; the suite's only reset isvi.clearAllMocks(), so a test added below it that triggers an unconfigured detail read hangs instead of receiving undefined. Replace with per-testmockReturnValueOnceor restore the implementation in that test's teardown.Refs #2571, PR #2631, PR #2567.