From 03f7f6bb80228c87f942662dd073edbaf12b73ca Mon Sep 17 00:00:00 2001 From: agentHits Date: Sat, 22 Aug 2026 14:13:26 +0300 Subject: [PATCH 1/2] fix(google): scale antigravity replay capacity and TTL for deep sessions - Scale REPLAY_MAX_CALLS_PER_SESSION from 256 to 8,192 and max bytes from 2MB to 8MB to prevent LRU eviction of early function call signatures in deep conversations (positions 500+ / 1500). - Scale REPLAY_TTL_MS from 1h to 7d to match the Responses durable store TTL, ensuring resumed and multi-day sessions retain their cached thought signatures. - Update TTL regression tests to reflect the 7d expiration window. --- src/adapters/google-antigravity-replay.ts | 6 +++--- tests/google-antigravity-replay.test.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/adapters/google-antigravity-replay.ts b/src/adapters/google-antigravity-replay.ts index 3d574b54d9..59649838cb 100644 --- a/src/adapters/google-antigravity-replay.ts +++ b/src/adapters/google-antigravity-replay.ts @@ -34,11 +34,11 @@ interface ReplayEntry { } const MIN_SIGNATURE_LEN = 16; -const REPLAY_TTL_MS = 60 * 60 * 1000; // 1h +const REPLAY_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7d (matches Responses durable store TTL for long-running and resumed threads) 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/tests/google-antigravity-replay.test.ts b/tests/google-antigravity-replay.test.ts index 8fab813cca..51614441be 100644 --- a/tests/google-antigravity-replay.test.ts +++ b/tests/google-antigravity-replay.test.ts @@ -343,7 +343,7 @@ describe("antigravity reasoning-replay cache", () => { now = 1_002; applyAntigravityReplay(MODEL, SESSION, [{ role: "model", parts: [fcPart("one", {})] }]); expect(antigravityReplayRetainedStoreSnapshot().oldestAt).toBe(1_001); - now = 1_001 + 60 * 60 * 1000; + now = 1_001 + 7 * 24 * 60 * 60 * 1000; const expired = [{ role: "model", parts: [fcPart("one", {})] }]; applyAntigravityReplay(MODEL, SESSION, expired); expect((expired[0].parts[0] as { thoughtSignature?: string }).thoughtSignature).toBeUndefined(); @@ -522,7 +522,7 @@ describe("antigravity replay fixed-size key identities", () => { observeAntigravityReplay(MODEL, "s-2", [fcPart("f", {}, "sig-1234567890abcdef")]); expect(antigravityReplayMetrics().sessions).toBe(2); // The periodic sweeper still removes expired sessions on its own pass. - Date.now = () => originalNow() + 60 * 60 * 1000 + 1000; + Date.now = () => originalNow() + 7 * 24 * 60 * 60 * 1000 + 1000; const removed = sweepExpiredAntigravityReplay(Date.now()); expect(removed).toBe(2); expect(antigravityReplayMetrics().sessions).toBe(0); From e830be4568531878a86bf7906a23bb7628af4c7e Mon Sep 17 00:00:00 2001 From: agentHits Date: Sat, 22 Aug 2026 14:31:18 +0300 Subject: [PATCH 2/2] fix(google): scale antigravity replay capacity for deep sessions - Scale REPLAY_MAX_CALLS_PER_SESSION from 256 to 8,192 and per-session bytes from 2 MiB to 8 MiB, preventing LRU eviction of early function call signatures in deep conversations (500+ / 1500+ calls). - Retain unchanged 1h REPLAY_TTL_MS, 64 MiB global cap, and 24 MiB snapshot cap. - Add regression tests exercising default-limit 1,500-call sequence survival (including position 0 and 1499 across disk snapshot flush/reset/reload) and >2 MiB session retention. - Document the capacity scaling Decision Log in structure/04_transports-and-sidecars.md (addressing review from @Ingwannu on #2375). --- src/adapters/google-antigravity-replay.ts | 2 +- structure/04_transports-and-sidecars.md | 8 ++++ tests/google-antigravity-replay.test.ts | 48 ++++++++++++++++++++++- 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/src/adapters/google-antigravity-replay.ts b/src/adapters/google-antigravity-replay.ts index 59649838cb..3fd2d66d4e 100644 --- a/src/adapters/google-antigravity-replay.ts +++ b/src/adapters/google-antigravity-replay.ts @@ -34,7 +34,7 @@ interface ReplayEntry { } const MIN_SIGNATURE_LEN = 16; -const REPLAY_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7d (matches Responses durable store TTL for long-running and resumed threads) +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 = 8_192; 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 51614441be..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")]); @@ -343,7 +387,7 @@ describe("antigravity reasoning-replay cache", () => { now = 1_002; applyAntigravityReplay(MODEL, SESSION, [{ role: "model", parts: [fcPart("one", {})] }]); expect(antigravityReplayRetainedStoreSnapshot().oldestAt).toBe(1_001); - now = 1_001 + 7 * 24 * 60 * 60 * 1000; + now = 1_001 + 60 * 60 * 1000; const expired = [{ role: "model", parts: [fcPart("one", {})] }]; applyAntigravityReplay(MODEL, SESSION, expired); expect((expired[0].parts[0] as { thoughtSignature?: string }).thoughtSignature).toBeUndefined(); @@ -522,7 +566,7 @@ describe("antigravity replay fixed-size key identities", () => { observeAntigravityReplay(MODEL, "s-2", [fcPart("f", {}, "sig-1234567890abcdef")]); expect(antigravityReplayMetrics().sessions).toBe(2); // The periodic sweeper still removes expired sessions on its own pass. - Date.now = () => originalNow() + 7 * 24 * 60 * 60 * 1000 + 1000; + Date.now = () => originalNow() + 60 * 60 * 1000 + 1000; const removed = sweepExpiredAntigravityReplay(Date.now()); expect(removed).toBe(2); expect(antigravityReplayMetrics().sessions).toBe(0);