Skip to content

[v2] Response cache accepts pre-reset generation-0 writes after reconnect #3075

Open

Description

@ilovvveu

What happened?

Follow-up to the write-side concern raised in #3072 (comment).

At the cache coordinator boundary, a response whose generation was captured before resetForReconnect() can still be written afterward. Reset clears _evictionGeneration; write() treats the missing key as generation 0 via ?? 0, so an old generation-0 token passes. It then derives the storage partition from the current server identity.

Consequences reproduced with a user-supplied store:

  • Reconnect to the same identity: the old response can overwrite a fresh response already cached on the new connection.
  • Reconnect to a different identity: the old response is stored in the new server identity's partition.
  • Both private and public scope are affected.

I ran a deterministic coordinator-level matrix: private/public × same/different server identity × with/without a fresh write before the old write. All 8 cases accept the stale write on both pinned main 9adb8d5 and #3072 head 15e9641. This establishes the coordinator behavior; it is not an end-to-end transport reconnect reproduction. Client._listAllPages() and _readResource() capture a generation before awaiting their fetch and pass it to write() afterward.

Related scope:

AI assistance was used to investigate and reproduce this report.

What did you expect?

A generation snapshot captured on an old connection should not authorize a cache write after reset, even when both per-key counters are 0. Fresh writes and intentional reuse of retained store entries should continue to work.

A possible direction is an internal snapshot/token containing both connection and per-key generations, captured before the asynchronous fetch and checked by write(). Capturing a connection generation only when write() begins would miss the demonstrated ordering. Ordering of store mutations already in flight is a separate concern and also needs to be considered alongside #2688.

Code to reproduce

// Run from a repository checkout with dependencies installed:
// pnpm exec tsx repro.ts
// ClientResponseCache is an internal export, not a package-root API.
import assert from 'node:assert/strict';
import { ClientResponseCache, InMemoryResponseCacheStore }
  from './packages/client/src/client/responseCache.ts';

const store = new InMemoryResponseCacheStore();
const cache = new ClientResponseCache(store, true, undefined, 'alice');
cache.setServerIdentity('server-a');
const oldGeneration = cache.captureGeneration('tools/list'); // 0

cache.resetForReconnect();
cache.setServerIdentity('server-b');
const freshness = { expiresAt: Date.now() + 60_000, scope: 'private' as const };
const newGeneration = cache.captureGeneration('tools/list'); // also 0
await cache.write('tools/list', { tools: [{ name: 'new' }] }, newGeneration, freshness);

// Model the old response reaching the write boundary only after reset.
await cache.write('tools/list', { tools: [{ name: 'old' }] }, oldGeneration, freshness);
const hit = await cache.read('tools/list');
assert.deepEqual(hit?.value, { tools: [{ name: 'new' }] });
// Fails: actual is { tools: [{ name: 'old' }] }, under server-b's partition.

SDK version

main 9adb8d5; also PR #3072 head 15e9641; macOS, Node 22.23.3, pnpm 10.26.1

Area

Client

Activity

  1. added
    v2Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes
    on Oct 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    v2Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions