fix(FN-954): allow retry for stuck-killed tasks across all interfaces

- Extend retry API to accept 'stuck-killed' as a retryable status alongside 'failed'
- Update CLI retry command to recognize stuck-killed tasks
- Update pi extension retry tool to handle stuck-killed tasks
- Show retry button in TaskDetailModal for stuck-killed task status
- Add changeset for patch release
This commit is contained in:
gsxdsm
2026-04-05 00:41:15 -07:00
parent fa379f3091
commit 7f3a100202
7 changed files with 61 additions and 14 deletions

View File

@@ -1243,7 +1243,7 @@ export function TaskDetailModal({
Refine
</button>
)}
{task.status === "failed" && onRetryTask && (
{(task.status === "failed" || task.status === "stuck-killed") && onRetryTask && (
<button className="btn btn-warning btn-sm" onClick={handleRetry}>
Retry
</button>

View File

@@ -704,7 +704,7 @@ describe("POST /tasks/:id/retry", () => {
expect(store.moveTask).toHaveBeenCalledWith("KB-001", "todo");
});
it("returns 400 when task is not in failed state", async () => {
it("returns 400 when task is not in a retryable state", async () => {
const activeTask = { ...FAKE_TASK_DETAIL, status: "executing" };
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(activeTask);
@@ -713,7 +713,7 @@ describe("POST /tasks/:id/retry", () => {
});
expect(res.status).toBe(400);
expect(res.body.error).toContain("not in a failed state");
expect(res.body.error).toContain("not in a retryable state");
});
it("retries a failed task in any column (not just in-progress)", async () => {
@@ -731,6 +731,23 @@ describe("POST /tasks/:id/retry", () => {
expect(store.updateTask).toHaveBeenCalledWith("KB-001", { status: undefined, error: undefined });
expect(store.moveTask).toHaveBeenCalledWith("KB-001", "todo");
});
it("retries a stuck-killed task and moves it to todo", async () => {
const stuckTask = { ...FAKE_TASK_DETAIL, status: "stuck-killed", column: "in-progress" };
const movedTask = { ...FAKE_TASK_DETAIL, column: "todo", status: undefined };
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(stuckTask);
(store.updateTask as ReturnType<typeof vi.fn>).mockResolvedValue(stuckTask);
(store.moveTask as ReturnType<typeof vi.fn>).mockResolvedValue(movedTask);
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/retry", JSON.stringify({}), {
"Content-Type": "application/json",
});
expect(res.status).toBe(200);
expect(store.updateTask).toHaveBeenCalledWith("KB-001", { status: undefined, error: undefined });
expect(store.moveTask).toHaveBeenCalledWith("KB-001", "todo");
expect(store.logEntry).toHaveBeenCalledWith("KB-001", "Retry requested from dashboard");
});
});
describe("POST /tasks/:id/duplicate", () => {

View File

@@ -1784,13 +1784,13 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
}
});
// Retry failed task
// Retry failed or stuck-killed task
router.post("/tasks/:id/retry", async (req, res) => {
try {
const scopedStore = await getScopedStore(req);
const task = await scopedStore.getTask(req.params.id);
if (task.status !== "failed") {
res.status(400).json({ error: "Task is not in a failed state" });
if (task.status !== "failed" && task.status !== "stuck-killed") {
res.status(400).json({ error: `Task is not in a retryable state (current status: ${task.status || 'none'})` });
return;
}
await scopedStore.updateTask(req.params.id, { status: undefined, error: undefined });