Skip to content

Commit 52ea7e6

Browse files
fix: recover codex context overflow prompt errors
1 parent c127334 commit 52ea7e6

5 files changed

Lines changed: 60 additions & 4 deletions

File tree

src/agents/pi-embedded-helpers/errors.test.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { AssistantMessage } from "@earendil-works/pi-ai";
22
import { describe, expect, it } from "vitest";
33
import { MALFORMED_STREAMING_FRAGMENT_ERROR_MESSAGE } from "../../shared/assistant-error-format.js";
44
import { makeAssistantMessageFixture } from "../test-helpers/assistant-message-fixtures.js";
5-
import { formatAssistantErrorText } from "./errors.js";
5+
import { formatAssistantErrorText, isLikelyContextOverflowError } from "./errors.js";
66

77
describe("formatAssistantErrorText streaming JSON parse classification", () => {
88
const makeAssistantError = (errorMessage: string): AssistantMessage =>
@@ -36,3 +36,13 @@ describe("formatAssistantErrorText streaming JSON parse classification", () => {
3636
);
3737
});
3838
});
39+
40+
describe("isLikelyContextOverflowError", () => {
41+
it("detects Codex promptError wording for a full context window", () => {
42+
expect(
43+
isLikelyContextOverflowError(
44+
"Codex ran out of room in the model's context window. Start a new session or reset the conversation.",
45+
),
46+
).toBe(true);
47+
});
48+
});

src/agents/pi-embedded-helpers/errors.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ export function isContextOverflowError(errorMessage?: string): boolean {
107107
lower.includes("context window") ||
108108
lower.includes("context length") ||
109109
lower.includes("maximum context length");
110+
const hasContextWindowOutOfRoom =
111+
hasContextWindow && (lower.includes("ran out of room") || lower.includes("ran out of space"));
110112
return (
111113
lower.includes("request_too_large") ||
112114
(lower.includes("invalid_argument") && lower.includes("maximum number of tokens")) ||
@@ -118,6 +120,7 @@ export function isContextOverflowError(errorMessage?: string): boolean {
118120
lower.includes("exceeds model context window") ||
119121
lower.includes("model token limit") ||
120122
(lower.includes("input exceeds") && lower.includes("maximum number of tokens")) ||
123+
hasContextWindowOutOfRoom ||
121124
(hasRequestSizeExceeds && hasContextWindow) ||
122125
lower.includes("context overflow:") ||
123126
lower.includes("exceed context limit") ||

src/agents/pi-embedded-runner/run.overflow-compaction.harness.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ export const mockedIsLikelyContextOverflowError = vi.fn((msg?: string) => {
182182
return (
183183
lower.includes("request_too_large") ||
184184
lower.includes("context window exceeded") ||
185+
(lower.includes("context window") && lower.includes("ran out of room")) ||
185186
lower.includes("prompt is too long")
186187
);
187188
});
@@ -360,6 +361,7 @@ export function resetRunOverflowCompactionHarnessMocks(): void {
360361
return (
361362
lower.includes("request_too_large") ||
362363
lower.includes("context window exceeded") ||
364+
(lower.includes("context window") && lower.includes("ran out of room")) ||
363365
lower.includes("prompt is too long")
364366
);
365367
});

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
mockedEnsureAuthProfileStoreWithoutExternalProfiles,
2323
mockedGlobalHookRunner,
2424
mockedGetApiKeyForModel,
25+
mockedIsLikelyContextOverflowError,
2526
mockedMarkAuthProfileSuccess,
2627
mockedPickFallbackThinkingLevel,
2728
mockedResolveAuthProfileOrder,
@@ -1511,6 +1512,39 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => {
15111512
expect(result.meta.error).toBeUndefined();
15121513
});
15131514

1515+
it("surfaces a visible blocked payload for Codex promptError overflow without assistant text", async () => {
1516+
const promptError = new Error(
1517+
"Codex ran out of room in the model's context window. Start a new session or reset the conversation.",
1518+
);
1519+
const terminalLifecycleMeta: Array<Record<string, unknown>> = [];
1520+
mockedRunEmbeddedAttempt.mockResolvedValueOnce(
1521+
makeAttemptResult({
1522+
promptError,
1523+
promptErrorSource: "prompt",
1524+
assistantTexts: [],
1525+
attemptUsage: { input: 0, output: 0, total: 0 },
1526+
setTerminalLifecycleMeta: (meta) => {
1527+
terminalLifecycleMeta.push(meta);
1528+
},
1529+
}),
1530+
);
1531+
1532+
const result = await runEmbeddedPiAgent(overflowBaseRunParams);
1533+
1534+
expect(mockedIsLikelyContextOverflowError).toHaveBeenCalledWith(promptError.message);
1535+
expect(mockedCompactDirect).toHaveBeenCalledTimes(1);
1536+
expect(result.payloads?.[0]).toMatchObject({
1537+
isError: true,
1538+
text: expect.stringContaining("Context overflow"),
1539+
});
1540+
expect(result.payloads?.[0]?.text).toContain("/reset");
1541+
expect(result.payloads?.[0]?.text).toContain("/new");
1542+
expect(result.meta.error?.kind).toBe("context_overflow");
1543+
expect(result.meta.livenessState).toBe("blocked");
1544+
expect(result.meta.finalAssistantVisibleText).toBe(result.payloads?.[0]?.text);
1545+
expect(terminalLifecycleMeta.at(-1)).toMatchObject({ livenessState: "blocked" });
1546+
});
1547+
15141548
it("does not reset compaction attempt budget after successful tool-result truncation", async () => {
15151549
const overflowError = queueOverflowAttemptWithOversizedToolOutput(
15161550
mockedRunEmbeddedAttempt,

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2123,16 +2123,21 @@ export async function runEmbeddedPiAgent(
21232123
);
21242124
}
21252125
const kind = isCompactionFailure ? "compaction_failure" : "context_overflow";
2126+
const overflowRecoveryText =
2127+
"Context overflow: prompt too large for the model. " +
2128+
"Try /reset (or /new) to start a fresh session, or use a larger-context model.";
2129+
log.warn(
2130+
`[context-overflow-recovery] exhausted provider overflow recovery for ${provider}/${modelId}; ` +
2131+
`livenessState=blocked suggestedAction=reset_or_new kind=${kind}`,
2132+
);
21262133
attempt.setTerminalLifecycleMeta?.({
21272134
replayInvalid: resolveReplayInvalidForAttempt(),
21282135
livenessState: "blocked",
21292136
});
21302137
return {
21312138
payloads: [
21322139
{
2133-
text:
2134-
"Context overflow: prompt too large for the model. " +
2135-
"Try /reset (or /new) to start a fresh session, or use a larger-context model.",
2140+
text: overflowRecoveryText,
21362141
isError: true,
21372142
},
21382143
],
@@ -2150,6 +2155,8 @@ export async function runEmbeddedPiAgent(
21502155
lastTurnTotal,
21512156
}),
21522157
systemPromptReport: attempt.systemPromptReport,
2158+
finalAssistantVisibleText: overflowRecoveryText,
2159+
finalAssistantRawText: overflowRecoveryText,
21532160
finalPromptText: attempt.finalPromptText,
21542161
replayInvalid: resolveReplayInvalidForAttempt(),
21552162
livenessState: "blocked",

0 commit comments

Comments
 (0)