From 12e07f646f4283500686f6a888693e437582c0f5 Mon Sep 17 00:00:00 2001 From: Ingwannu Date: Sun, 9 Aug 2026 04:41:29 +0000 Subject: [PATCH] fix(vertex): scope replay by client thread --- src/adapters/google.ts | 4 +- tests/google-vertex-thought-signature.test.ts | 38 +++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/adapters/google.ts b/src/adapters/google.ts index 91f8bc0994..f8709714a7 100644 --- a/src/adapters/google.ts +++ b/src/adapters/google.ts @@ -54,8 +54,8 @@ function resolveVertexApiKey(optKey?: string): string | undefined { /** Prefer Codex's stable opaque thread key; retain the existing deterministic fallback for clients * that omit it. The replay store hashes this value and never retains the raw session identifier. */ function vertexReplaySessionId(parsed: OcxParsedRequest): string { - const promptCacheKey = parsed.options.promptCacheKey?.trim(); - return promptCacheKey || antigravitySessionId(parsed); + const threadId = parsed._clientThreadId?.trim(); + return threadId || antigravitySessionId(parsed); } /** diff --git a/tests/google-vertex-thought-signature.test.ts b/tests/google-vertex-thought-signature.test.ts index f24a33f7c5..5761462da8 100644 --- a/tests/google-vertex-thought-signature.test.ts +++ b/tests/google-vertex-thought-signature.test.ts @@ -55,6 +55,16 @@ const continuation = () => request([ }, ], false); +function scopedReplayRequest( + parsed: OcxParsedRequest, + threadId: string | undefined, + promptCacheKey: string | undefined, +): OcxParsedRequest { + if (threadId !== undefined) parsed._clientThreadId = threadId; + if (promptCacheKey !== undefined) parsed.options.promptCacheKey = promptCacheKey; + return parsed; +} + function vertexResponseBody(): Record { return { candidates: [{ @@ -107,6 +117,34 @@ describe("Vertex thought-signature continuation (#1254)", () => { expect(replayedFunctionCall(followup.body as string).thoughtSignature).toBe(SIGNATURE); }); + test("#1312: shared prompt cache keys cannot cross client-thread replay namespaces", async () => { + const first = scopedReplayRequest(firstTurn(false), "thread-a", "shared-cache-cohort"); + const firstAdapter = createGoogleAdapter(provider); + await firstAdapter.buildRequest(first); + await firstAdapter.parseResponse!(new Response(JSON.stringify(vertexResponseBody()))); + + const otherThread = await createGoogleAdapter(provider).buildRequest( + scopedReplayRequest(continuation(), "thread-b", "shared-cache-cohort"), + ); + expect(replayedFunctionCall(otherThread.body as string).thoughtSignature).toBeUndefined(); + + const originalThread = await createGoogleAdapter(provider).buildRequest( + scopedReplayRequest(continuation(), "thread-a", "different-cache-cohort"), + ); + expect(replayedFunctionCall(originalThread.body as string).thoughtSignature).toBe(SIGNATURE); + }); + + test("#1312: threadless clients keep deterministic replay regardless of prompt cache key", async () => { + const firstAdapter = createGoogleAdapter(provider); + await firstAdapter.buildRequest(scopedReplayRequest(firstTurn(false), undefined, "cohort-a")); + await firstAdapter.parseResponse!(new Response(JSON.stringify(vertexResponseBody()))); + + const followup = await createGoogleAdapter(provider).buildRequest( + scopedReplayRequest(continuation(), undefined, "cohort-b"), + ); + expect(replayedFunctionCall(followup.body as string).thoughtSignature).toBe(SIGNATURE); + }); + test("Vertex signatures cannot enter the Antigravity replay namespace", async () => { const first = firstTurn(false); const adapter = createGoogleAdapter(provider);