Merge remote-tracking branch 'gitea/main'
# Conflicts: # prompts/owner-common-paired-room.md # src/channels/discord.ts # src/codex-warmup.ts # src/db/bootstrap.test.ts # src/db/migrations/index.ts # src/paired-execution-context-reviewer.ts # src/usage-primer.test.ts # src/usage-primer.ts
This commit is contained in:
@@ -3,7 +3,10 @@ import { getPairedWorkspace } from './db.js';
|
||||
import { isTerminalCodexAccountFailure } from './agent-error-detection.js';
|
||||
import { logger } from './logger.js';
|
||||
import { requestArbiterOrEscalate } from './paired-arbiter-request.js';
|
||||
import { transitionPairedTaskStatus } from './paired-task-status.js';
|
||||
import {
|
||||
applyPairedTaskPatch,
|
||||
transitionPairedTaskStatus,
|
||||
} from './paired-task-status.js';
|
||||
import {
|
||||
resolveReviewerCompletionSignal,
|
||||
resolveReviewerFailureSignal,
|
||||
@@ -12,6 +15,17 @@ import { resolveCanonicalSourceRef } from './paired-source-ref.js';
|
||||
import { parseVisibleVerdict } from './paired-verdict.js';
|
||||
import type { PairedTask } from './types.js';
|
||||
|
||||
/**
|
||||
* Maximum consecutive reviewer executions that may produce no verdict
|
||||
* before we stop the ping-pong loop. Mirrors the owner-side failure cap and
|
||||
* closes the "reviewer silently fails forever" hole: codex sometimes emits a
|
||||
* final result with no visible text, the runtime marks it failed, the
|
||||
* next-turn scheduler re-queues another reviewer-turn, and the original
|
||||
* round_trip_count never increments — so the existing round-trip / arbiter
|
||||
* caps never fire.
|
||||
*/
|
||||
const REVIEWER_FAILURE_ESCALATION_THRESHOLD = 2;
|
||||
|
||||
export function handleFailedReviewerExecution(args: {
|
||||
task: PairedTask;
|
||||
taskId: string;
|
||||
@@ -104,6 +118,35 @@ export function handleFailedReviewerExecution(args: {
|
||||
}
|
||||
}
|
||||
|
||||
// Reviewer produced no usable verdict and no recognised infrastructure
|
||||
// failure. Track consecutive flakes — after the threshold, stop the
|
||||
// ping-pong loop instead of preserving review_ready forever.
|
||||
const nextFailureCount = (task.reviewer_failure_count ?? 0) + 1;
|
||||
|
||||
if (nextFailureCount >= REVIEWER_FAILURE_ESCALATION_THRESHOLD) {
|
||||
requestArbiterOrEscalate({
|
||||
taskId,
|
||||
currentStatus: task.status,
|
||||
expectedUpdatedAt: task.updated_at,
|
||||
now,
|
||||
arbiterLogMessage:
|
||||
'Reviewer failed repeatedly without a visible verdict — requesting arbiter',
|
||||
escalateLogMessage:
|
||||
'Reviewer failed repeatedly without a visible verdict — escalating to user',
|
||||
logContext: {
|
||||
taskId,
|
||||
role: 'reviewer',
|
||||
previousStatus: task.status,
|
||||
reviewerFailureCount: nextFailureCount,
|
||||
summary: summary?.slice(0, 160),
|
||||
},
|
||||
patch: {
|
||||
reviewer_failure_count: nextFailureCount,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const fallbackStatus =
|
||||
task.status === 'in_review' || task.status === 'review_ready'
|
||||
? 'review_ready'
|
||||
@@ -115,17 +158,30 @@ export function handleFailedReviewerExecution(args: {
|
||||
nextStatus: fallbackStatus,
|
||||
expectedUpdatedAt: task.updated_at,
|
||||
updatedAt: now,
|
||||
});
|
||||
logger.warn(
|
||||
{
|
||||
taskId,
|
||||
role: 'reviewer',
|
||||
previousStatus: task.status,
|
||||
nextStatus: fallbackStatus,
|
||||
patch: {
|
||||
reviewer_failure_count: nextFailureCount,
|
||||
},
|
||||
'Preserved reviewer task in review-ready state after failed execution',
|
||||
);
|
||||
});
|
||||
} else {
|
||||
applyPairedTaskPatch({
|
||||
taskId,
|
||||
expectedUpdatedAt: task.updated_at,
|
||||
updatedAt: now,
|
||||
patch: {
|
||||
reviewer_failure_count: nextFailureCount,
|
||||
},
|
||||
});
|
||||
}
|
||||
logger.warn(
|
||||
{
|
||||
taskId,
|
||||
role: 'reviewer',
|
||||
previousStatus: task.status,
|
||||
reviewerFailureCount: nextFailureCount,
|
||||
summary: summary?.slice(0, 160),
|
||||
},
|
||||
'Preserved reviewer task in review-ready state after failed execution',
|
||||
);
|
||||
}
|
||||
|
||||
export function handleReviewerCompletion(args: {
|
||||
@@ -157,6 +213,7 @@ export function handleReviewerCompletion(args: {
|
||||
patch: {
|
||||
source_ref: approvedSourceRef,
|
||||
owner_failure_count: 0,
|
||||
reviewer_failure_count: 0,
|
||||
owner_step_done_streak: 0,
|
||||
finalize_step_done_count: 0,
|
||||
empty_step_done_streak: 0,
|
||||
@@ -194,6 +251,7 @@ export function handleReviewerCompletion(args: {
|
||||
escalateLogMessage,
|
||||
logContext: { taskId, verdict, summary: summary?.slice(0, 100) },
|
||||
arbiterInterventionCount: task.arbiter_intervention_count ?? 0,
|
||||
patch: { reviewer_failure_count: 0 },
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -205,6 +263,7 @@ export function handleReviewerCompletion(args: {
|
||||
nextStatus: 'active',
|
||||
expectedUpdatedAt: task.updated_at,
|
||||
updatedAt: now,
|
||||
patch: { reviewer_failure_count: 0 },
|
||||
});
|
||||
logger.info(
|
||||
{ taskId, verdict },
|
||||
|
||||
Reference in New Issue
Block a user