fix: force-requeue stuck task when hung subprocess blocks executor unwind
When a stuck task's session is disposed but an in-flight bash subprocess keeps running, the executor's Promise never resolves, so the requeue finally block never fires and the task stays stranded in-progress indefinitely (observed with FN-1269). Add a 60s safety-net timer in markStuckAborted: if the task is still in executing after the grace period, force-requeue it directly by clearing the worktree, setting status=stuck-killed, moving to todo, and removing it from the executing set so the scheduler can re-dispatch normally. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2954,6 +2954,37 @@ If issues are found that need attention, describe them clearly.`;
|
||||
);
|
||||
}
|
||||
this.stuckAborted.set(taskId, shouldRequeue);
|
||||
|
||||
// Safety net: if the executor's Promise never resolves (e.g. a bash subprocess
|
||||
// is blocking the agent session even after dispose()), force-requeue the task
|
||||
// directly after a short grace period. Without this, a task with a hung tool
|
||||
// call stays stranded in "in-progress" until the engine restarts.
|
||||
if (shouldRequeue && this.executing.has(taskId)) {
|
||||
const FORCE_REQUEUE_GRACE_MS = 60_000; // 60 s — generous, but bounded
|
||||
setTimeout(async () => {
|
||||
if (!this.executing.has(taskId)) return; // executor unwound normally — nothing to do
|
||||
executorLog.warn(
|
||||
`${taskId} still executing ${FORCE_REQUEUE_GRACE_MS / 1000}s after stuck-kill signal ` +
|
||||
`(likely a hung subprocess) — force-requeueing`,
|
||||
);
|
||||
try {
|
||||
await this.store.logEntry(
|
||||
taskId,
|
||||
`Force-requeued after stuck-kill: executor did not unwind within ${FORCE_REQUEUE_GRACE_MS / 1000}s (hung subprocess)`,
|
||||
);
|
||||
await this.store.updateTask(taskId, { status: "stuck-killed", worktree: null, branch: null });
|
||||
await this.store.moveTask(taskId, "todo");
|
||||
// Remove from executing so the scheduler can re-dispatch normally.
|
||||
// The old Promise is still running but the executing guard is cleared so
|
||||
// a fresh execute() call won't be blocked.
|
||||
this.executing.delete(taskId);
|
||||
this.stuckAborted.delete(taskId);
|
||||
executorLog.log(`${taskId} force-requeued to todo`);
|
||||
} catch (err: any) {
|
||||
executorLog.error(`Failed to force-requeue stuck task ${taskId}: ${err.message}`);
|
||||
}
|
||||
}, FORCE_REQUEUE_GRACE_MS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user