Unify paired status transitions and retry handling
This commit is contained in:
164
src/agent-attempt-retry.ts
Normal file
164
src/agent-attempt-retry.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import {
|
||||
classifyRotationTrigger,
|
||||
type AgentTriggerReason,
|
||||
type CodexRotationReason,
|
||||
isCodexRotationReason,
|
||||
} from './agent-error-detection.js';
|
||||
import { detectCodexRotationTrigger } from './codex-token-rotation.js';
|
||||
import { getErrorMessage } from './utils.js';
|
||||
|
||||
export interface AttemptStreamedTrigger {
|
||||
reason: AgentTriggerReason;
|
||||
retryAfterMs?: number;
|
||||
}
|
||||
|
||||
export interface AttemptRetryState {
|
||||
sawOutput: boolean;
|
||||
retryableSessionFailureDetected?: boolean;
|
||||
streamedTriggerReason?: AttemptStreamedTrigger;
|
||||
error?: unknown;
|
||||
outputError?: string | null;
|
||||
}
|
||||
|
||||
export function isRetryableClaudeSessionFailureAttempt(args: {
|
||||
attempt: AttemptRetryState;
|
||||
isClaudeCodeAgent: boolean;
|
||||
provider: 'claude' | 'codex';
|
||||
shouldRetryFreshSessionOnAgentFailure: (args: {
|
||||
result: null;
|
||||
error: string;
|
||||
}) => boolean;
|
||||
}): boolean {
|
||||
if (
|
||||
!args.isClaudeCodeAgent ||
|
||||
args.provider !== 'claude' ||
|
||||
args.attempt.sawOutput
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (args.attempt.retryableSessionFailureDetected === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (args.attempt.error == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return args.shouldRetryFreshSessionOnAgentFailure({
|
||||
result: null,
|
||||
error: getErrorMessage(args.attempt.error),
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveClaudeRetryTrigger(args: {
|
||||
canRetryClaudeCredentials: boolean;
|
||||
provider: 'claude' | 'codex';
|
||||
attempt: Pick<AttemptRetryState, 'sawOutput' | 'streamedTriggerReason'>;
|
||||
fallbackMessage?: string | null;
|
||||
}): { reason: AgentTriggerReason; retryAfterMs?: number } | null {
|
||||
if (
|
||||
!(
|
||||
args.canRetryClaudeCredentials &&
|
||||
args.provider === 'claude' &&
|
||||
!args.attempt.sawOutput
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (args.attempt.streamedTriggerReason) {
|
||||
return {
|
||||
reason: args.attempt.streamedTriggerReason.reason,
|
||||
retryAfterMs: args.attempt.streamedTriggerReason.retryAfterMs,
|
||||
};
|
||||
}
|
||||
|
||||
const trigger = classifyRotationTrigger(args.fallbackMessage);
|
||||
if (!trigger.shouldRetry) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
reason: trigger.reason,
|
||||
retryAfterMs: trigger.retryAfterMs,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveCodexRetryTrigger(args: {
|
||||
canRetryCodex: boolean;
|
||||
attempt: Pick<AttemptRetryState, 'streamedTriggerReason'>;
|
||||
rotationMessage?: string | null;
|
||||
}): { reason: CodexRotationReason } | null {
|
||||
if (!args.canRetryCodex) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (args.attempt.streamedTriggerReason) {
|
||||
if (!isCodexRotationReason(args.attempt.streamedTriggerReason.reason)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
reason: args.attempt.streamedTriggerReason.reason,
|
||||
};
|
||||
}
|
||||
|
||||
const trigger = detectCodexRotationTrigger(args.rotationMessage);
|
||||
if (!trigger.shouldRotate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { reason: trigger.reason };
|
||||
}
|
||||
|
||||
export type AttemptRetryAction =
|
||||
| {
|
||||
kind: 'claude';
|
||||
trigger: { reason: AgentTriggerReason; retryAfterMs?: number };
|
||||
rotationMessage?: string;
|
||||
}
|
||||
| {
|
||||
kind: 'codex';
|
||||
trigger: { reason: CodexRotationReason };
|
||||
rotationMessage?: string;
|
||||
}
|
||||
| { kind: 'none' };
|
||||
|
||||
export function resolveAttemptRetryAction(args: {
|
||||
provider: 'claude' | 'codex';
|
||||
canRetryClaudeCredentials: boolean;
|
||||
canRetryCodex: boolean;
|
||||
attempt: Pick<AttemptRetryState, 'sawOutput' | 'streamedTriggerReason'>;
|
||||
rotationMessage?: string | null;
|
||||
}): AttemptRetryAction {
|
||||
const normalizedRotationMessage = args.rotationMessage ?? undefined;
|
||||
|
||||
const claudeTrigger = resolveClaudeRetryTrigger({
|
||||
canRetryClaudeCredentials: args.canRetryClaudeCredentials,
|
||||
provider: args.provider,
|
||||
attempt: args.attempt,
|
||||
fallbackMessage: normalizedRotationMessage,
|
||||
});
|
||||
if (claudeTrigger) {
|
||||
return {
|
||||
kind: 'claude',
|
||||
trigger: claudeTrigger,
|
||||
rotationMessage: normalizedRotationMessage,
|
||||
};
|
||||
}
|
||||
|
||||
const codexTrigger = resolveCodexRetryTrigger({
|
||||
canRetryCodex: args.canRetryCodex,
|
||||
attempt: args.attempt,
|
||||
rotationMessage: normalizedRotationMessage,
|
||||
});
|
||||
if (codexTrigger) {
|
||||
return {
|
||||
kind: 'codex',
|
||||
trigger: codexTrigger,
|
||||
rotationMessage: normalizedRotationMessage,
|
||||
};
|
||||
}
|
||||
|
||||
return { kind: 'none' };
|
||||
}
|
||||
Reference in New Issue
Block a user