fix(reply): project preflight compaction gate by next-input on fresh tokens (#91488)

The fresh-tokens path of runPreflightCompactionIfNeeded fed the prompt-only
entry.totalTokens snapshot straight into the budget threshold check, dropping
the current user prompt estimate and the previous turn's output. The sibling
memory-flush gate and this function's own stale branch already project
base + output + estimate via resolveEffectivePromptTokens, so the preflight
gate under-triggered and let over-budget requests through to overflow-retry.

Project the fresh persisted base the same way: read transcript output when near
the threshold (mirroring the memory-flush gate's buffer) and run the fresh base
through resolveEffectivePromptTokens before the threshold check.
This commit is contained in:
Yuval Dinodia
2026-06-15 13:10:42 -04:00
committed by GitHub
parent 8e55348ff9
commit caab343461
2 changed files with 98 additions and 11 deletions
@@ -1361,6 +1361,79 @@ describe("runMemoryFlushIfNeeded", () => {
expect(compactCall.authProfileId).toBe("anthropic:claude@martian.engineering");
expect(compactCall.contextTokenBudget).toBe(258_000);
});
it("preflight compacts a fresh session when the current prompt estimate pushes the next request over budget", async () => {
registerMemoryFlushPlanResolverForTest(() => ({
softThresholdTokens: 0,
forceFlushTranscriptBytes: 1_000_000_000,
reserveTokensFloor: 10,
prompt: "Pre-compaction memory flush.\nNO_REPLY",
systemPrompt: "Write memory to memory/YYYY-MM-DD.md.",
relativePath: "memory/2023-11-14.md",
}));
const sessionEntry: SessionEntry = {
sessionId: "session",
updatedAt: Date.now(),
totalTokens: 985,
totalTokensFresh: true,
compactionCount: 0,
};
await runPreflightCompactionIfNeeded({
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
followupRun: createTestFollowupRun({
provider: "anthropic",
model: "claude",
sessionKey: "agent:main:main",
}),
promptForEstimate: "Please summarize the entire design discussion above. ".repeat(8),
defaultModel: "anthropic/claude",
agentCfgContextTokens: 1000,
sessionEntry,
sessionStore: { "agent:main:main": sessionEntry },
sessionKey: "agent:main:main",
isHeartbeat: false,
replyOperation: createReplyOperation(),
});
expect(compactEmbeddedAgentSessionMock).toHaveBeenCalledTimes(1);
});
it("does not preflight compact a fresh session when only accumulated output tokens are large and the latest output keeps the request under budget", async () => {
registerMemoryFlushPlanResolverForTest(() => ({
softThresholdTokens: 0,
forceFlushTranscriptBytes: 1_000_000_000,
reserveTokensFloor: 10,
prompt: "Pre-compaction memory flush.\nNO_REPLY",
systemPrompt: "Write memory to memory/YYYY-MM-DD.md.",
relativePath: "memory/2023-11-14.md",
}));
const sessionEntry: SessionEntry = {
sessionId: "session",
updatedAt: Date.now(),
totalTokens: 985,
outputTokens: 50_000,
totalTokensFresh: true,
compactionCount: 0,
};
await runPreflightCompactionIfNeeded({
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
followupRun: createTestFollowupRun({
provider: "anthropic",
model: "claude",
sessionKey: "agent:main:main",
}),
promptForEstimate: "",
defaultModel: "anthropic/claude",
agentCfgContextTokens: 1000,
sessionEntry,
sessionStore: { "agent:main:main": sessionEntry },
sessionKey: "agent:main:main",
isHeartbeat: false,
replyOperation: createReplyOperation(),
});
expect(compactEmbeddedAgentSessionMock).not.toHaveBeenCalled();
});
it("updates the active preflight run after transcript rotation", async () => {
const sessionFile = path.join(rootDir, "session.jsonl");
const successorFile = path.join(rootDir, "session-rotated.jsonl");
@@ -2113,7 +2186,7 @@ describe("runMemoryFlushIfNeeded", () => {
const compactCall = requireCompactEmbeddedAgentSessionCall();
expect(compactCall.sessionId).toBe("session");
expect(compactCall.trigger).toBe("budget");
expect(compactCall.currentTokenCount).toBe(10);
expect(compactCall.currentTokenCount).toBe(12);
expect(compactCall.sessionFile).toContain("large-session.jsonl");
});
+24 -10
View File
@@ -779,10 +779,24 @@ export async function runPreflightCompactionIfNeeded(params: {
const promptTokenEstimate = estimatePromptTokensForMemoryFlush(
params.promptForEstimate ?? params.followupRun.prompt,
);
const serverCompactionThreshold = resolveResponsesServerCompactionThreshold({
cfg: params.cfg,
provider: params.followupRun.run.provider,
modelId: params.followupRun.run.model ?? params.defaultModel,
});
const threshold = Math.max(
contextWindowTokens - reserveTokensFloor - softThresholdTokens,
serverCompactionThreshold ?? 0,
);
const freshNeedsOutputRead =
typeof freshPersistedTokens === "number" &&
typeof promptTokenEstimate === "number" &&
threshold > 0 &&
freshPersistedTokens + promptTokenEstimate >= threshold - TRANSCRIPT_OUTPUT_READ_BUFFER_TOKENS;
const maxActiveTranscriptBytes = resolveMaxActiveTranscriptBytes(params.cfg);
const shouldCheckActiveTranscriptBytes = typeof maxActiveTranscriptBytes === "number";
const transcriptUsageTokens =
typeof freshPersistedTokens === "number"
typeof freshPersistedTokens === "number" && !freshNeedsOutputRead
? undefined
: await estimatePromptTokensFromSessionTranscript({
sessionId: entry.sessionId,
@@ -824,8 +838,17 @@ export async function runPreflightCompactionIfNeeded(params: {
promptTokenEstimate,
)
: undefined;
const freshProjectedTokenCount =
typeof freshPersistedTokens === "number"
? resolveEffectivePromptTokens(
freshPersistedTokens,
transcriptOutputTokens,
promptTokenEstimate,
)
: undefined;
const projectedTokenCount = Math.max(
usageProjectedTokenCount ?? 0,
freshProjectedTokenCount ?? 0,
stalePersistedPromptTokens ?? 0,
);
const tokenCountForCompaction =
@@ -833,15 +856,6 @@ export async function runPreflightCompactionIfNeeded(params: {
? projectedTokenCount
: undefined;
const serverCompactionThreshold = resolveResponsesServerCompactionThreshold({
cfg: params.cfg,
provider: params.followupRun.run.provider,
modelId: params.followupRun.run.model ?? params.defaultModel,
});
const threshold = Math.max(
contextWindowTokens - reserveTokensFloor - softThresholdTokens,
serverCompactionThreshold ?? 0,
);
logVerbose(
`preflightCompaction check: sessionKey=${params.sessionKey} ` +
`tokenCount=${tokenCountForCompaction ?? freshPersistedTokens ?? "undefined"} ` +