feat(FN-4145): clear task pause state when fn_task_done is received

The fix ensures paused tasks are unpaused when `fn_task_done` is received, preventing tasks from remaining stuck in paused state after completion. Test coverage was added for the paused-to-done handoff scenario in both the executor prompt and review verdicts test files. This is a patch release (`@ru

Fusion-Task-Id: FN-4145
This commit is contained in:
Fusion
2026-05-12 17:56:19 -07:00
committed by gsxdsm
parent 2ea489d689
commit d899443a44
4 changed files with 41 additions and 11 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Fix: clear task-level pause on `fn_task_done` so explicit agent completions cannot strand tasks in a `paused` state. Hard-pause gating for deferred completion handoff now keys off `globalPause` only.

View File

@@ -2063,6 +2063,7 @@ describe("TaskExecutor global pause behavior", () => {
it("parks todo tasks in in-progress when fn_task_done is called during global pause", async () => {
const store = createMockStore();
let capturedCustomTools: any[] = [];
let taskDoneResult: any;
const todoTask = {
id: "FN-001",
@@ -2097,7 +2098,7 @@ describe("TaskExecutor global pause behavior", () => {
prompt: vi.fn().mockImplementation(async () => {
const taskDoneTool = capturedCustomTools.find((tool: any) => tool.name === "fn_task_done");
if (taskDoneTool) {
await taskDoneTool.execute("call-1", { summary: "done" });
taskDoneResult = await taskDoneTool.execute("call-1", { summary: "done" });
}
}),
dispose: vi.fn(),
@@ -2106,15 +2107,17 @@ describe("TaskExecutor global pause behavior", () => {
}) as any);
const executor = new TaskExecutor(store, "/tmp/test");
const watchdogSpy = vi.spyOn(executor as any, "scheduleCompletedTaskWatchdog");
await executor.execute(todoTask as any);
expect(store.updateTask).toHaveBeenCalledWith("FN-001", {
paused: false,
pausedByAgentId: null,
paused: undefined,
pausedByAgentId: undefined,
status: null,
});
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "in-progress");
expect(store.moveTask).not.toHaveBeenCalledWith("FN-001", "in-review");
expect(watchdogSpy).not.toHaveBeenCalledWith("FN-001", "fn_task_done");
expect(store.logEntry).toHaveBeenCalledWith(
"FN-001",
expect.stringContaining("fn_task_done called while task was in todo during pause"),
@@ -2125,12 +2128,16 @@ describe("TaskExecutor global pause behavior", () => {
id === "FN-001" && action.includes("Completion handoff deferred — global pause active"),
),
).toBe(true);
expect(taskDoneResult.content[0].text).toBe(
"Task marked complete. Completion handoff deferred until pause is cleared.",
);
});
describe("fn_task_done with paused state (FN-3964 / FN-4167 regression)", () => {
it("advances todo + paused tasks through normal completion handoff", async () => {
const store = createMockStore();
let capturedCustomTools: any[] = [];
let taskDoneResult: any;
const todoTask = {
id: "FN-001",
title: "Paused todo task",
@@ -2165,7 +2172,7 @@ describe("TaskExecutor global pause behavior", () => {
prompt: vi.fn().mockImplementation(async () => {
const taskDoneTool = capturedCustomTools.find((tool: any) => tool.name === "fn_task_done");
if (taskDoneTool) {
await taskDoneTool.execute("call-1", { summary: "done" });
taskDoneResult = await taskDoneTool.execute("call-1", { summary: "done" });
}
}),
dispose: vi.fn(),
@@ -2178,9 +2185,10 @@ describe("TaskExecutor global pause behavior", () => {
await executor.execute(todoTask as any);
// FN-4145: explicit agent completion always clears task-level pause state.
expect(store.updateTask).toHaveBeenCalledWith("FN-001", {
paused: false,
pausedByAgentId: null,
paused: undefined,
pausedByAgentId: undefined,
status: null,
});
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "in-progress");
@@ -2192,12 +2200,16 @@ describe("TaskExecutor global pause behavior", () => {
id === "FN-001" && action.includes("Completion handoff deferred — global pause active"),
),
).toBe(false);
expect(taskDoneResult.content[0].text).toBe(
"Task marked complete with summary. All steps done. Moving to in-review.",
);
// globalPause:true deferred behavior is intentionally covered by the test above.
});
it("completes in-progress + paused tasks after clearing task-level pause state", async () => {
const store = createMockStore();
let capturedCustomTools: any[] = [];
let taskDoneResult: any;
const inProgressTask = {
id: "FN-001",
title: "Paused in-progress task",
@@ -2231,7 +2243,7 @@ describe("TaskExecutor global pause behavior", () => {
prompt: vi.fn().mockImplementation(async () => {
const taskDoneTool = capturedCustomTools.find((tool: any) => tool.name === "fn_task_done");
if (taskDoneTool) {
await taskDoneTool.execute("call-1", { summary: "done" });
taskDoneResult = await taskDoneTool.execute("call-1", { summary: "done" });
}
}),
dispose: vi.fn(),
@@ -2245,8 +2257,8 @@ describe("TaskExecutor global pause behavior", () => {
await executor.execute(inProgressTask as any);
expect(store.updateTask).toHaveBeenCalledWith("FN-001", {
paused: false,
pausedByAgentId: null,
paused: undefined,
pausedByAgentId: undefined,
status: null,
});
expect(watchdogSpy).toHaveBeenCalledWith("FN-001", "fn_task_done");
@@ -2259,6 +2271,9 @@ describe("TaskExecutor global pause behavior", () => {
id === "FN-001" && action.includes("Completion handoff deferred — global pause active"),
),
).toBe(false);
expect(taskDoneResult.content[0].text).toBe(
"Task marked complete with summary. All steps done. Moving to in-review.",
);
// globalPause:true deferred behavior is intentionally covered by
// "parks todo tasks in in-progress when fn_task_done is called during global pause".
});

View File

@@ -133,7 +133,10 @@ describe("TaskExecutor enginePaused soft pause (no agent termination)", () => {
"Task marked complete with summary. All steps done. Moving to in-review.",
);
expect(watchdogSpy).toHaveBeenCalledWith("FN-001", "fn_task_done");
expect(store.updateTask).toHaveBeenCalledWith("FN-001", expect.objectContaining({ paused: false, status: null }));
expect(store.updateTask).toHaveBeenCalledWith(
"FN-001",
expect.objectContaining({ paused: undefined, pausedByAgentId: undefined, status: null }),
);
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "in-review");
});

View File

@@ -4595,7 +4595,14 @@ export class TaskExecutor {
}
const settings = await store.getSettings();
const hardPauseActive = Boolean(settings.globalPause);
await store.updateTask(taskId, { paused: false, pausedByAgentId: null, status: null });
// Task-level pause prevents new work from starting, not completion of
// in-flight work. Always clear it on explicit agent completion so the
// board cannot strand a completed task in a paused state.
await store.updateTask(taskId, {
paused: undefined,
pausedByAgentId: undefined,
status: null,
});
await store.logEntry(taskId, "Task marked done by agent");
const latestTask = await store.getTask(taskId);