fix(paired): break reviewer step_done ⇄ owner task_done infinite loop

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 <noreply@anthropic.com>
This commit is contained in:
Codex
2026-06-06 18:05:12 +09:00
parent 8cf351580b
commit 184a8eff6a
2 changed files with 23 additions and 0 deletions

View File

@@ -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({

View File

@@ -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':