import { logger } from './logger.js'; import { isTerminalCodexAccountFailure } from './agent-error-detection.js'; import { transitionPairedTaskStatus } from './paired-task-status.js'; import { classifyArbiterVerdict } from './paired-verdict.js'; import type { PairedTask } from './types.js'; const ARBITER_RESOLUTION_ROUND_TRIP_COUNT = 0; export function handleFailedArbiterExecution(args: { task: PairedTask; taskId: string; summary?: string | null; }): void { const { task, taskId, summary } = args; if (isTerminalCodexAccountFailure(summary)) { const now = new Date().toISOString(); const nextOwnerFailureCount = Math.max( (task.owner_failure_count ?? 0) + 1, 1, ); transitionPairedTaskStatus({ taskId, currentStatus: task.status, nextStatus: 'active', expectedUpdatedAt: task.updated_at, updatedAt: now, patch: { owner_failure_count: nextOwnerFailureCount, owner_step_done_streak: 0, finalize_step_done_count: 0, empty_step_done_streak: 0, arbiter_verdict: null, arbiter_requested_at: null, }, }); logger.warn( { taskId, role: 'arbiter', status: task.status, nextOwnerFailureCount, summary: summary?.slice(0, 200), }, 'Returned arbiter task to owner after terminal Codex account failure', ); return; } const now = new Date().toISOString(); const fallbackStatus = task.status === 'in_arbitration' || task.status === 'arbiter_requested' ? 'arbiter_requested' : task.status; if (fallbackStatus !== task.status) { transitionPairedTaskStatus({ taskId, currentStatus: task.status, nextStatus: fallbackStatus, expectedUpdatedAt: task.updated_at, updatedAt: now, }); logger.warn( { taskId, role: 'arbiter', previousStatus: task.status, nextStatus: fallbackStatus, }, 'Preserved arbiter task in arbitration-requested state after failed execution', ); } } export function handleArbiterCompletion(args: { task: PairedTask; taskId: string; summary?: string | null; }): void { const { task, taskId, summary } = args; const now = new Date().toISOString(); const arbiterVerdict = classifyArbiterVerdict(summary); // Persist a running count of arbiter interventions so the loop can be bounded. // This must NOT be reset by the round_trip_count reset below — otherwise the // owner↔reviewer loop could re-trigger the arbiter without limit. const nextArbiterInterventionCount = (task.arbiter_intervention_count ?? 0) + 1; logger.info( { taskId, arbiterVerdict, arbiterInterventionCount: nextArbiterInterventionCount, summary: summary?.slice(0, 200), }, 'Arbiter verdict rendered', ); switch (arbiterVerdict) { case 'proceed': transitionPairedTaskStatus({ taskId, currentStatus: 'in_arbitration', nextStatus: 'review_ready', expectedUpdatedAt: task.updated_at, updatedAt: now, patch: { round_trip_count: ARBITER_RESOLUTION_ROUND_TRIP_COUNT, owner_failure_count: 0, owner_step_done_streak: 0, finalize_step_done_count: 0, empty_step_done_streak: 0, arbiter_intervention_count: nextArbiterInterventionCount, arbiter_verdict: arbiterVerdict, arbiter_requested_at: null, }, }); logger.info( { taskId, arbiterVerdict }, 'Arbiter proceeded — returning task to reviewer for approval', ); return; case 'revise': case 'reset': transitionPairedTaskStatus({ taskId, currentStatus: 'in_arbitration', nextStatus: 'active', expectedUpdatedAt: task.updated_at, updatedAt: now, patch: { round_trip_count: ARBITER_RESOLUTION_ROUND_TRIP_COUNT, owner_failure_count: 0, owner_step_done_streak: 0, finalize_step_done_count: 0, empty_step_done_streak: 0, arbiter_intervention_count: nextArbiterInterventionCount, arbiter_verdict: arbiterVerdict, arbiter_requested_at: null, }, }); logger.info( { taskId, arbiterVerdict, arbiterInterventionCount: nextArbiterInterventionCount, }, 'Arbiter requested owner changes — resuming owner flow', ); return; case 'escalate': transitionPairedTaskStatus({ taskId, currentStatus: 'in_arbitration', nextStatus: 'completed', expectedUpdatedAt: task.updated_at, updatedAt: now, patch: { arbiter_verdict: 'escalate', completion_reason: 'arbiter_escalated', }, }); logger.info({ taskId }, 'Arbiter escalated to user — task completed'); return; default: transitionPairedTaskStatus({ taskId, currentStatus: 'in_arbitration', nextStatus: 'completed', expectedUpdatedAt: task.updated_at, updatedAt: now, patch: { arbiter_verdict: 'unknown', completion_reason: 'arbiter_escalated', }, }); logger.warn( { taskId, summary: summary?.slice(0, 200) }, 'Arbiter verdict unrecognized — escalating instead of treating it as approval', ); return; } }