fix(paired): cap consecutive reviewer silent-failure retries

The reviewer (codex) sometimes emits a final result with no visible text.
The runtime marks the run failed → handleFailedReviewerExecution
preserves status as review_ready → the follow-up scheduler re-queues
another reviewer-turn → loop. The existing round_trip / arbiter caps
never fire because round_trip_count only advances on owner-side
submissions; bot-side reviewer flakes never increment it.

Mirror the owner_failure_count pattern: add reviewer_failure_count to
paired_tasks, increment on each silent failure, and escalate via
requestArbiterOrEscalate once it reaches 2. Reset to 0 on every
successful reviewer completion path (PROCEED/REVISE/arbiter/wait-for-user)
and when the owner re-submits a fresh review cycle.

Repro chat: 1507762222724546560 (stock-adiviser). Task
33968d31-0da2-480c-85d3-7a3999822ab4 logged 11 consecutive
reviewer-turn entries with no work_items and round_trip_count stuck
at 1.
This commit is contained in:
Codex
2026-05-29 23:40:35 +09:00
parent cdb59ce9fe
commit 32fbb00dd2
11 changed files with 147 additions and 11 deletions

View File

@@ -121,6 +121,7 @@ export function applyBaseSchema(database: Database): void {
review_requested_at TEXT,
round_trip_count INTEGER NOT NULL DEFAULT 0,
owner_failure_count INTEGER NOT NULL DEFAULT 0,
reviewer_failure_count INTEGER NOT NULL DEFAULT 0,
owner_step_done_streak INTEGER NOT NULL DEFAULT 0,
finalize_step_done_count INTEGER NOT NULL DEFAULT 0,
task_done_then_user_reopen_count INTEGER NOT NULL DEFAULT 0,

View File

@@ -39,6 +39,7 @@ function getExpectedSchemaMigrations(): Array<{
{ version: 12, name: 'paired_verdict_and_step_telemetry' },
{ version: 13, name: 'message_source_kind' },
{ version: 14, name: 'work_item_attachments' },
{ version: 15, name: 'reviewer_failure_count' },
];
}

View File

@@ -0,0 +1,23 @@
import type { Database } from 'bun:sqlite';
import { tableHasColumn } from './helpers.js';
import type { SchemaMigrationDefinition } from './types.js';
export const REVIEWER_FAILURE_COUNT_MIGRATION: SchemaMigrationDefinition = {
version: 15,
name: 'reviewer_failure_count',
apply(database: Database) {
if (!tableHasColumn(database, 'paired_tasks', 'reviewer_failure_count')) {
database.exec(`
ALTER TABLE paired_tasks
ADD COLUMN reviewer_failure_count INTEGER NOT NULL DEFAULT 0
`);
}
database.exec(`
UPDATE paired_tasks
SET reviewer_failure_count = 0
WHERE reviewer_failure_count IS NULL
`);
},
};

View File

@@ -14,6 +14,7 @@ import { OWNER_FAILURE_COUNT_MIGRATION } from './011_owner-failure-count.js';
import { PAIRED_VERDICT_AND_STEP_TELEMETRY_MIGRATION } from './012_paired-verdict-and-step-telemetry.js';
import { MESSAGE_SOURCE_KIND_MIGRATION } from './013_message-source-kind.js';
import { WORK_ITEM_ATTACHMENTS_MIGRATION } from './014_work-item-attachments.js';
import { REVIEWER_FAILURE_COUNT_MIGRATION } from './015_reviewer-failure-count.js';
import type {
SchemaMigrationArgs,
SchemaMigrationDefinition,
@@ -36,6 +37,7 @@ const ORDERED_SCHEMA_MIGRATIONS: readonly SchemaMigrationDefinition[] = [
PAIRED_VERDICT_AND_STEP_TELEMETRY_MIGRATION,
MESSAGE_SOURCE_KIND_MIGRATION,
WORK_ITEM_ATTACHMENTS_MIGRATION,
REVIEWER_FAILURE_COUNT_MIGRATION,
];
function ensureSchemaMigrationsTable(database: Database): void {

View File

@@ -51,6 +51,7 @@ export type PairedTaskUpdates = Partial<
| 'review_requested_at'
| 'round_trip_count'
| 'owner_failure_count'
| 'reviewer_failure_count'
| 'owner_step_done_streak'
| 'finalize_step_done_count'
| 'task_done_then_user_reopen_count'
@@ -177,6 +178,7 @@ export function createPairedTaskInDatabase(
review_requested_at,
round_trip_count,
owner_failure_count,
reviewer_failure_count,
owner_step_done_streak,
finalize_step_done_count,
task_done_then_user_reopen_count,
@@ -188,7 +190,7 @@ export function createPairedTaskInDatabase(
created_at,
updated_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`,
)
.run(
@@ -206,6 +208,7 @@ export function createPairedTaskInDatabase(
task.review_requested_at,
task.round_trip_count,
task.owner_failure_count ?? 0,
task.reviewer_failure_count ?? 0,
task.owner_step_done_streak ?? 0,
task.finalize_step_done_count ?? 0,
task.task_done_then_user_reopen_count ?? 0,
@@ -318,6 +321,10 @@ export function updatePairedTaskInDatabase(
fields.push('owner_failure_count = ?');
values.push(updates.owner_failure_count);
}
if (updates.reviewer_failure_count !== undefined) {
fields.push('reviewer_failure_count = ?');
values.push(updates.reviewer_failure_count);
}
if (updates.owner_step_done_streak !== undefined) {
fields.push('owner_step_done_streak = ?');
values.push(updates.owner_step_done_streak);
@@ -397,6 +404,10 @@ export function updatePairedTaskIfUnchangedInDatabase(
fields.push('owner_failure_count = ?');
values.push(updates.owner_failure_count);
}
if (updates.reviewer_failure_count !== undefined) {
fields.push('reviewer_failure_count = ?');
values.push(updates.reviewer_failure_count);
}
if (updates.owner_step_done_streak !== undefined) {
fields.push('owner_step_done_streak = ?');
values.push(updates.owner_step_done_streak);