Skip to content

Commit 6a2ed87

Browse files
committed
fix: preserve preflight overflow token counts
1 parent 444a093 commit 6a2ed87

5 files changed

Lines changed: 96 additions & 6 deletions

File tree

src/agents/embedded-agent-runner/run.overflow-compaction.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2290,6 +2290,41 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => {
22902290
expect(result.meta.error).toBeUndefined();
22912291
});
22922292

2293+
it("passes preflight prompt estimates into synthetic overflow compaction", async () => {
2294+
mockedExtractObservedOverflowTokenCount.mockReturnValueOnce(undefined);
2295+
mockedRunEmbeddedAttempt
2296+
.mockResolvedValueOnce(
2297+
makeAttemptResult({
2298+
promptError: makeOverflowError(),
2299+
promptErrorSource: "precheck",
2300+
preflightRecovery: {
2301+
route: "compact_then_truncate",
2302+
estimatedPromptTokens: 268138,
2303+
promptBudgetBeforeReserve: 241616,
2304+
overflowTokens: 26522,
2305+
},
2306+
}),
2307+
)
2308+
.mockResolvedValueOnce(makeAttemptResult({ promptError: null }));
2309+
mockedCompactDirect.mockResolvedValueOnce(
2310+
makeCompactionSuccess({
2311+
summary: "Compacted session",
2312+
firstKeptEntryId: "entry-preflight",
2313+
tokensBefore: 268138,
2314+
}),
2315+
);
2316+
2317+
const result = await runEmbeddedAgent(overflowBaseRunParams);
2318+
2319+
expectMockCallFields(mockedCompactDirect, {
2320+
currentTokenCount: 268138,
2321+
});
2322+
expectRecordFields(expectMockCallFields(mockedCompactDirect, {}).runtimeContext, {
2323+
currentTokenCount: 268138,
2324+
});
2325+
expect(result.meta.error).toBeUndefined();
2326+
});
2327+
22932328
it("passes minimally over-budget count when overflow text is confirmed but unparseable", async () => {
22942329
mockedExtractObservedOverflowTokenCount.mockReturnValueOnce(undefined);
22952330
mockedRunEmbeddedAttempt

src/agents/embedded-agent-runner/run.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2636,8 +2636,15 @@ async function runEmbeddedAgentInternal(
26362636
const errorText = contextOverflowError.text;
26372637
const msgCount = attempt.messagesSnapshot?.length ?? 0;
26382638
const observedOverflowTokens = extractObservedOverflowTokenCount(errorText);
2639+
const preflightEstimatedPromptTokens =
2640+
typeof preflightRecovery?.estimatedPromptTokens === "number" &&
2641+
Number.isFinite(preflightRecovery.estimatedPromptTokens) &&
2642+
preflightRecovery.estimatedPromptTokens > 0
2643+
? Math.ceil(preflightRecovery.estimatedPromptTokens)
2644+
: undefined;
26392645
const overflowTokenCountForCompaction =
26402646
observedOverflowTokens ??
2647+
preflightEstimatedPromptTokens ??
26412648
(ctxInfo.tokens > 0
26422649
? // Confirmed overflow with an unparseable provider message still carries a
26432650
// minimally over-budget count for compaction engines and diagnostics.
@@ -2649,6 +2656,7 @@ async function runEmbeddedAgentInternal(
26492656
`messages=${msgCount} sessionFile=${activeSessionFile} ` +
26502657
`diagId=${overflowDiagId} compactionAttempts=${overflowCompactionAttempts} ` +
26512658
`observedTokens=${observedOverflowTokens ?? "unknown"} ` +
2659+
`preflightEstimatedTokens=${preflightEstimatedPromptTokens ?? "unknown"} ` +
26522660
`compactionTokens=${overflowTokenCountForCompaction ?? "unknown"} ` +
26532661
`error=${errorText.slice(0, 200)}`,
26542662
);

src/agents/embedded-agent-runner/run/attempt.spawn-workspace.context-engine.test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2951,7 +2951,13 @@ describe("runEmbeddedAttempt context engine mid-turn precheck integration", () =
29512951
});
29522952

29532953
expect(result.promptErrorSource).toBe("precheck");
2954-
expect(result.preflightRecovery).toEqual({ route: "compact_only", source: "mid-turn" });
2954+
expect(result.preflightRecovery).toEqual({
2955+
route: "compact_only",
2956+
source: "mid-turn",
2957+
estimatedPromptTokens: 9000,
2958+
promptBudgetBeforeReserve: 7000,
2959+
overflowTokens: 2000,
2960+
});
29552961
expect(result.messagesSnapshot).toEqual([seedMessage]);
29562962
});
29572963
});

src/agents/embedded-agent-runner/run/attempt.ts

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,22 @@ import {
510510
} from "./runtime-context-prompt.js";
511511
import type { EmbeddedRunAttemptParams, EmbeddedRunAttemptResult } from "./types.js";
512512

