…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>
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