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:
@@ -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,
|
||||
|
||||
25
src/db/migrations/020_arbiter-intervention-count.ts
Normal file
25
src/db/migrations/020_arbiter-intervention-count.ts
Normal 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
|
||||
`);
|
||||
},
|
||||
};
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user