[codex] Recover Codex context overflow sessions
This commit is contained in:
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user