diff --git a/src/adapters/google-antigravity-replay.ts b/src/adapters/google-antigravity-replay.ts index 3d574b54d9..3fd2d66d4e 100644 --- a/src/adapters/google-antigravity-replay.ts +++ b/src/adapters/google-antigravity-replay.ts @@ -37,8 +37,8 @@ const MIN_SIGNATURE_LEN = 16; const REPLAY_TTL_MS = 60 * 60 * 1000; // 1h export const ANTIGRAVITY_REPLAY_MAX_ENTRIES = 10_240; const REPLAY_EVICT_BATCH = 128; -const REPLAY_MAX_CALLS_PER_SESSION = 256; -export const ANTIGRAVITY_REPLAY_MAX_BYTES_PER_SESSION = 2 * 1024 * 1024; +const REPLAY_MAX_CALLS_PER_SESSION = 8_192; +export const ANTIGRAVITY_REPLAY_MAX_BYTES_PER_SESSION = 8 * 1024 * 1024; export const ANTIGRAVITY_REPLAY_MAX_TOTAL_BYTES = 64 * 1024 * 1024; const REPLAY_MAX_SIGNATURE_BYTES = 64 * 1024; /** Fixed 64-hex outer key length, counted once per session entry. */ diff --git a/structure/04_transports-and-sidecars.md b/structure/04_transports-and-sidecars.md index 699993a97e..bd14b0c014 100644 --- a/structure/04_transports-and-sidecars.md +++ b/structure/04_transports-and-sidecars.md @@ -779,6 +779,14 @@ so matching uses the provider-visible tool name. - 다른 대안 대신 이 방식을 선택한 이유: Responses ids are not Gemini signatures and previously caused Base64/TYPE_BYTES failures; a second cache duplicates limits; an unscoped cache could send provider-private state across destinations. - 장점, 단점 및 영향: Tool loops continue with exact opaque state and bounded memory while cross-transport reuse fails closed. Replay remains process-local, matching the existing Antigravity contract. +[Decision Log: Deep-session replay capacity scaling] +- 목적과 의도: Prevent premature LRU eviction of early function call signatures in deep conversations (500+ / 1,500+ calls) on Google Antigravity / Vertex without compromising the global memory or disk snapshot bounds. +- 기존 구현 및 제약 조건: `REPLAY_MAX_CALLS_PER_SESSION` was capped at 256 calls and `REPLAY_MAX_BYTES_PER_SESSION` at 2 MiB, which evicted early historical calls once active sessions exceeded 256 calls, causing upstream Gemini to reject the turn with HTTP 400 (`Function call is missing a thought_signature in functionCall parts`). +- 검토한 주요 대안: Retain the 256-call cap and rely on client-side re-generation; remove the per-session cap entirely; scale the per-session limits to 8,192 calls and 8 MiB while keeping the unchanged 64 MiB global cap, 10,240 session cap, and 24 MiB snapshot write bound. +- 선택한 방식: Scale `REPLAY_MAX_CALLS_PER_SESSION` to 8,192 and `REPLAY_MAX_BYTES_PER_SESSION` to 8 MiB. Retain the unchanged 1-hour `REPLAY_TTL_MS`, 10,240 session cap, 64 MiB global memory cap, and 24 MiB snapshot disk cap. +- 다른 대안 대신 이 방식을 선택한 이유: 8,192 calls per session covers long-running multi-agent tasks and 1,500+ call deep transcripts without memory leaks, while the global 64 MiB cap and snapshot LRU sweep protect against unbounded heap growth. +- 장점, 단점 및 영향: Deep sessions with up to 8,192 calls reliably restore their historical thought signatures without 400 rejections; global memory limits remain strictly enforced. + ## Google tool-result adjacency repair Google-family requests serialize a model tool-call turn and its results as one adjacent diff --git a/tests/google-antigravity-replay.test.ts b/tests/google-antigravity-replay.test.ts index 8fab813cca..77dac5f309 100644 --- a/tests/google-antigravity-replay.test.ts +++ b/tests/google-antigravity-replay.test.ts @@ -298,6 +298,50 @@ describe("antigravity reasoning-replay cache", () => { expect(contents.every(c => typeof (c.parts[0] as { thoughtSignature?: string }).thoughtSignature === "string")).toBe(true); }); + test("preserves and restores signatures in deep 1500+ call sessions under production default limits", async () => { + const totalCalls = 1_500; + for (let i = 0; i < totalCalls; i++) { + observeAntigravityReplay(MODEL, SESSION, [fcPart("exec", { cmd: `cmd-${i}` }, `sig-call-${i}-${"a".repeat(24)}`)]); + } + const metricsBefore = antigravityReplayMetrics(); + expect(metricsBefore.calls).toBe(totalCalls); + + // Both earliest (position 0) and latest (position 1499) calls must restore under default limits: + const testContents = [ + { role: "model", parts: [fcPart("exec", { cmd: "cmd-0" })] }, + { role: "model", parts: [fcPart("exec", { cmd: `cmd-${totalCalls - 1}` })] }, + ]; + applyAntigravityReplay(MODEL, SESSION, testContents); + expect((testContents[0].parts[0] as { thoughtSignature?: string }).thoughtSignature).toContain("sig-call-0-"); + expect((testContents[1].parts[0] as { thoughtSignature?: string }).thoughtSignature).toContain(`sig-call-${totalCalls - 1}-`); + + // Verify survival across durable snapshot flush, reset, and reload: + await flushAntigravityReplay(); + setAntigravityReplayLimitsForTests(); + const reloadedContents = [ + { role: "model", parts: [fcPart("exec", { cmd: "cmd-0" })] }, + { role: "model", parts: [fcPart("exec", { cmd: `cmd-${totalCalls - 1}` })] }, + ]; + applyAntigravityReplay(MODEL, SESSION, reloadedContents); + expect((reloadedContents[0].parts[0] as { thoughtSignature?: string }).thoughtSignature).toContain("sig-call-0-"); + expect((reloadedContents[1].parts[0] as { thoughtSignature?: string }).thoughtSignature).toContain(`sig-call-${totalCalls - 1}-`); + }); + + test("retains session between 2 MiB and 8 MiB without tripping the old 2 MiB cap", () => { + // 5000 calls x (64 key bytes + 500 signature bytes) = ~2.8 MiB (exceeds the old 2 MiB session cap): + const callCount = 5_000; + for (let i = 0; i < callCount; i++) { + observeAntigravityReplay(MODEL, SESSION, [fcPart("exec", { index: i }, `sig-${i}-${"s".repeat(500)}`)]); + } + const metrics = antigravityReplayMetrics(); + expect(metrics.calls).toBe(callCount); + expect(metrics.totalBytes).toBeGreaterThan(2 * 1024 * 1024); + expect(metrics.totalBytes).toBeLessThanOrEqual(8 * 1024 * 1024); + const contents = [{ role: "model", parts: [fcPart("exec", { index: 0 })] }]; + applyAntigravityReplay(MODEL, SESSION, contents); + expect((contents[0].parts[0] as { thoughtSignature?: string }).thoughtSignature).toContain("sig-0-"); + }); + test("evicts oldest inner call at the exact per-session count boundary", () => { setAntigravityReplayLimitsForTests({ maxCallsPerSession: 2 }); observeAntigravityReplay(MODEL, SESSION, [fcPart("one", {}, "sig-one-aaaaaaaaaaaa")]);