From 184a8eff6a9e92e94977c1f84d87cce141252863 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 6 Jun 2026 18:05:12 +0900 Subject: [PATCH] =?UTF-8?q?fix(paired):=20break=20reviewer=20step=5Fdone?= =?UTF-8?q?=20=E2=87=84=20owner=20task=5Fdone=20infinite=20loop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reviewer step_done verdict mapped to request_owner_changes unconditionally, bypassing the deadlock guard that every sibling verdict respects. When the owner kept replying task_done and the reviewer kept replying step_done, the room oscillated forever, spamming duplicate reviewer messages into the channel. Apply the same `roundTripCount >= deadlockThreshold` guard used by the done_with_concerns/continue/default branch so step_done oscillation escalates to the arbiter (or completes via escalation when no arbiter) instead of looping. Reuses existing round_trip_count and deadlockThreshold; no migration needed. Co-Authored-By: Claude Opus 4 --- src/paired-execution-context-shared.test.ts | 20 ++++++++++++++++++++ src/paired-execution-context-shared.ts | 3 +++ 2 files changed, 23 insertions(+) diff --git a/src/paired-execution-context-shared.test.ts b/src/paired-execution-context-shared.test.ts index ed5befe..8f6bb59 100644 --- a/src/paired-execution-context-shared.test.ts +++ b/src/paired-execution-context-shared.test.ts @@ -166,6 +166,26 @@ describe('paired execution context shared verdict helpers', () => { ).toEqual({ kind: 'request_arbiter' }); }); + it('routes reviewer step_done to arbiter once round trips hit the deadlock threshold', () => { + // Below threshold: keep iterating with the owner. + expect( + resolveReviewerCompletionSignal({ + visibleVerdict: 'step_done', + roundTripCount: 1, + deadlockThreshold: 2, + }), + ).toEqual({ kind: 'request_owner_changes' }); + // At/above threshold: break the owner⇄reviewer step_done oscillation by + // escalating to the arbiter instead of looping forever. + expect( + resolveReviewerCompletionSignal({ + visibleVerdict: 'step_done', + roundTripCount: 2, + deadlockThreshold: 2, + }), + ).toEqual({ kind: 'request_arbiter' }); + }); + it('maps reviewer failure verdicts to explicit failure signals', () => { expect( resolveReviewerFailureSignal({ diff --git a/src/paired-execution-context-shared.ts b/src/paired-execution-context-shared.ts index b5b51ba..dcd995c 100644 --- a/src/paired-execution-context-shared.ts +++ b/src/paired-execution-context-shared.ts @@ -112,6 +112,9 @@ export function resolveReviewerCompletionSignal(args: { case 'done': return { kind: 'request_owner_finalize' }; case 'step_done': + if (roundTripCount >= deadlockThreshold) { + return { kind: 'request_arbiter' }; + } return { kind: 'request_owner_changes' }; case 'blocked': case 'needs_context':