Uh oh!
There was an error while loading. Please reload this page.
Committed-session index stamps atMs at scan start, so a slow scan never serves a cache hit - #689
Conversation
…er serves a hit (#686) The committed-session index stamped its freshness timestamp when the session_id scan STARTED. A scan that itself takes longer than SESSION_INDEX_REBUILD_MS was therefore already stale when it resolved, so the very next miss rebuilt immediately: back-to-back whole-table scans that never serve a single cache hit, on exactly the table size that makes the index worth having. atMs now starts at Infinity and is stamped when the scan COMPLETES, so the window ages the answer rather than the attempt. The entry is still published to `built` synchronously, so concurrent callers share the in-flight scan and the round-1 stampede stays fixed; the stamp is chained onto the scan promise so it is ordered before every awaiter. Co-Authored-By: Claude <noreply@anthropic.com>
philcunliffe
commented
Aug 9, 2026
Neutral review round: PR #689 @ |
Uh oh!
There was an error while loading. Please reload this page.
Both sides appended a new test at the same point in test/plugins/ai-gateway-message-projector.test.js: - master (#688) added 'committed-session index: a build that could not scan is not cached as "no committed rows"' - this branch added 'seed failure: a storage that breaks its discover contract loses no rows and does not poison the session memo' The tests cover different defects and neither is redundant, so both are kept. master's test is placed first, directly after the test its own comment refers to ('a throwing storage degrades to not-seeded'). message_projector.js merged without conflict: #689 reworked rebuild() in the committed-session index, this branch changed seedSeenMessagesForSession and scanCommittedMessageIds, and the regions are disjoint. Co-Authored-By: Claude <noreply@anthropic.com>
Both sides changed `createCommittedSessionIndex().rebuild()`: - master (#689) chained the self-clearing guard onto the scan so `atMs` is stamped when the scan COMPLETES; - this branch (#685) normalizes a rejecting scan to the `undefined` "could not scan" outcome so the failure degrades the index instead of escaping. Resolved by keeping both: the `.catch` normalization now wraps the scan promise that master's chain is built on, so the stamp-on-completion and the shared in-flight attempt are untouched and the chained handler still only ever sees a fulfilled value. master's chaining incidentally gave the rejection an awaiter, so the unhandled-rejection-kills-the-daemon shape is no longer reachable on its own; the wedge it caused still is. Without the `.catch`, the rejected attempt stays published in `built` and never self-clears, so every later exchange on that listener re-awaits the same rejection (verified: 3 sequential exchanges, all rejected, index never rebuilt). The regression test now pins that directly with a second exchange, and keeps the unhandled-rejection assertion so the chaining cannot silently regress. Co-Authored-By: Claude <noreply@anthropic.com>
What was wrong
hypaware-core/plugins-workspace/ai-gateway/src/message_projector.jsstamped the committed-session index's freshness timestamp when itssession_idscan started:while the freshness test in
mightHaveCommittedRowsreadsnow() - current.atMs < SESSION_INDEX_REBUILD_MS.Root cause
The rebuild window was aging the attempt, not the answer. Any full
session_idscan that itself takes longer thanSESSION_INDEX_REBUILD_MS(10 min) resolves already stale, so the very next miss rebuilds at once: the index rebuilds back-to-back indefinitely and never serves a single cache hit, on exactly the table size that makes the index worth having. As the deferred finding from #683's round-2 review notes, this does not stampede (one chained rebuild at a time) and is still better than the pre-#683 one-scan-per-session, so it is a degradation of the optimisation, not a regression against master.The fix
atMsstarts atInfinity(never stale) and is stamped when the scan completes. The entry is still published tobuiltsynchronously, so concurrent callers keep sharing the in-flight scan and the round-1 stampede fix is untouched; only the timestamp is deferred. The stamp is chained onto the scan promise (rather than a floating.then) so it is structurally ordered before every awaiter, not dependent on microtask registration order. The self-clearing failed-build guard moved into that same handler unchanged.Also updated: the
SESSION_INDEX_REBUILD_MSJSDoc and the matching bullet in LLP 0204 (Status: Draft) now say the window is measured from scan completion.Regression test
test/plugins/ai-gateway-message-projector.test.js- "committed-session index: a scan slower than the rebuild window still serves cache hits". It gates the stub'sdiscoverCachePartitionson a promise, advances the injected clock pastSESSION_INDEX_REBUILD_MSwhile the scan is in flight, releases it, and then asserts a later miss one millisecond after completion is served from the index.FAIL before the fix (fixed test, source reverted to
origin/master):PASS after the fix, with the existing single-rebuild-under-concurrency and rebuild-after-window tests still green:
Verification
npm test:# tests 3864 / # pass 3858 / # fail 0 / # skipped 6npm run typecheck: clean, exit 0🤖 Generated with Claude Code
Fixes#686