diff --git a/src/owner-final-codex-unavailable.test.ts b/src/owner-final-codex-unavailable.test.ts index 0856f09..f705845 100644 --- a/src/owner-final-codex-unavailable.test.ts +++ b/src/owner-final-codex-unavailable.test.ts @@ -151,6 +151,34 @@ describe('owner final Codex unavailable handling', () => { ); }); + it('escalates to the user after persistent owner and arbiter Codex account failures', () => { + vi.mocked(db.getPairedTaskById).mockReturnValue( + buildPairedTask({ + status: 'active', + owner_failure_count: 3, + updated_at: '2026-03-28T00:00:05.000Z', + }), + ); + + completePairedExecutionContext({ + taskId: 'task-1', + role: 'owner', + status: 'failed', + summary: CODEX_UNAVAILABLE_SUMMARY, + }); + + expect(db.updatePairedTask).toHaveBeenCalledWith( + 'task-1', + expect.objectContaining({ + status: 'completed', + owner_failure_count: 4, + arbiter_verdict: 'escalate', + arbiter_requested_at: null, + completion_reason: 'escalated', + }), + ); + }); + it('requeues owner finalization once after preserving merge_ready', () => { expect( resolvePairedFollowUpQueueAction({ diff --git a/src/paired-execution-context-owner.ts b/src/paired-execution-context-owner.ts index 80aca0c..d551897 100644 --- a/src/paired-execution-context-owner.ts +++ b/src/paired-execution-context-owner.ts @@ -22,6 +22,7 @@ import type { PairedTask } from './types.js'; type OwnerFinalizeOutcome = 'stop' | 're_review'; const OWNER_FAILURE_ESCALATION_THRESHOLD = 2; +const OWNER_CODEX_UNAVAILABLE_USER_ESCALATION_THRESHOLD = 4; const EMPTY_STEP_DONE_THRESHOLD = 2; export function handleFailedOwnerExecution(args: { @@ -33,6 +34,33 @@ export function handleFailedOwnerExecution(args: { const now = new Date().toISOString(); const nextFailureCount = (task.owner_failure_count ?? 0) + 1; if (isTerminalCodexAccountFailure(summary)) { + if (nextFailureCount >= OWNER_CODEX_UNAVAILABLE_USER_ESCALATION_THRESHOLD) { + transitionPairedTaskStatus({ + taskId, + currentStatus: task.status, + nextStatus: 'completed', + expectedUpdatedAt: task.updated_at, + updatedAt: now, + patch: { + owner_failure_count: nextFailureCount, + arbiter_verdict: 'escalate', + arbiter_requested_at: null, + completion_reason: 'escalated', + }, + }); + logger.warn( + { + taskId, + role: 'owner', + previousStatus: task.status, + ownerFailureCount: nextFailureCount, + summary: summary?.slice(0, 160), + }, + 'Escalated owner task after persistent Codex account failures', + ); + return; + } + if (nextFailureCount >= OWNER_FAILURE_ESCALATION_THRESHOLD) { requestArbiterOrEscalate({ taskId,