513+
type PreflightRecoveryBudgetSnapshot = Pick<
514+
MidTurnPrecheckRequest,
515+
"estimatedPromptTokens" | "promptBudgetBeforeReserve" | "overflowTokens"
516+
>;
517+
518+
// Carries the measured prompt budget into the outer recovery loop. The synthetic
519+
// precheck error is only a routing signal, so compaction engines need these
520+
// fields to compact against the prompt OpenClaw actually rendered.
521+
function buildPreflightRecoveryBudgetSnapshot(snapshot: PreflightRecoveryBudgetSnapshot) {
522+
return {
523+
estimatedPromptTokens: snapshot.estimatedPromptTokens,
524+
promptBudgetBeforeReserve: snapshot.promptBudgetBeforeReserve,
525+
overflowTokens: snapshot.overflowTokens,
526+
};
527+
}
528+
513529
export {
514530
appendAttemptCacheTtlIfNeeded,
515531
composeSystemPromptWithHookContext,
@@ -3878,6 +3894,7 @@ export async function runEmbeddedAttempt(
38783894
preflightRecovery = {
38793895
route: "truncate_tool_results_only",
38803896
source: "mid-turn",
3897+
...buildPreflightRecoveryBudgetSnapshot(request),
38813898
handled: true,
38823899
truncatedCount: truncationResult.truncatedCount,
38833900
};
@@ -3888,7 +3905,11 @@ export async function runEmbeddedAttempt(
38883905
`handled=true truncatedCount=${truncationResult.truncatedCount}`,
38893906
);
38903907
} else {
3891-
preflightRecovery = { route: "compact_only", source: "mid-turn" };
3908+
preflightRecovery = {
3909+
route: "compact_only",
3910+
source: "mid-turn",
3911+
...buildPreflightRecoveryBudgetSnapshot(request),
3912+
};
38923913
promptError = new Error(PREEMPTIVE_OVERFLOW_ERROR_TEXT);
38933914
promptErrorSource = "precheck";
38943915
logMidTurnPrecheck(
@@ -3897,7 +3918,11 @@ export async function runEmbeddedAttempt(
38973918
);
38983919
}
38993920
} else {
3900-
preflightRecovery = { route: request.route, source: "mid-turn" };
3921+
preflightRecovery = {
3922+
route: request.route,
3923+
source: "mid-turn",
3924+
...buildPreflightRecoveryBudgetSnapshot(request),
3925+
};
39013926
promptError = new Error(PREEMPTIVE_OVERFLOW_ERROR_TEXT);
39023927
promptErrorSource = "precheck";
39033928
logMidTurnPrecheck(request.route);
@@ -4686,6 +4711,7 @@ export async function runEmbeddedAttempt(
46864711
if (truncationResult.truncated) {
46874712
preflightRecovery = {
46884713
route: "truncate_tool_results_only",
4714+
...buildPreflightRecoveryBudgetSnapshot(preemptiveCompaction),
46894715
handled: true,
46904716
truncatedCount: truncationResult.truncatedCount,
46914717
};
@@ -4708,7 +4734,10 @@ export async function runEmbeddedAttempt(
47084734
`${params.provider}/${params.modelId}; falling back to compaction ` +
47094735
`reason=${truncationResult.reason ?? "unknown"} sessionFile=${params.sessionFile}`,
47104736
);
4711-
preflightRecovery = { route: "compact_only" };
4737+
preflightRecovery = {
4738+
route: "compact_only",
4739+
...buildPreflightRecoveryBudgetSnapshot(preemptiveCompaction),
4740+
};
47124741
promptError = new Error(PREEMPTIVE_OVERFLOW_ERROR_TEXT);
47134742
promptErrorSource = "precheck";
47144743
skipPromptSubmission = true;
@@ -4717,8 +4746,14 @@ export async function runEmbeddedAttempt(
47174746
if (preemptiveCompaction?.shouldCompact) {
47184747
preflightRecovery =
47194748
preemptiveCompaction.route === "compact_then_truncate"
4720-
? { route: "compact_then_truncate" }
4721-
: { route: "compact_only" };
4749+
? {
4750+
route: "compact_then_truncate",
4751+
...buildPreflightRecoveryBudgetSnapshot(preemptiveCompaction),
4752+
}
4753+
: {
4754+
route: "compact_only",
4755+
...buildPreflightRecoveryBudgetSnapshot(preemptiveCompaction),
4756+
};
47224757
promptError = new Error(PREEMPTIVE_OVERFLOW_ERROR_TEXT);
47234758
promptErrorSource = "precheck";
47244759
log.warn(

src/agents/embedded-agent-runner/run/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,12 +139,18 @@ export type EmbeddedRunAttemptResult = {
139139
| {
140140
route: Exclude<PreemptiveCompactionRoute, "fits">;
141141
source?: "mid-turn";
142+
estimatedPromptTokens?: number;
143+
promptBudgetBeforeReserve?: number;
144+
overflowTokens?: number;
142145
handled: true;
143146
truncatedCount?: number;
144147
}
145148
| {
146149
route: Exclude<PreemptiveCompactionRoute, "fits">;
147150
source?: "mid-turn";
151+
estimatedPromptTokens?: number;
152+
promptBudgetBeforeReserve?: number;
153+
overflowTokens?: number;
148154
handled?: false;
149155
};
150156
sessionIdUsed: string;

0 commit comments

Comments
 (0)