fix(memoryFlush): guard transcript-size forced flush against repeated runs (#32358)

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 <chenglunhu@gmail.com>
This commit is contained in:
hcl
2026-03-03 09:00:18 +08:00
committed by GitHub
parent 924d9e34ef
commit 503d395780
3 changed files with 55 additions and 4 deletions

View File

@@ -31,6 +31,7 @@ import {
resolveModelFallbackOptions, resolveModelFallbackOptions,
} from "./agent-runner-utils.js"; } from "./agent-runner-utils.js";
import { import {
hasAlreadyFlushedForCurrentCompaction,
resolveMemoryFlushContextWindowTokens, resolveMemoryFlushContextWindowTokens,
resolveMemoryFlushPromptForRun, resolveMemoryFlushPromptForRun,
resolveMemoryFlushSettings, resolveMemoryFlushSettings,
@@ -437,7 +438,9 @@ export async function runMemoryFlushIfNeeded(params: {
reserveTokensFloor: memoryFlushSettings.reserveTokensFloor, reserveTokensFloor: memoryFlushSettings.reserveTokensFloor,
softThresholdTokens: memoryFlushSettings.softThresholdTokens, softThresholdTokens: memoryFlushSettings.softThresholdTokens,
})) || })) ||
shouldForceFlushByTranscriptSize; (shouldForceFlushByTranscriptSize &&
entry != null &&
!hasAlreadyFlushedForCurrentCompaction(entry));
if (!shouldFlushMemory) { if (!shouldFlushMemory) {
return entry ?? params.sessionEntry; return entry ?? params.sessionEntry;

View File

@@ -161,11 +161,22 @@ export function shouldRunMemoryFlush(params: {
return false; return false;
} }
const compactionCount = params.entry.compactionCount ?? 0; if (hasAlreadyFlushedForCurrentCompaction(params.entry)) {
const lastFlushAt = params.entry.memoryFlushCompactionCount;
if (typeof lastFlushAt === "number" && lastFlushAt === compactionCount) {
return false; return false;
} }
return true; 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-sizebased trigger paths.
*/
export function hasAlreadyFlushedForCurrentCompaction(
entry: Pick<SessionEntry, "compactionCount" | "memoryFlushCompactionCount">,
): boolean {
const compactionCount = entry.compactionCount ?? 0;
const lastFlushAt = entry.memoryFlushCompactionCount;
return typeof lastFlushAt === "number" && lastFlushAt === compactionCount;
}

View File

@@ -17,6 +17,7 @@ import {
import { import {
DEFAULT_MEMORY_FLUSH_FORCE_TRANSCRIPT_BYTES, DEFAULT_MEMORY_FLUSH_FORCE_TRANSCRIPT_BYTES,
DEFAULT_MEMORY_FLUSH_SOFT_TOKENS, DEFAULT_MEMORY_FLUSH_SOFT_TOKENS,
hasAlreadyFlushedForCurrentCompaction,
resolveMemoryFlushContextWindowTokens, resolveMemoryFlushContextWindowTokens,
resolveMemoryFlushSettings, resolveMemoryFlushSettings,
shouldRunMemoryFlush, 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", () => { describe("resolveMemoryFlushContextWindowTokens", () => {
it("falls back to agent config or default tokens", () => { it("falls back to agent config or default tokens", () => {
expect(resolveMemoryFlushContextWindowTokens({ agentCfgContextTokens: 42_000 })).toBe(42_000); expect(resolveMemoryFlushContextWindowTokens({ agentCfgContextTokens: 42_000 })).toBe(42_000);