import { classifyRotationTrigger, type AgentTriggerReason, type CodexRotationReason, isCodexRotationReason, } from './agent-error-detection.js'; import { detectCodexRotationTrigger } from './codex-token-rotation.js'; import { resolveNextTurnAction } from './message-runtime-rules.js'; import type { PairedRoomRole, PairedTaskStatus } from './types.js'; import { getErrorMessage } from './utils.js'; export interface ExecutorStreamedTrigger { reason: AgentTriggerReason; retryAfterMs?: number; } export interface ExecutorAttemptState { sawOutput: boolean; retryableSessionFailureDetected?: boolean; streamedTriggerReason?: ExecutorStreamedTrigger; error?: unknown; } export function isRetryableClaudeSessionFailureAttempt(args: { attempt: ExecutorAttemptState; 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; 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; 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 PairedFollowUpQueueAction = | 'generic' | 'pending' | 'skip-inline-finalize' | 'none'; export function resolvePairedFollowUpQueueAction(args: { completedRole: PairedRoomRole; executionStatus: 'succeeded' | 'failed'; sawOutput: boolean; taskStatus: PairedTaskStatus | null; }): PairedFollowUpQueueAction { const nextTurnAction = resolveNextTurnAction({ taskStatus: args.taskStatus, lastTurnOutputRole: args.executionStatus === 'succeeded' && args.sawOutput ? args.completedRole : null, }); if (args.executionStatus === 'succeeded' && args.sawOutput) { if (nextTurnAction.kind === 'reviewer-turn') { return 'generic'; } return args.completedRole === 'reviewer' && nextTurnAction.kind === 'finalize-owner-turn' ? 'skip-inline-finalize' : 'none'; } const shouldRequeuePendingPairedTurn = (args.completedRole === 'reviewer' || args.completedRole === 'arbiter') && (nextTurnAction.kind === 'reviewer-turn' || nextTurnAction.kind === 'arbiter-turn' || nextTurnAction.kind === 'finalize-owner-turn'); return shouldRequeuePendingPairedTurn ? 'pending' : 'none'; }