From 99baa9b0401e854cc8b5581d8048c7ddcba5b480 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 9 Apr 2026 20:12:09 -0700 Subject: [PATCH] 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 --- packages/engine/src/executor.ts | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/packages/engine/src/executor.ts b/packages/engine/src/executor.ts index 30d67d113..f4e09f9cb 100644 --- a/packages/engine/src/executor.ts +++ b/packages/engine/src/executor.ts @@ -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); + } } /**