fix(triage): clear specifying status on pause/stuck and add StuckTaskDetector support
- Fix pause-abort handler using updateTask({status: undefined}) which was a
no-op, leaving tasks stuck in 'specifying' forever after a pause interrupts
a session post-APPROVE. Changed to status: null to actually clear the field.
- Apply same fix to transient-error retry and general error catch paths.
- Wire StuckTaskDetector into TriageProcessor: trackTask/untrackTask/recordActivity
on session lifecycle, markStuckAborted to prevent stuck kills from being
reported as errors, and clear status to null on stuck-kill for next-poll retry.
- Update dashboard.ts to pass stuckTaskDetector to TriageProcessor and call
triageRef.current?.markStuckAborted in the shared onStuck callback.
- Add 3 tests covering pause-abort status clearing and stuck detector wiring.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -578,15 +578,6 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
|
||||
|
||||
// Start the AI engine (unless in dev mode)
|
||||
if (!opts.dev) {
|
||||
const triage = new TriageProcessor(store, cwd, {
|
||||
semaphore,
|
||||
usageLimitPauser,
|
||||
agentStore,
|
||||
onSpecifyStart: (t) => console.log(`[engine] Specifying ${t.id}...`),
|
||||
onSpecifyComplete: (t) => console.log(`[engine] ✓ ${t.id} → todo`),
|
||||
onSpecifyError: (t, e) => console.log(`[engine] ✗ ${t.id}: ${e.message}`),
|
||||
});
|
||||
|
||||
// ── Self-healing: auto-unpause, stuck kill budgets, maintenance ─────
|
||||
const selfHealing = new SelfHealingManager(store, {
|
||||
rootDir: cwd,
|
||||
@@ -595,14 +586,18 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
|
||||
});
|
||||
|
||||
// ── Stuck task detector: monitors agent sessions for stagnation ────
|
||||
// Created before the executor so it can be passed in options.
|
||||
// The onStuck callback is wired to executor.markStuckAborted after
|
||||
// executor creation (late-binding via closure on executorRef).
|
||||
// Created before triage/executor so it can be passed in options.
|
||||
// The onStuck callback is wired via late-binding closures on triageRef
|
||||
// and executorRef to avoid circular construction order dependencies.
|
||||
const executorRef: { current: TaskExecutor | null } = { current: null };
|
||||
const triageRef: { current: TriageProcessor | null } = { current: null };
|
||||
const stuckTaskDetector = new StuckTaskDetector(store, {
|
||||
beforeRequeue: (taskId) => selfHealing.checkStuckBudget(taskId),
|
||||
onLoopDetected: (event) => executorRef.current?.handleLoopDetected(event) ?? Promise.resolve(false),
|
||||
onStuck: (event) => {
|
||||
// Notify whichever component owns this task (triage or executor).
|
||||
// Both check their own tracking sets so only the owner acts.
|
||||
triageRef.current?.markStuckAborted(event.taskId);
|
||||
executorRef.current?.markStuckAborted(event.taskId, event.shouldRequeue);
|
||||
console.log(
|
||||
`[engine] ⚠ ${event.taskId} stuck (${event.reason}) — ` +
|
||||
@@ -613,6 +608,17 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
|
||||
},
|
||||
});
|
||||
|
||||
const triage = new TriageProcessor(store, cwd, {
|
||||
semaphore,
|
||||
usageLimitPauser,
|
||||
stuckTaskDetector,
|
||||
agentStore,
|
||||
onSpecifyStart: (t) => console.log(`[engine] Specifying ${t.id}...`),
|
||||
onSpecifyComplete: (t) => console.log(`[engine] ✓ ${t.id} → todo`),
|
||||
onSpecifyError: (t, e) => console.log(`[engine] ✗ ${t.id}: ${e.message}`),
|
||||
});
|
||||
triageRef.current = triage;
|
||||
|
||||
const executor = new TaskExecutor(store, cwd, {
|
||||
semaphore,
|
||||
pool,
|
||||
|
||||
Reference in New Issue
Block a user