diff --git a/src/config.ts b/src/config.ts index 1666ab7..29ab193 100644 --- a/src/config.ts +++ b/src/config.ts @@ -91,6 +91,12 @@ export const RECOVERY_CONCURRENT_AGENTS = parseInt( 10, ); +// ── Paired review ───────────────────────────────────────────────── +// Max owner↔reviewer round trips per task. 0 = unlimited. +const rawMaxRoundTrips = getEnv('PAIRED_MAX_ROUND_TRIPS') || '10'; +export const PAIRED_MAX_ROUND_TRIPS = + rawMaxRoundTrips === '0' ? Infinity : parseInt(rawMaxRoundTrips, 10) || 10; + // ── Container isolation ─────────────────────────────────────────── export const REVIEWER_CONTAINER_IMAGE = getEnv('REVIEWER_CONTAINER_IMAGE') || 'ejclaw-reviewer:latest'; diff --git a/src/db.test.ts b/src/db.test.ts index 1d1924e..6b9fce3 100644 --- a/src/db.test.ts +++ b/src/db.test.ts @@ -556,7 +556,7 @@ describe('paired task state', () => { reviewer_service_id: 'codex-review', title: 'wire up workspaces', source_ref: 'HEAD', - plan_notes: null, + plan_notes: null, round_trip_count: 0, review_requested_at: null, status: 'active', created_at: '2026-03-28T00:00:00.000Z', @@ -594,7 +594,7 @@ describe('paired task state', () => { reviewer_service_id: 'codex-review', title: null, source_ref: null, - plan_notes: null, + plan_notes: null, round_trip_count: 0, review_requested_at: null, status: 'active', created_at: '2026-03-28T00:00:00.000Z', diff --git a/src/db.ts b/src/db.ts index e592b12..b39e5b8 100644 --- a/src/db.ts +++ b/src/db.ts @@ -250,11 +250,7 @@ function createSchema(database: Database.Database): void { source_ref TEXT, plan_notes TEXT, review_requested_at TEXT, - last_finalized_checkpoint TEXT, - gate_turn_kind TEXT, - reviewer_verdict TEXT, - reviewer_verdict_at TEXT, - reviewer_verdict_note TEXT, + round_trip_count INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT 'active', created_at TEXT NOT NULL, updated_at TEXT NOT NULL, @@ -566,7 +562,9 @@ function createSchema(database: Database.Database): void { .get() as { sql?: string } | undefined; const pairedTasksSql = pairedTasksSqlRow?.sql || ''; const pairedTasksNeedsRebuild = - pairedTasksSql && pairedTasksSql.includes('task_policy'); + pairedTasksSql && + (pairedTasksSql.includes('task_policy') || + !pairedTasksSql.includes('round_trip_count')); if (pairedTasksNeedsRebuild) { database.exec(`DROP TABLE IF EXISTS paired_tasks`); database.exec(` @@ -580,11 +578,7 @@ function createSchema(database: Database.Database): void { source_ref TEXT, plan_notes TEXT, review_requested_at TEXT, - last_finalized_checkpoint TEXT, - gate_turn_kind TEXT, - reviewer_verdict TEXT, - reviewer_verdict_at TEXT, - reviewer_verdict_note TEXT, + round_trip_count INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT 'active', created_at TEXT NOT NULL, updated_at TEXT NOT NULL, @@ -1847,11 +1841,12 @@ export function createPairedTask(task: PairedTask): void { source_ref, plan_notes, review_requested_at, + round_trip_count, status, created_at, updated_at ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, ).run( task.id, @@ -1863,6 +1858,7 @@ export function createPairedTask(task: PairedTask): void { task.source_ref, task.plan_notes, task.review_requested_at, + task.round_trip_count, task.status, task.created_at, task.updated_at, @@ -1917,6 +1913,7 @@ export function updatePairedTask( | 'source_ref' | 'plan_notes' | 'review_requested_at' + | 'round_trip_count' | 'status' | 'updated_at' > @@ -1941,6 +1938,10 @@ export function updatePairedTask( fields.push('review_requested_at = ?'); values.push(updates.review_requested_at); } + if (updates.round_trip_count !== undefined) { + fields.push('round_trip_count = ?'); + values.push(updates.round_trip_count); + } if (updates.status !== undefined) { fields.push('status = ?'); values.push(updates.status); diff --git a/src/message-agent-executor.test.ts b/src/message-agent-executor.test.ts index da7ec38..76259db 100644 --- a/src/message-agent-executor.test.ts +++ b/src/message-agent-executor.test.ts @@ -326,7 +326,7 @@ describe('runAgentForGroup room memory', () => { title: null, source_ref: 'HEAD', review_requested_at: null, - plan_notes: null, + plan_notes: null, round_trip_count: 0, status: 'active', created_at: '2026-03-29T00:00:00.000Z', updated_at: '2026-03-29T00:00:00.000Z', @@ -386,7 +386,7 @@ describe('runAgentForGroup room memory', () => { reviewer_service_id: 'codex-main', title: null, source_ref: 'HEAD', - plan_notes: null, + plan_notes: null, round_trip_count: 0, review_requested_at: null, status: 'active', created_at: '2026-03-28T00:00:00.000Z', @@ -467,7 +467,7 @@ describe('runAgentForGroup room memory', () => { reviewer_service_id: 'claude', title: null, source_ref: 'HEAD', - plan_notes: null, + plan_notes: null, round_trip_count: 0, review_requested_at: null, status: 'active', created_at: '2026-03-28T00:00:00.000Z', diff --git a/src/message-agent-executor.ts b/src/message-agent-executor.ts index 9716316..b0fa0be 100644 --- a/src/message-agent-executor.ts +++ b/src/message-agent-executor.ts @@ -1,6 +1,9 @@ import { getErrorMessage } from './utils.js'; -import { getAgentOutputText, getStructuredAgentOutput } from './agent-output.js'; +import { + getAgentOutputText, + getStructuredAgentOutput, +} from './agent-output.js'; import { AgentOutput, runAgentProcess, @@ -226,6 +229,7 @@ export async function runAgentForGroup( }); completePairedExecutionContext({ taskId: pairedExecutionContext.task.id, + role: roomRoleContext?.role ?? 'owner', status: pairedExecutionStatus, summary: pairedExecutionSummary, }); @@ -871,6 +875,7 @@ export async function runAgentForGroup( if (pairedExecutionContext) { completePairedExecutionContext({ taskId: pairedExecutionContext.task.id, + role: roomRoleContext?.role ?? 'owner', status: pairedExecutionStatus, summary: pairedExecutionSummary, }); diff --git a/src/message-runtime.test.ts b/src/message-runtime.test.ts index fab166d..09173c7 100644 --- a/src/message-runtime.test.ts +++ b/src/message-runtime.test.ts @@ -262,7 +262,7 @@ describe('createMessageRuntime', () => { reviewer_service_id: 'codex-main', title: null, source_ref: 'HEAD', - plan_notes: null, + plan_notes: null, round_trip_count: 0, review_requested_at: '2026-03-29T00:00:00.000Z', status: 'review_ready', created_at: '2026-03-29T00:00:00.000Z', @@ -354,7 +354,7 @@ describe('createMessageRuntime', () => { reviewer_service_id: 'codex-main', title: null, source_ref: 'HEAD', - plan_notes: null, + plan_notes: null, round_trip_count: 0, review_requested_at: null, status: 'active', created_at: '2026-03-29T00:00:00.000Z', @@ -2841,5 +2841,4 @@ describe('createMessageRuntime', () => { expect(enqueueTask).not.toHaveBeenCalled(); expect(db.getMessagesSinceSeq).not.toHaveBeenCalled(); }); - }); diff --git a/src/paired-execution-context.test.ts b/src/paired-execution-context.test.ts index 37422ca..c74c3e9 100644 --- a/src/paired-execution-context.test.ts +++ b/src/paired-execution-context.test.ts @@ -215,7 +215,7 @@ describe('paired execution context', () => { reviewer_service_id: 'codex-review', title: null, source_ref: 'HEAD', - plan_notes: null, + plan_notes: null, round_trip_count: 0, review_requested_at: '2026-03-28T00:00:00.000Z', status: 'review_ready', created_at: '2026-03-28T00:00:00.000Z', @@ -246,7 +246,7 @@ describe('paired execution context', () => { reviewer_service_id: 'codex-review', title: null, source_ref: 'HEAD', - plan_notes: null, + plan_notes: null, round_trip_count: 0, review_requested_at: '2026-03-28T00:00:00.000Z', status: 'review_ready', created_at: '2026-03-28T00:00:00.000Z', @@ -280,7 +280,7 @@ describe('paired execution context', () => { reviewer_service_id: 'codex-review', title: null, source_ref: 'HEAD', - plan_notes: null, + plan_notes: null, round_trip_count: 0, review_requested_at: null, status: 'active', created_at: '2026-03-28T00:00:00.000Z', @@ -320,7 +320,7 @@ describe('paired execution context', () => { reviewer_service_id: 'codex-review', title: null, source_ref: 'HEAD', - plan_notes: null, + plan_notes: null, round_trip_count: 0, review_requested_at: '2026-03-28T00:00:00.000Z', status: 'in_review', created_at: '2026-03-28T00:00:00.000Z', @@ -351,6 +351,7 @@ describe('paired execution context', () => { it('completePairedExecutionContext logs without error', () => { completePairedExecutionContext({ taskId: 'task-1', + role: 'owner', status: 'succeeded', summary: 'done', }); @@ -373,7 +374,7 @@ describe('paired execution context', () => { reviewer_service_id: 'codex-review', title: null, source_ref: 'HEAD', - plan_notes: null, + plan_notes: null, round_trip_count: 0, review_requested_at: null, status: 'active', created_at: '2026-03-28T00:00:00.000Z', @@ -387,7 +388,7 @@ describe('paired execution context', () => { reviewer_service_id: 'codex-review', title: null, source_ref: 'HEAD', - plan_notes: null, + plan_notes: null, round_trip_count: 0, review_requested_at: '2026-03-28T00:00:00.000Z', status: 'review_ready', created_at: '2026-03-28T00:00:00.000Z', @@ -454,7 +455,7 @@ describe('paired execution context', () => { reviewer_service_id: 'codex-review', title: null, source_ref: 'HEAD', - plan_notes: null, + plan_notes: null, round_trip_count: 0, review_requested_at: null, status: 'active', created_at: '2026-03-28T00:00:00.000Z', @@ -468,7 +469,7 @@ describe('paired execution context', () => { reviewer_service_id: 'codex-review', title: null, source_ref: 'HEAD', - plan_notes: null, + plan_notes: null, round_trip_count: 0, review_requested_at: '2026-03-28T00:00:00.000Z', status: 'review_ready', created_at: '2026-03-28T00:00:00.000Z', @@ -510,7 +511,7 @@ describe('paired execution context', () => { reviewer_service_id: 'codex-review', title: null, source_ref: 'HEAD', - plan_notes: null, + plan_notes: null, round_trip_count: 0, review_requested_at: null, status: 'active', created_at: '2026-03-28T00:00:00.000Z', @@ -524,7 +525,7 @@ describe('paired execution context', () => { reviewer_service_id: 'codex-review', title: null, source_ref: 'HEAD', - plan_notes: null, + plan_notes: null, round_trip_count: 0, review_requested_at: '2026-03-29T00:00:00.000Z', status: 'review_ready', created_at: '2026-03-28T00:00:00.000Z', diff --git a/src/paired-execution-context.ts b/src/paired-execution-context.ts index 8e200f1..4ba55b1 100644 --- a/src/paired-execution-context.ts +++ b/src/paired-execution-context.ts @@ -1,7 +1,7 @@ import { execFileSync } from 'child_process'; import crypto from 'crypto'; -import { SERVICE_ID, normalizeServiceId } from './config.js'; +import { SERVICE_ID, normalizeServiceId, PAIRED_MAX_ROUND_TRIPS } from './config.js'; import { createPairedTask, getLatestPairedTaskForChat, @@ -103,6 +103,7 @@ function ensureActiveTask( source_ref: resolveCanonicalSourceRef(canonicalWorkDir), plan_notes: null, review_requested_at: null, + round_trip_count: 0, status: 'active', created_at: now, updated_at: now, @@ -203,17 +204,63 @@ export function preparePairedExecutionContext(args: { export function completePairedExecutionContext(args: { taskId: string; + role: 'owner' | 'reviewer'; status: 'succeeded' | 'failed'; summary?: string | null; }): void { + const { taskId, role, status } = args; logger.info( { - taskId: args.taskId, - status: args.status, + taskId, + role, + status, summary: args.summary?.slice(0, 200), }, 'Paired execution completed', ); + + if (status !== 'succeeded') return; + + const task = getPairedTaskById(taskId); + if (!task) return; + + // Owner finished → auto-trigger reviewer (if within round trip limit) + if (role === 'owner') { + if (task.round_trip_count >= PAIRED_MAX_ROUND_TRIPS) { + logger.info( + { taskId, roundTrips: task.round_trip_count, max: PAIRED_MAX_ROUND_TRIPS }, + 'Round trip limit reached, skipping auto-review', + ); + return; + } + + const now = new Date().toISOString(); + const result = markPairedTaskReviewReady(taskId); + if (result) { + updatePairedTask(taskId, { + round_trip_count: task.round_trip_count + 1, + review_requested_at: now, + updated_at: now, + }); + logger.info( + { taskId, roundTrip: task.round_trip_count + 1 }, + 'Auto-triggered reviewer after owner completion', + ); + } + } + + // Reviewer finished → set task back to active so owner can respond + if (role === 'reviewer') { + const now = new Date().toISOString(); + updatePairedTask(taskId, { + status: 'active', + updated_at: now, + }); + logger.info( + { taskId }, + 'Reviewer completed, task set back to active for owner', + ); + } } // --------------------------------------------------------------------------- diff --git a/src/paired-workspace-manager.test.ts b/src/paired-workspace-manager.test.ts index 79a0aab..8b576d3 100644 --- a/src/paired-workspace-manager.test.ts +++ b/src/paired-workspace-manager.test.ts @@ -74,7 +74,7 @@ describe('paired workspace manager', () => { reviewer_service_id: 'codex-review', title: 'review the owner changes', source_ref: 'HEAD', - plan_notes: null, + plan_notes: null, round_trip_count: 0, review_requested_at: null, status: 'active', created_at: now, @@ -162,7 +162,7 @@ describe('paired workspace manager', () => { reviewer_service_id: 'codex-review', title: null, source_ref: 'HEAD', - plan_notes: null, + plan_notes: null, round_trip_count: 0, review_requested_at: null, status: 'active', created_at: now, @@ -214,7 +214,7 @@ describe('paired workspace manager', () => { reviewer_service_id: 'codex-review', title: 'cross service review', source_ref: 'HEAD', - plan_notes: null, + plan_notes: null, round_trip_count: 0, review_requested_at: null, status: 'active', created_at: now, @@ -289,7 +289,7 @@ describe('paired workspace manager', () => { reviewer_service_id: 'codex-review', title: null, source_ref: 'HEAD', - plan_notes: null, + plan_notes: null, round_trip_count: 0, review_requested_at: null, status: 'active', created_at: now, @@ -363,7 +363,7 @@ describe('paired workspace manager', () => { reviewer_service_id: 'codex-review', title: null, source_ref: 'HEAD', - plan_notes: null, + plan_notes: null, round_trip_count: 0, review_requested_at: null, status: 'active', created_at: now, @@ -503,7 +503,7 @@ describe('paired workspace manager', () => { reviewer_service_id: 'codex-review', title: null, source_ref: 'HEAD', - plan_notes: null, + plan_notes: null, round_trip_count: 0, review_requested_at: null, status: 'active', created_at: now, @@ -564,7 +564,7 @@ describe('paired workspace manager', () => { reviewer_service_id: 'codex-review', title: null, source_ref: 'HEAD', - plan_notes: null, + plan_notes: null, round_trip_count: 0, review_requested_at: null, status: 'active', created_at: now, @@ -619,7 +619,7 @@ describe('paired workspace manager', () => { reviewer_service_id: 'codex-review', title: null, source_ref: 'HEAD', - plan_notes: null, + plan_notes: null, round_trip_count: 0, review_requested_at: '2026-03-28T00:01:00.000Z', status: 'review_ready', created_at: now, @@ -680,7 +680,7 @@ describe('paired workspace manager', () => { reviewer_service_id: 'codex-review', title: null, source_ref: 'HEAD', - plan_notes: null, + plan_notes: null, round_trip_count: 0, review_requested_at: null, status: 'active', created_at: now, @@ -743,7 +743,7 @@ describe('paired workspace manager', () => { reviewer_service_id: 'codex-review', title: null, source_ref: 'HEAD', - plan_notes: null, + plan_notes: null, round_trip_count: 0, review_requested_at: null, status: 'active', created_at: now, @@ -831,7 +831,7 @@ describe('paired workspace manager', () => { reviewer_service_id: 'codex-review', title: null, source_ref: 'HEAD', - plan_notes: null, + plan_notes: null, round_trip_count: 0, review_requested_at: null, status: 'active', created_at: now, diff --git a/src/types.ts b/src/types.ts index 626f5f9..25fde0f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -62,6 +62,7 @@ export interface PairedTask { source_ref: string | null; plan_notes: string | null; review_requested_at: string | null; + round_trip_count: number; status: PairedTaskStatus; created_at: string; updated_at: string;