fix(engine): respect global pause in reviewer + stuck detector
Reviewer subprocesses were spawned via fn_review_spec / fn_review_step even with globalPause on, because reviewer.ts had no pause awareness. Stuck detector also kept running, treating pause-disposed sessions as inactivity and re-queuing tasks. Pause-transition listeners only called session.dispose(), which doesn't always interrupt an in-flight LLM stream — letting reviewer spawns leak through after pause flipped. - reviewer.ts: re-read settings, return UNAVAILABLE without spawning when globalPause/enginePaused is on. - stuck-task-detector.ts: skip checkStuckTasks() while paused. - triage.ts / executor.ts: call session.abort() before dispose() in the pause-transition listener to interrupt in-flight work. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -800,6 +800,14 @@ export class TaskExecutor {
|
||||
executorLog.log(`Global pause — terminating agent session for ${taskId}`);
|
||||
this.pausedAborted.add(taskId);
|
||||
this.options.stuckTaskDetector?.untrackTask(taskId);
|
||||
// abort() interrupts any in-flight LLM stream / tool call;
|
||||
// dispose() then releases session resources.
|
||||
const sessionWithAbort = session as unknown as { abort?: () => Promise<void> };
|
||||
if (typeof sessionWithAbort.abort === "function") {
|
||||
void sessionWithAbort.abort().catch((err) => {
|
||||
executorLog.warn(`Failed to abort agent session for ${taskId}: ${err}`);
|
||||
});
|
||||
}
|
||||
session.dispose();
|
||||
// Clean up all in-memory state so nothing leaks when tasks are later unpaused
|
||||
this.loopRecoveryState.delete(taskId);
|
||||
|
||||
@@ -252,6 +252,39 @@ export async function reviewStep(
|
||||
baseline?: string,
|
||||
options: ReviewOptions = {},
|
||||
): Promise<ReviewResult> {
|
||||
// Pause gate: do not spawn a reviewer subprocess while the engine is paused.
|
||||
// Re-read settings from the store so a stale `options.settings` snapshot can't
|
||||
// leak a reviewer past a pause that flipped on after the parent agent started.
|
||||
let liveSettings: Settings | undefined = options.settings;
|
||||
if (options.store) {
|
||||
try {
|
||||
liveSettings = await options.store.getSettings();
|
||||
} catch {
|
||||
// Fall back to the snapshot — better to spawn than crash on a transient store error.
|
||||
}
|
||||
}
|
||||
if (liveSettings?.globalPause || liveSettings?.enginePaused) {
|
||||
const reason = liveSettings.globalPause ? "Global pause" : "Engine paused";
|
||||
reviewerLog.log(
|
||||
`${taskId}: ${reviewType} review for Step ${stepNumber} skipped — ${reason} active`,
|
||||
);
|
||||
if (options.store && options.taskId) {
|
||||
try {
|
||||
await options.store.logEntry(
|
||||
options.taskId,
|
||||
`${reviewType} review skipped — ${reason} active`,
|
||||
);
|
||||
} catch {
|
||||
// best-effort
|
||||
}
|
||||
}
|
||||
return {
|
||||
verdict: "UNAVAILABLE",
|
||||
review: `${reason} active — reviewer not spawned. Stop calling fn_review_* and exit cleanly; the parent task will resume after unpause.`,
|
||||
summary: `Skipped: ${reason}`,
|
||||
};
|
||||
}
|
||||
|
||||
// Build the review request
|
||||
const request = buildReviewRequest(
|
||||
taskId, stepNumber, stepName, reviewType, promptContent, cwd, baseline, options.userComments,
|
||||
|
||||
@@ -437,6 +437,12 @@ export class StuckTaskDetector {
|
||||
return; // Can't read settings — skip this cycle
|
||||
}
|
||||
|
||||
// Pause gate: when globalPause or enginePaused is on, sessions are
|
||||
// intentionally idle (engine listeners dispose them on transition) and
|
||||
// long pauses would otherwise look like inactivity → trigger spurious
|
||||
// stuck-kill / requeue cycles. Skip detection while paused.
|
||||
if (settings.globalPause || settings.enginePaused) return;
|
||||
|
||||
const timeoutMs = settings.taskStuckTimeoutMs;
|
||||
if (!timeoutMs || timeoutMs <= 0) return; // Disabled
|
||||
|
||||
|
||||
@@ -494,6 +494,14 @@ export class TriageProcessor {
|
||||
);
|
||||
this.pauseAborted.add(taskId);
|
||||
this.options.stuckTaskDetector?.untrackTask(taskId);
|
||||
// abort() interrupts any in-flight LLM stream / tool call;
|
||||
// dispose() then releases session resources.
|
||||
const sessionWithAbort = session as { abort?: () => Promise<void>; dispose: () => void };
|
||||
if (typeof sessionWithAbort.abort === "function") {
|
||||
void sessionWithAbort.abort().catch((err) => {
|
||||
planLog.warn(`Failed to abort triage session for ${taskId}: ${err}`);
|
||||
});
|
||||
}
|
||||
session.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user