Repository navigation
fix(client): reject pre-reset cache writes after resetForReconnect - #3092
liangqianxing wants to merge 2 commits into
Conversation
…rReconnect When `ClientResponseCache.resetForReconnect()` clears `_evictionGeneration`, an in-flight request from before the reset whose generation snapshot was `0` previously passed the `write()` guard (`_evictionGeneration.get(gk) ?? 0`) both when no post-reset capture had happened yet and when a post-reset request had also captured generation `0` and written. As a result, a delayed pre-reset response could populate the new server identity's partition or overwrite a fresh post-reset cache entry. Require `_evictionGeneration.get(gk) === capturedGen` (rejecting writes when no capture has occurred since construction or reset) and track keys that had a pre-reset generation `0` capture across `resetForReconnect()` so that after the post-reset generation `0` capture writes once, any delayed pre-reset generation `0` write for that key is ignored. Fixes modelcontextprotocol#3075 Co-Authored-By: Claude Code <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 96daba1 The changes in this PR will be included in the next version bump. This PR includes changesets to release 6 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
@modelcontextprotocol/client
@modelcontextprotocol/codemod
@modelcontextprotocol/core
@modelcontextprotocol/server
@modelcontextprotocol/server-legacy
@modelcontextprotocol/express
@modelcontextprotocol/fastify
@modelcontextprotocol/hono
@modelcontextprotocol/node
commit: |
|
Read the diff of
A side effect of the one-write rule: two legitimate post-reset requests that both captured The root cause is that |
Replace the per-key generation-0 tracking sets with a monotonic `_generationBase` that starts at `0` and advances past the highest recorded generation when `resetForReconnect()` clears a non-empty `_evictionGeneration` map. `captureGeneration()` and `evict()` now fall back to `this._generationBase` instead of `0`, so post-reset generation snapshots are strictly greater than any pre-reset snapshot (including keys evicted before reset) while `_evictionGeneration` remains cleared on reconnect and concurrent post-reset captures can write normally. Co-Authored-By: Claude Code <noreply@anthropic.com>
|
Thanks for catching those orderings — you're right that as long as I've replaced That keeps I also updated the |
|
Read One case I would add to the matrix: two resets in a row with nothing captured in between, then a stale write using a token from before the first reset. The Question: does anything outside |
Summary
Fixes #3075.
When
ClientResponseCache.resetForReconnect()clears_evictionGeneration, an in-flight request from before the reset whose generation snapshot was0previously passed thewrite()guard ((this._evictionGeneration.get(gk) ?? 0) !== capturedGen) in two cases:_evictionGeneration.get(gk)wasundefined, falling back via?? 0to0 === 0).0(both snapshots shared the same numeric token).Changes
packages/client/src/client/responseCache.ts:this._evictionGeneration.get(gk) === capturedGeninwrite()without the?? 0fallback so writes for keys that have not captured a generation in the current connection epoch are rejected._generationBase(starting at0and advancing tomax(this._generationBase, ...this._evictionGeneration.values()) + 1whenresetForReconnect()clears a non-empty_evictionGenerationmap) and use?? this._generationBaseincaptureGeneration()andevict(), ensuring post-reset tokens are strictly greater than any pre-reset token while still clearing_evictionGenerationacross reconnects.packages/client/test/client/responseCache.test.ts:resetForReconnectunit test to verify that_evictionGenerationis emptied and that the next capture advances past the pre-reset counter.private/publicscopes ×changeServer(false/true) ×ordering(none,capture-write-stale,capture-stale-write,write-recapture-stale,concurrent-post-reset,evicted-pre-reset-and-post-reset)..changeset/reject-pre-reset-cache-writes.md:@modelcontextprotocol/client.Verification
pnpm --filter @modelcontextprotocol/client test(1,100 tests passed)pnpm --filter @modelcontextprotocol/client run typecheckpnpm --filter @modelcontextprotocol/client run lint🤖 Generated with Claude Code