[codex] Recover Codex context overflow sessions

This commit is contained in:
Eyejoker
2026-05-19 21:52:54 +09:00
committed by GitHub
parent 59c7318833
commit f240cf7820
4 changed files with 46 additions and 33 deletions

View File

@@ -21,6 +21,23 @@ import { getErrorMessage } from './utils.js';
type AttemptResult = 'success' | 'error'; 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: { export async function executeMessageAgentAttemptLifecycle(args: {
provider: 'claude' | 'codex'; provider: 'claude' | 'codex';
runAttempt: (provider: 'claude' | 'codex') => Promise<MessageAgentAttempt>; runAttempt: (provider: 'claude' | 'codex') => Promise<MessageAgentAttempt>;
@@ -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 ( const recoverRetryableCodexSessionFailure = async (
attempt: MessageAgentAttempt, attempt: MessageAgentAttempt,
): Promise<{ ): Promise<{
attempt: MessageAgentAttempt; attempt: MessageAgentAttempt;
resolved: AttemptResult | null; resolved: AttemptResult | null;
}> => { }> => {
if (!isRetryableCodexSessionFailure(attempt)) { if (!isRetryableCodexSessionFailureAttempt({ provider, attempt })) {
return { attempt, resolved: null }; return { attempt, resolved: null };
} }
@@ -250,7 +246,12 @@ export async function executeMessageAgentAttemptLifecycle(args: {
); );
const freshAttempt = await runTrackedAttempt('codex'); const freshAttempt = await runTrackedAttempt('codex');
if (!isRetryableCodexSessionFailure(freshAttempt)) { if (
!isRetryableCodexSessionFailureAttempt({
provider,
attempt: freshAttempt,
})
) {
return { attempt: freshAttempt, resolved: null }; return { attempt: freshAttempt, resolved: null };
} }

View File

@@ -3461,7 +3461,7 @@ describe('runAgentForGroup Claude rotation', () => {
expect(deps.clearSession).toHaveBeenCalledWith('test-claude'); 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 = { const group = {
...makeGroup(), ...makeGroup(),
folder: 'test-codex', folder: 'test-codex',
@@ -3494,20 +3494,19 @@ describe('runAgentForGroup Claude rotation', () => {
vi.mocked(agentRunner.runAgentProcess) vi.mocked(agentRunner.runAgentProcess)
.mockImplementationOnce(async (_group, input) => { .mockImplementationOnce(async (_group, input) => {
expect(input.sessionId).toBe('stale-codex-session-id'); expect(input.sessionId).toBe('stale-codex-session-id');
throw new Error( return {
"Error running remote compact task: Unknown parameter: 'prompt_cache_retention'", 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) => { .mockImplementationOnce(async (_group, input, _onProcess, onOutput) => {
expect(input.sessionId).toBeUndefined(); expect(input.sessionId).toBeUndefined();
await onOutput?.({ await onOutput?.({ status: 'success', phase: 'final', result: 'ok' });
status: 'success',
phase: 'final',
result: 'fresh Codex retry success',
});
return { return {
status: 'success', status: 'success',
result: 'fresh Codex retry success', result: 'ok',
newSessionId: 'fresh-codex-session-id', newSessionId: 'fresh-codex-session-id',
}; };
}); });

View File

@@ -135,6 +135,17 @@ describe('shouldRetryFreshCodexSessionOnAgentFailure', () => {
).toBe(true); ).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', () => { it('does not retry generic Codex Bad Request signals during observation-only rollout', () => {
const output = { const output = {
result: null, result: null,

View File

@@ -20,6 +20,8 @@ const SESSION_RETRY_PATTERNS = [
const CODEX_SESSION_RESET_PATTERNS = [ const CODEX_SESSION_RESET_PATTERNS = [
/Error running remote compact task/i, /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, /prompt_cache_retention/i,
]; ];