From f240cf7820d12c892539de47c7eefcab6daf7dfa Mon Sep 17 00:00:00 2001 From: Eyejoker Date: Tue, 19 May 2026 21:52:54 +0900 Subject: [PATCH] [codex] Recover Codex context overflow sessions --- src/message-agent-executor-lifecycle.ts | 47 +++++++++++++------------ src/message-agent-executor.test.ts | 19 +++++----- src/session-recovery.test.ts | 11 ++++++ src/session-recovery.ts | 2 ++ 4 files changed, 46 insertions(+), 33 deletions(-) diff --git a/src/message-agent-executor-lifecycle.ts b/src/message-agent-executor-lifecycle.ts index 95c63b5..5aeff75 100644 --- a/src/message-agent-executor-lifecycle.ts +++ b/src/message-agent-executor-lifecycle.ts @@ -21,6 +21,23 @@ import { getErrorMessage } from './utils.js'; type AttemptResult = 'success' | 'error'; +function isRetryableCodexSessionFailureAttempt(args: { + provider: 'claude' | 'codex'; + attempt: MessageAgentAttempt; +}): boolean { + const { provider, attempt } = args; + if (provider !== 'codex' || attempt.sawOutput) return false; + if (attempt.retryableSessionFailureDetected === true) return true; + if (attempt.output != null) + return shouldRetryFreshCodexSessionOnAgentFailure(attempt.output); + return attempt.error == null + ? false + : shouldRetryFreshCodexSessionOnAgentFailure({ + result: null, + error: getErrorMessage(attempt.error), + }); +} + export async function executeMessageAgentAttemptLifecycle(args: { provider: 'claude' | 'codex'; runAttempt: (provider: 'claude' | 'codex') => Promise; @@ -212,34 +229,13 @@ export async function executeMessageAgentAttemptLifecycle(args: { }; }; - const isRetryableCodexSessionFailure = ( - attempt: MessageAgentAttempt, - ): boolean => { - if (provider !== 'codex' || attempt.sawOutput) { - return false; - } - - if (attempt.retryableSessionFailureDetected === true) { - return true; - } - - if (attempt.error == null) { - return false; - } - - return shouldRetryFreshCodexSessionOnAgentFailure({ - result: null, - error: getErrorMessage(attempt.error), - }); - }; - const recoverRetryableCodexSessionFailure = async ( attempt: MessageAgentAttempt, ): Promise<{ attempt: MessageAgentAttempt; resolved: AttemptResult | null; }> => { - if (!isRetryableCodexSessionFailure(attempt)) { + if (!isRetryableCodexSessionFailureAttempt({ provider, attempt })) { return { attempt, resolved: null }; } @@ -250,7 +246,12 @@ export async function executeMessageAgentAttemptLifecycle(args: { ); const freshAttempt = await runTrackedAttempt('codex'); - if (!isRetryableCodexSessionFailure(freshAttempt)) { + if ( + !isRetryableCodexSessionFailureAttempt({ + provider, + attempt: freshAttempt, + }) + ) { return { attempt: freshAttempt, resolved: null }; } diff --git a/src/message-agent-executor.test.ts b/src/message-agent-executor.test.ts index 7b2bf63..d1eef0c 100644 --- a/src/message-agent-executor.test.ts +++ b/src/message-agent-executor.test.ts @@ -3461,7 +3461,7 @@ describe('runAgentForGroup Claude rotation', () => { expect(deps.clearSession).toHaveBeenCalledWith('test-claude'); }); - it('drops a poisoned Codex session id before retrying a fresh session after remote compaction failure', async () => { + it('drops a poisoned Codex session id before retrying after context-window overflow', async () => { const group = { ...makeGroup(), folder: 'test-codex', @@ -3494,20 +3494,19 @@ describe('runAgentForGroup Claude rotation', () => { vi.mocked(agentRunner.runAgentProcess) .mockImplementationOnce(async (_group, input) => { expect(input.sessionId).toBe('stale-codex-session-id'); - throw new Error( - "Error running remote compact task: Unknown parameter: 'prompt_cache_retention'", - ); + return { + status: 'error', + result: null, + error: "Codex ran out of room in the model's context window.", + newSessionId: 'stale-codex-session-id', + }; }) .mockImplementationOnce(async (_group, input, _onProcess, onOutput) => { expect(input.sessionId).toBeUndefined(); - await onOutput?.({ - status: 'success', - phase: 'final', - result: 'fresh Codex retry success', - }); + await onOutput?.({ status: 'success', phase: 'final', result: 'ok' }); return { status: 'success', - result: 'fresh Codex retry success', + result: 'ok', newSessionId: 'fresh-codex-session-id', }; }); diff --git a/src/session-recovery.test.ts b/src/session-recovery.test.ts index 453af1d..24e9fa8 100644 --- a/src/session-recovery.test.ts +++ b/src/session-recovery.test.ts @@ -135,6 +135,17 @@ describe('shouldRetryFreshCodexSessionOnAgentFailure', () => { ).toBe(true); }); + it('retries Codex context-window overflow with a fresh session', () => { + const output = { + result: null, + error: + "Codex ran out of room in the model's context window.\nStart a new thread or clear earlier history before retrying.", + }; + + expect(shouldResetCodexSessionOnAgentFailure(output)).toBe(true); + expect(shouldRetryFreshCodexSessionOnAgentFailure(output)).toBe(true); + }); + it('does not retry generic Codex Bad Request signals during observation-only rollout', () => { const output = { result: null, diff --git a/src/session-recovery.ts b/src/session-recovery.ts index b5a6486..72e6abb 100644 --- a/src/session-recovery.ts +++ b/src/session-recovery.ts @@ -20,6 +20,8 @@ const SESSION_RETRY_PATTERNS = [ const CODEX_SESSION_RESET_PATTERNS = [ /Error running remote compact task/i, + /Codex ran out of room in the model's context window/i, + /Start a new thread or clear earlier history before retrying/i, /prompt_cache_retention/i, ];