feat(FN-3185): preserve progress on task reset with explicit confirmation d

This merge implements a "preserve progress" option for task resets across the system. FN-3185 adds a `preserveProgress` flag to `moveTask` that keeps status/history when resetting tasks back to `todo`, with required explicit confirmation dialogs to prevent accidental resets. The feature is wired thr

Fusion-Task-Id: FN-3185
This commit is contained in:
Fusion
2026-05-02 10:17:56 -07:00
committed by gsxdsm
parent ec2b55252a
commit fce79cece0
18 changed files with 394 additions and 50 deletions

View File

@@ -6085,7 +6085,7 @@ describe("TaskExecutor usage limit detection", () => {
recoveryRetryCount: 1,
nextRecoveryAt: expect.any(String),
}));
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "todo");
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "todo", { preserveProgress: true });
expect(store.updateTask).not.toHaveBeenCalledWith(
"FN-001",
expect.objectContaining({ status: "failed" }),
@@ -6227,7 +6227,7 @@ describe("TaskExecutor bounded recovery retries", () => {
recoveryRetryCount: 1,
nextRecoveryAt: expect.any(String),
}));
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "todo");
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "todo", { preserveProgress: true });
expect(onError).not.toHaveBeenCalled();
// Second failure: count goes from 1 to 2
@@ -6250,7 +6250,7 @@ describe("TaskExecutor bounded recovery retries", () => {
expect(store.updateTask).toHaveBeenCalledWith("FN-001", expect.objectContaining({
recoveryRetryCount: 2,
}));
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "todo");
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "todo", { preserveProgress: true });
expect(onError).not.toHaveBeenCalled();
});
@@ -6487,7 +6487,7 @@ describe("TaskExecutor bounded recovery retries", () => {
});
// Exhausted no-fn_task_done retries now requeue immediately to todo.
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "todo");
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "todo", { preserveProgress: true });
});
});
@@ -7542,7 +7542,7 @@ describe("Workflow Steps Execution", () => {
error: "Agent finished without calling fn_task_done (after 3 retries)",
taskDoneRetryCount: 1,
});
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "todo");
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "todo", { preserveProgress: true });
expect(store.logEntry).toHaveBeenCalledWith(
"FN-001",
"Agent finished without calling fn_task_done (after 3 retries) — requeued to todo immediately (1/3)",
@@ -13359,7 +13359,7 @@ describe("TaskExecutor messaging tools", () => {
taskDoneRetryCount: 1,
}),
);
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "todo");
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "todo", { preserveProgress: true });
});
it("still checks completion blockers in fast mode", async () => {

View File

@@ -1099,7 +1099,7 @@ describe("SelfHealingManager", () => {
"FN-2164",
expect.stringContaining("Auto-retry 1/3"),
);
expect(store.moveTask).toHaveBeenCalledWith("FN-2164", "todo");
expect(store.moveTask).toHaveBeenCalledWith("FN-2164", "todo", { preserveProgress: true });
managerWithRecovery.stop();
});
@@ -1783,7 +1783,7 @@ describe("SelfHealingManager", () => {
"FN-1572",
expect.stringContaining("in-review task still had incomplete steps"),
);
expect(store.moveTask).toHaveBeenCalledWith("FN-1572", "todo");
expect(store.moveTask).toHaveBeenCalledWith("FN-1572", "todo", { preserveProgress: true });
managerWithRecovery.stop();
});
@@ -2237,7 +2237,7 @@ describe("SelfHealingManager", () => {
expect(result).toBe(1);
expect(store.updateTask).not.toHaveBeenCalled();
expect(store.moveTask).toHaveBeenCalledWith("FN-9003", "todo");
expect(store.moveTask).toHaveBeenCalledWith("FN-9003", "todo", { preserveProgress: true });
managerWithRecovery.stop();
});
@@ -2276,7 +2276,7 @@ describe("SelfHealingManager", () => {
"FN-200",
"Auto-recovered orphaned executor task — missing worktree/session, moved back to todo",
);
expect(store.moveTask).toHaveBeenCalledWith("FN-200", "todo");
expect(store.moveTask).toHaveBeenCalledWith("FN-200", "todo", { preserveProgress: true });
managerWithRecovery.stop();
});
@@ -2404,7 +2404,7 @@ describe("SelfHealingManager", () => {
"FN-210",
expect.stringContaining("worktree exists but no active session"),
);
expect(store.moveTask).toHaveBeenCalledWith("FN-210", "todo");
expect(store.moveTask).toHaveBeenCalledWith("FN-210", "todo", { preserveProgress: true });
managerWithRecovery.stop();
});

View File

@@ -2516,7 +2516,7 @@ export class TaskExecutor {
worktree: null,
branch: null,
});
await this.store.moveTask(task.id, "todo");
await this.store.moveTask(task.id, "todo", { preserveProgress: true });
stuckRequeue = null; // Prevent outer finally from re-processing
return;
}
@@ -3177,7 +3177,7 @@ export class TaskExecutor {
undefined,
this.currentRunContext,
);
await this.store.moveTask(task.id, "todo");
await this.store.moveTask(task.id, "todo", { preserveProgress: true });
executorLog.log(`${task.id} failed after ${MAX_TASK_DONE_SESSION_RETRIES} retries — requeued to todo (${nextRequeueCount}/${MAX_TASK_DONE_REQUEUE_RETRIES})`);
} else {
await this.store.updateTask(task.id, { status: "failed", error: errorMessage });
@@ -3429,7 +3429,7 @@ export class TaskExecutor {
worktree: null,
branch: null,
});
await this.store.moveTask(task.id, "todo");
await this.store.moveTask(task.id, "todo", { preserveProgress: true });
return;
}

View File

@@ -1008,7 +1008,7 @@ export class SelfHealingManager {
task.id,
"Auto-recovered: in-review task still had incomplete steps — moved back to todo for retry",
);
await this.store.moveTask(task.id, "todo");
await this.store.moveTask(task.id, "todo", { preserveProgress: true });
log.log(`Recovered stale incomplete review task ${task.id}: moved back to todo`);
recovered++;
} catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err);
@@ -1082,7 +1082,7 @@ export class SelfHealingManager {
task.id,
"Auto-recovered: in-review task idle past stuck-task timeout — kicked back to todo",
);
await this.store.moveTask(task.id, "todo");
await this.store.moveTask(task.id, "todo", { preserveProgress: true });
log.log(`Kicked ghost review task ${task.id} back to todo`);
recovered++;
} catch (err: unknown) {
@@ -1351,7 +1351,7 @@ export class SelfHealingManager {
task.id,
`Auto-recovered orphaned executor task — ${reason}, moved back to todo`,
);
await this.store.moveTask(task.id, "todo");
await this.store.moveTask(task.id, "todo", { preserveProgress: true });
recovered++;
} catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err);
log.error(`Failed to recover orphaned executor task ${task.id}: ${errorMessage}`);
@@ -1484,7 +1484,7 @@ export class SelfHealingManager {
task.id,
`Auto-retry ${nextCount}/${MAX_TASK_DONE_RETRIES}: agent finished without task_done — requeuing to todo to resume partial work`,
);
await this.store.moveTask(task.id, "todo");
await this.store.moveTask(task.id, "todo", { preserveProgress: true });
recovered++;
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);