Repository navigation
Conversation
…results race The thread-detail bootstrap and the environment rename reply wrote Environment and Host data straight into their dedicated query caches, and those queries' fetches wrote their results back unconditionally. Whichever landed last won, so an older response could overwrite newer data until the next realtime event. Compare updatedAt at every write point: the rename reply and fetch results keep the cached copy only when it is strictly newer, and the bootstrap, a side-channel prefill, replaces the cached copy only when it is strictly newer. Host status is derived from live sessions and a disconnect does not bump updatedAt, so on a tie the dedicated query's own copy wins over the bootstrap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…start replies Saving a plugin setting, and starting or cancelling a plugin install or update, wrote the server's reply straight into the cache. A fetch of the same key already in flight, such as a realtime refetch or the update job list's one-second poll, then landed with older data: the settings form reset its draft to the old value, a cancelled install flipped back to running, and new jobs dropped out of the list. Route these writes through setQueryDataOverInFlightFetch: when a fetch of the key is in flight it cancels that fetch, which reverts to the written value, and refetches, so the cache holds the reply until a fetch started after the write lands. With no fetch in flight it is a plain write. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g fresh replies Applying or acknowledging an app update, forcing a version check, and changing the AI service each wrote the server's reply straight into the cache while a focus, realtime or poll fetch of the same key could still be in flight. When that fetch landed later it put the older value back: an idle app update phase switched off the one-second progress poll, an unforced version result replaced the forced one, and the AI service selection flickered back. Route these writes through setQueryDataOverInFlightFetch, which cancels an in-flight fetch of the key and refetches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ymichael
marked this pull request as ready for review
October 30, 2026 06:06
ymichael
deleted the
bb/fix-stale-response-cache-races-from-the-cache-wr-thr_6jchqxrk2u
branch
October 30, 2026 16:36
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Human comments
What was wrong
Several cache owners wrote a server reply or the thread-detail bootstrap straight into the React Query cache with
setQueryDatawhile a fetch of the same key could still be in flight. React Query writes fetch results without checking whether they're older than what's cached (query-corequery.ts), so the older fetch landed last and put stale data back. Effects included: an environment or host status that stayed stale after opening a thread, a plugin settings form resetting its draft to the old value, a cancelled plugin install flipping back to running, new plugin update jobs dropping out of the list, anidleapp update phase switching off the progress poll, and the AI service selection flickering back. These are items #2–#10 from the direct cache write audit (@thread:thr_fjr5x8dzaz). #1 (thread tabs) is fixed separately in #6611.What changed
Two fixes, depending on whether the data has a version field:
updatedAt(Milestone 1: shared foundation plus Linux support #2, Add sidebar row context menus #3, [codex] add dev auto multi-stack runner #10). The bootstrap ingest (thread-detail-cache-owner.ts), the rename reply (applyEnvironmentUpdateResult), and theuseEnvironment/useHostsfetches all compareupdatedAt. The fetch-side check is what catches a fetch that started before the write and lands after it. To make it testable, those fetches now live inenvironmentQueryOptionsandhostsQueryOptions. Hoststatuscomes from live daemon sessions and a disconnect doesn't changeupdatedAt, so on a tie the cached copy wins over the bootstrap. The bootstrap only fills an empty cache or replaces strictly older data. Rename replies and fetch results still win ties.setQueryDataOverInFlightFetch(cache-effect-utils.ts) for data with no version ([codex] Bundle Claude Code SDK CLI support file #4–[codex] Add Cmd+Enter steer sends #9): it writes the value, and if a fetch of the key is in flight it cancels that fetch (the cache keeps the written value) and refetches. With nothing in flight it's a plain write. Used byapplyPluginSettingsView,applyPluginInstallJob,applyPluginUpdateJob,hydrateAppUpdateStatus,hydrateSystemVersionCacheandwriteCachedAiServices, so every caller gets the fix. Where the audit suggested an unconditional cancel-write-invalidate ([codex] Route new manager flow #7) or a cancel before the write (fix(agent-runtime): expose user/global ~/.claude settings to Claude SDK #8, [codex] Add Cmd+Enter steer sends #9), this helper refetches only when a fetch was actually in flight. That avoids an extra request on every write. Forced version and app-update checks update the server's cache, so the trailing unforced refetch returns the forced result.No wire, CLI or plugin API changes.
Not covered here: audit items #11 (plugin safe mode) and #12 (prompt history), both low risk. Also not covered: a lint rule restricting direct
setQueryDatacalls to the helper, which would stop new call sites from bringing the race back.How you verified
QueryClient/QueryObserverinstances: start a fetch that resolves late with older data, do the write, let the fetch land, then check that the newer value survives (and, for the helper, that the trailing refetch runs).thread-detail-cache-owner.test.ts(new): late environment and host-list fetches after a bootstrap, an older bootstrap over a newer cache, and the equal-updatedAthost status case.environment-workspace-cache-owner.test.ts: a late environment fetch after a rename, and an older rename reply over a newer cache.in-flight-fetch-writes.test.ts(new): one row per cache-owner write for [codex] Bundle Claude Code SDK CLI support file #4–[codex] Add Cmd+Enter steer sends #9, plus helper tests for the no-fetch case (no extra request) and a fetch of an unobserved query.pnpm exec turbo run typecheck lint test --filter=@bb/app: passed (647 files, 6515 tests; 0 lint errors, no new warnings). After rebasing onto0472409243I re-ran the three changed test files (21 tests passed).🤖 Generated with Claude Code