From d739f44cf970359278fc587d3bb6687b292946b9 Mon Sep 17 00:00:00 2001 From: FullerStackDev <263060202+fuller-stack-dev@users.noreply.github.com> Date: Thu, 28 May 2026 22:07:45 -0600 Subject: [PATCH] fix(codex): clear resumed context thread after terminal overflow --- .../run-attempt.context-engine.test.ts | 76 +++++++++++++++++++ .../codex/src/app-server/run-attempt.ts | 25 +++++- 2 files changed, 99 insertions(+), 2 deletions(-) diff --git a/extensions/codex/src/app-server/run-attempt.context-engine.test.ts b/extensions/codex/src/app-server/run-attempt.context-engine.test.ts index a5356dfb42b..53c27995fdb 100644 --- a/extensions/codex/src/app-server/run-attempt.context-engine.test.ts +++ b/extensions/codex/src/app-server/run-attempt.context-engine.test.ts @@ -1174,6 +1174,82 @@ describe("runCodexAppServerAttempt context-engine lifecycle", () => { expect(savedBinding?.contextEngine?.projection?.epoch).toBe("epoch-before"); }); + it("clears a resumed context-engine binding when a turn terminally overflows", async () => { + const sessionFile = path.join(tempDir, "session.jsonl"); + const workspaceDir = path.join(tempDir, "workspace"); + SessionManager.open(sessionFile).appendMessage( + assistantMessage("pre-compaction context", Date.now()) as never, + ); + await writeCodexAppServerBinding(sessionFile, { + threadId: "thread-old", + cwd: workspaceDir, + dynamicToolsFingerprint: "[]", + contextEngine: { + schemaVersion: 1, + engineId: "lossless-claw", + policyFingerprint: + '{"schemaVersion":1,"engineId":"lossless-claw","ownsCompaction":true,"contextTokenBudget":400000,"projectionMaxChars":1000000}', + projection: { + schemaVersion: 1, + mode: "thread_bootstrap", + epoch: "epoch-before", + }, + }, + }); + const compact = vi.fn(async () => ({ + ok: true, + compacted: true, + result: { summary: "summary", firstKeptEntryId: "entry-1", tokensBefore: 100_000 }, + })); + const assemble = vi.fn( + async ({ messages, prompt }: Parameters[0]) => ({ + messages: [...messages, userMessage(prompt ?? "", 11)], + estimatedTokens: 42, + systemPromptAddition: "context-engine system", + contextProjection: { mode: "thread_bootstrap" as const, epoch: "epoch-before" }, + }), + ); + const contextEngine = createContextEngine({ assemble, compact }); + const harness = createStartedThreadHarness(async (method) => { + if (method === "thread/resume") { + return threadStartResult("thread-old"); + } + if (method === "turn/start") { + return turnStartResult("turn-old"); + } + return undefined; + }); + const params = createParams(sessionFile, workspaceDir); + params.contextEngine = contextEngine; + params.contextTokenBudget = 400_000; + + const run = runCodexAppServerAttempt(params); + await harness.waitForMethod("turn/start"); + await harness.notify({ + method: "turn/completed", + params: { + threadId: "thread-old", + turnId: "turn-old", + turn: { + id: "turn-old", + status: "failed", + error: { message: "Codex ran out of room in the model's context window" }, + items: [], + }, + }, + }); + const result = await run; + + expect(result.promptError).toBe("Codex ran out of room in the model's context window"); + expect(compact).not.toHaveBeenCalled(); + expect(harness.requests.map((request) => request.method)).toEqual([ + "thread/resume", + "turn/start", + "thread/unsubscribe", + ]); + expect(await readCodexAppServerBinding(sessionFile)).toBeUndefined(); + }); + it("does not pre-compact over-budget rendered context-engine prompts before Codex turn/start", async () => { const sessionFile = path.join(tempDir, "session.jsonl"); const workspaceDir = path.join(tempDir, "workspace"); diff --git a/extensions/codex/src/app-server/run-attempt.ts b/extensions/codex/src/app-server/run-attempt.ts index 66a5c3e9e12..67770e407cb 100644 --- a/extensions/codex/src/app-server/run-attempt.ts +++ b/extensions/codex/src/app-server/run-attempt.ts @@ -1653,7 +1653,7 @@ export async function runCodexAppServerAttempt( } catch (error) { let turnStartError = error; if ( - shouldRetryContextEngineTurnOnFreshCodexThread({ + shouldUseFreshCodexThreadAfterContextEngineOverflow({ error: turnStartError, contextEngineActive: Boolean(activeContextEngine), thread, @@ -1964,6 +1964,27 @@ export async function runCodexAppServerAttempt( error: finalPromptErrorMessage, }); } + if ( + shouldUseFreshCodexThreadAfterContextEngineOverflow({ + error: finalPromptError, + contextEngineActive: Boolean(activeContextEngine), + thread, + }) + ) { + embeddedAgentLog.warn( + "codex app-server context-engine turn overflowed after resume; clearing thread binding for recovery", + { + threadId: thread.threadId, + turnId: activeTurnId, + error: finalPromptErrorMessage, + }, + ); + const preClearSessionFile = activeSessionFile; + await clearCodexAppServerBinding(preClearSessionFile); + if (activeSessionFile !== preClearSessionFile) { + await clearCodexAppServerBinding(activeSessionFile); + } + } const refreshedUsageLimitPromptError = await refreshCodexUsageLimitPromptError({ client, message: finalPromptErrorMessage, @@ -2259,7 +2280,7 @@ function isUnscopedCodexNotification( ); } -function shouldRetryContextEngineTurnOnFreshCodexThread(params: { +function shouldUseFreshCodexThreadAfterContextEngineOverflow(params: { error: unknown; contextEngineActive: boolean; thread: CodexAppServerThreadLifecycleBinding;