From 503d39578066a556469f1566990283d40c9941b2 Mon Sep 17 00:00:00 2001 From: hcl Date: Tue, 3 Mar 2026 09:00:18 +0800 Subject: [PATCH] fix(memoryFlush): guard transcript-size forced flush against repeated runs (#32358) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `forceFlushTranscriptBytes` path (introduced in d729ab21) bypasses the `memoryFlushCompactionCount` guard that prevents repeated flushes within the same compaction cycle. Once the session transcript exceeds 2 MB, memory flush fires on every single message — even when token count is well under the compaction threshold. Extract `hasAlreadyFlushedForCurrentCompaction()` from the inline guard in `shouldRunMemoryFlush` and apply it to both the token-based and the transcript-size trigger paths. Fixes #32317 Signed-off-by: HCL --- src/auto-reply/reply/agent-runner-memory.ts | 5 ++- src/auto-reply/reply/memory-flush.ts | 17 ++++++++-- src/auto-reply/reply/reply-state.test.ts | 37 +++++++++++++++++++++ 3 files changed, 55 insertions(+), 4 deletions(-) diff --git a/src/auto-reply/reply/agent-runner-memory.ts b/src/auto-reply/reply/agent-runner-memory.ts index 4bbfc3fe012..985a5e2ee6c 100644 --- a/src/auto-reply/reply/agent-runner-memory.ts +++ b/src/auto-reply/reply/agent-runner-memory.ts @@ -31,6 +31,7 @@ import { resolveModelFallbackOptions, } from "./agent-runner-utils.js"; import { + hasAlreadyFlushedForCurrentCompaction, resolveMemoryFlushContextWindowTokens, resolveMemoryFlushPromptForRun, resolveMemoryFlushSettings, @@ -437,7 +438,9 @@ export async function runMemoryFlushIfNeeded(params: { reserveTokensFloor: memoryFlushSettings.reserveTokensFloor, softThresholdTokens: memoryFlushSettings.softThresholdTokens, })) || - shouldForceFlushByTranscriptSize; + (shouldForceFlushByTranscriptSize && + entry != null && + !hasAlreadyFlushedForCurrentCompaction(entry)); if (!shouldFlushMemory) { return entry ?? params.sessionEntry; diff --git a/src/auto-reply/reply/memory-flush.ts b/src/auto-reply/reply/memory-flush.ts index 4c8116fa03f..e23703c7b6c 100644 --- a/src/auto-reply/reply/memory-flush.ts +++ b/src/auto-reply/reply/memory-flush.ts @@ -161,11 +161,22 @@ export function shouldRunMemoryFlush(params: { return false; } - const compactionCount = params.entry.compactionCount ?? 0; - const lastFlushAt = params.entry.memoryFlushCompactionCount; - if (typeof lastFlushAt === "number" && lastFlushAt === compactionCount) { + if (hasAlreadyFlushedForCurrentCompaction(params.entry)) { return false; } return true; } + +/** + * Returns true when a memory flush has already been performed for the current + * compaction cycle. This prevents repeated flush runs within the same cycle — + * important for both the token-based and transcript-size–based trigger paths. + */ +export function hasAlreadyFlushedForCurrentCompaction( + entry: Pick, +): boolean { + const compactionCount = entry.compactionCount ?? 0; + const lastFlushAt = entry.memoryFlushCompactionCount; + return typeof lastFlushAt === "number" && lastFlushAt === compactionCount; +} diff --git a/src/auto-reply/reply/reply-state.test.ts b/src/auto-reply/reply/reply-state.test.ts index 0c619c13252..56623fe6cfa 100644 --- a/src/auto-reply/reply/reply-state.test.ts +++ b/src/auto-reply/reply/reply-state.test.ts @@ -17,6 +17,7 @@ import { import { DEFAULT_MEMORY_FLUSH_FORCE_TRANSCRIPT_BYTES, DEFAULT_MEMORY_FLUSH_SOFT_TOKENS, + hasAlreadyFlushedForCurrentCompaction, resolveMemoryFlushContextWindowTokens, resolveMemoryFlushSettings, shouldRunMemoryFlush, @@ -350,6 +351,42 @@ describe("shouldRunMemoryFlush", () => { }); }); +describe("hasAlreadyFlushedForCurrentCompaction", () => { + it("returns true when memoryFlushCompactionCount matches compactionCount", () => { + expect( + hasAlreadyFlushedForCurrentCompaction({ + compactionCount: 3, + memoryFlushCompactionCount: 3, + }), + ).toBe(true); + }); + + it("returns false when memoryFlushCompactionCount differs", () => { + expect( + hasAlreadyFlushedForCurrentCompaction({ + compactionCount: 3, + memoryFlushCompactionCount: 2, + }), + ).toBe(false); + }); + + it("returns false when memoryFlushCompactionCount is undefined", () => { + expect( + hasAlreadyFlushedForCurrentCompaction({ + compactionCount: 1, + }), + ).toBe(false); + }); + + it("treats missing compactionCount as 0", () => { + expect( + hasAlreadyFlushedForCurrentCompaction({ + memoryFlushCompactionCount: 0, + }), + ).toBe(true); + }); +}); + describe("resolveMemoryFlushContextWindowTokens", () => { it("falls back to agent config or default tokens", () => { expect(resolveMemoryFlushContextWindowTokens({ agentCfgContextTokens: 42_000 })).toBe(42_000);