fix(paired): bound arbiter interventions to stop infinite arbiter loop

The owner↔reviewer↔arbiter loop could repeat forever: when the arbiter
ruled PROCEED/REVISE/RESET it reset round_trip_count to 0, and nothing
tracked how many times the arbiter had already intervened. A re-deadlock
re-invoked the arbiter without bound.

Add a persistent arbiter_intervention_count (new column + migration 020)
that survives the round-trip reset, and a configurable cap
ARBITER_MAX_INTERVENTIONS (default 1). Once the arbiter has intervened
that many times and the loop still deadlocks, requestArbiterOrEscalate
escalates straight to the user instead of re-invoking the arbiter.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Codex
2026-06-09 02:15:21 +09:00
parent 5f06673ac6
commit de54f13469
15 changed files with 197 additions and 5 deletions

View File

@@ -147,6 +147,7 @@ export function applyBaseSchema(database: Database): void {
finalize_step_done_count INTEGER NOT NULL DEFAULT 0,
task_done_then_user_reopen_count INTEGER NOT NULL DEFAULT 0,
empty_step_done_streak INTEGER NOT NULL DEFAULT 0,
arbiter_intervention_count INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'active',
arbiter_verdict TEXT,
arbiter_requested_at TEXT,

View File

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

View File

@@ -19,6 +19,7 @@ import { ROOM_SKILL_OVERRIDES_MIGRATION } from './016_room-skill-overrides.js';
import { SCHEDULED_TASK_ROOM_ROLE_MIGRATION } from './017_scheduled-task-room-role.js';
import { PAIRED_TURN_OUTPUT_ATTACHMENTS_MIGRATION } from './018_paired-turn-output-attachments.js';
import { TURN_PROGRESS_TEXT_RECOVERY_MIGRATION } from './019_turn-progress-text-recovery.js';
import { ARBITER_INTERVENTION_COUNT_MIGRATION } from './020_arbiter-intervention-count.js';
import type {
SchemaMigrationArgs,
SchemaMigrationDefinition,
@@ -46,6 +47,7 @@ const ORDERED_SCHEMA_MIGRATIONS: readonly SchemaMigrationDefinition[] = [
SCHEDULED_TASK_ROOM_ROLE_MIGRATION,
PAIRED_TURN_OUTPUT_ATTACHMENTS_MIGRATION,
TURN_PROGRESS_TEXT_RECOVERY_MIGRATION,
ARBITER_INTERVENTION_COUNT_MIGRATION,
];
function ensureSchemaMigrationsTable(database: Database): void {

View File

@@ -54,6 +54,7 @@ export type PairedTaskUpdates = Partial<
| 'finalize_step_done_count'
| 'task_done_then_user_reopen_count'
| 'empty_step_done_streak'
| 'arbiter_intervention_count'
| 'status'
| 'arbiter_verdict'
| 'arbiter_requested_at'
@@ -180,6 +181,7 @@ export function createPairedTaskInDatabase(
finalize_step_done_count,
task_done_then_user_reopen_count,
empty_step_done_streak,
arbiter_intervention_count,
status,
arbiter_verdict,
arbiter_requested_at,
@@ -187,7 +189,7 @@ export function createPairedTaskInDatabase(
created_at,
updated_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`,
)
.run(
@@ -209,6 +211,7 @@ export function createPairedTaskInDatabase(
task.finalize_step_done_count ?? 0,
task.task_done_then_user_reopen_count ?? 0,
task.empty_step_done_streak ?? 0,
task.arbiter_intervention_count ?? 0,
task.status,
task.arbiter_verdict,
task.arbiter_requested_at,
@@ -355,6 +358,10 @@ export function updatePairedTaskInDatabase(
fields.push('empty_step_done_streak = ?');
values.push(updates.empty_step_done_streak);
}
if (updates.arbiter_intervention_count !== undefined) {
fields.push('arbiter_intervention_count = ?');
values.push(updates.arbiter_intervention_count);
}
if (updates.status !== undefined) {
fields.push('status = ?');
values.push(updates.status);
@@ -434,6 +441,10 @@ export function updatePairedTaskIfUnchangedInDatabase(
fields.push('empty_step_done_streak = ?');
values.push(updates.empty_step_done_streak);
}
if (updates.arbiter_intervention_count !== undefined) {
fields.push('arbiter_intervention_count = ?');
values.push(updates.arbiter_intervention_count);
}
if (updates.status !== undefined) {
fields.push('status = ?');
values.push(updates.status);