diff --git a/packages/engine/src/__tests__/self-healing.test.ts b/packages/engine/src/__tests__/self-healing.test.ts index fd4d3b395..65add43b6 100644 --- a/packages/engine/src/__tests__/self-healing.test.ts +++ b/packages/engine/src/__tests__/self-healing.test.ts @@ -440,14 +440,11 @@ describe("SelfHealingManager", () => { const result = await manager.checkStuckBudget("FN-001", "loop"); expect(result).toBe(false); - expect(store.updateTask).toHaveBeenCalledWith("FN-001", expect.objectContaining({ - stuckKillCount: 7, - paused: false, - userPaused: false, - pausedReason: null, - status: "queued", - })); - expect(store.moveTask).toHaveBeenCalledWith("FN-001", "todo", { preserveProgress: true }); + expect(store.updateTask).toHaveBeenCalledWith("FN-001", { stuckKillCount: 7 }); + expect(store.moveTask).toHaveBeenCalledWith("FN-001", "todo", { + preserveProgress: true, + preserveStatus: true, + }); expect(store.updateTask).toHaveBeenLastCalledWith("FN-001", expect.objectContaining({ stuckKillCount: 7, paused: false, @@ -462,7 +459,7 @@ describe("SelfHealingManager", () => { ); }); - it("leaves incomplete stuck-loop exhaustion unpaused when todo parking fails", async () => { + it("falls back to executor requeue when todo parking fails", async () => { (store.getTask as ReturnType).mockResolvedValue({ id: "FN-001", column: "in-progress", @@ -478,19 +475,22 @@ describe("SelfHealingManager", () => { const result = await manager.checkStuckBudget("FN-001", "loop"); - expect(result).toBe(false); - expect(store.updateTask).toHaveBeenCalledWith("FN-001", expect.objectContaining({ - stuckKillCount: 7, + expect(result).toBe(true); + expect(store.updateTask).toHaveBeenCalledWith("FN-001", { stuckKillCount: 7 }); + expect(store.moveTask).toHaveBeenCalledWith("FN-001", "todo", { + preserveProgress: true, + preserveStatus: true, + }); + expect(store.updateTask).not.toHaveBeenCalledWith("FN-001", expect.objectContaining({ paused: false, userPaused: false, pausedReason: null, status: "queued", })); - expect(store.moveTask).toHaveBeenCalledWith("FN-001", "todo", { preserveProgress: true }); expect(store.handoffToReview).not.toHaveBeenCalled(); expect(store.logEntry).toHaveBeenCalledWith( "FN-001", - "STUCK_LOOP_EXHAUSTED: incomplete task exhausted stuck kill budget (7/6), last reason=loop. Failed to move task to todo (database is busy); task remains unpaused for scheduler retry.", + "STUCK_LOOP_EXHAUSTED: incomplete task exhausted stuck kill budget (7/6), last reason=loop. Failed to move task to todo (database is busy); falling back to executor stuck-kill requeue.", ); }); diff --git a/packages/engine/src/self-healing.ts b/packages/engine/src/self-healing.ts index c2a00f424..9c88ef760 100644 --- a/packages/engine/src/self-healing.ts +++ b/packages/engine/src/self-healing.ts @@ -1073,34 +1073,32 @@ export class SelfHealingManager { if (hasIncompleteSteps) { log.warn(`${taskId} exceeded stuck kill budget (${newCount}/${maxKills}, reason=${reason}) with incomplete steps — re-queueing in todo with progress preserved`); - await this.store.updateTask(taskId, { - stuckKillCount: newCount, - paused: false, - userPaused: false, - pausedReason: null, - status: "queued", - } as unknown as Partial & { userPaused: boolean }); - let parkedInTodo = true; - let moveErrMessage = ""; + await this.store.updateTask(taskId, { stuckKillCount: newCount }); try { - await this.store.moveTask(taskId, "todo", { preserveProgress: true }); - await this.store.updateTask(taskId, { + await this.store.moveTask(taskId, "todo", { + preserveProgress: true, + preserveStatus: true, + }); + const requeueUpdate = { stuckKillCount: newCount, paused: false, userPaused: false, pausedReason: null, status: "queued", - } as unknown as Partial & { userPaused: boolean }); + } satisfies Parameters[1] & { userPaused: boolean }; + await this.store.updateTask(taskId, requeueUpdate); } catch (moveErr: unknown) { - parkedInTodo = false; - moveErrMessage = moveErr instanceof Error ? moveErr.message : String(moveErr); - log.warn(`${taskId} moveTask(todo) failed (${moveErrMessage}) after incomplete STUCK_LOOP_EXHAUSTED terminalization — task remains queued for scheduler retry`); + const moveErrMessage = moveErr instanceof Error ? moveErr.message : String(moveErr); + log.warn(`${taskId} moveTask(todo) failed (${moveErrMessage}) after incomplete STUCK_LOOP_EXHAUSTED terminalization — falling back to executor stuck-kill requeue`); + await this.store.logEntry( + taskId, + `STUCK_LOOP_EXHAUSTED: incomplete task exhausted stuck kill budget (${newCount}/${maxKills}), last reason=${reason}. Failed to move task to todo (${moveErrMessage}); falling back to executor stuck-kill requeue.`, + ); + return true; } await this.store.logEntry( taskId, - parkedInTodo - ? `STUCK_LOOP_EXHAUSTED: incomplete task exhausted stuck kill budget (${newCount}/${maxKills}), last reason=${reason}. Re-queued in todo with progress preserved; scheduler may retry without manual unpause.` - : `STUCK_LOOP_EXHAUSTED: incomplete task exhausted stuck kill budget (${newCount}/${maxKills}), last reason=${reason}. Failed to move task to todo (${moveErrMessage}); task remains unpaused for scheduler retry.`, + `STUCK_LOOP_EXHAUSTED: incomplete task exhausted stuck kill budget (${newCount}/${maxKills}), last reason=${reason}. Re-queued in todo with progress preserved; scheduler may retry without manual unpause.`, ); return false; }