diff --git a/.changeset/fn-7224-retry-clears-workflow-pins.md b/.changeset/fn-7224-retry-clears-workflow-pins.md new file mode 100644 index 0000000000..e8c6278522 --- /dev/null +++ b/.changeset/fn-7224-retry-clears-workflow-pins.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Make dashboard retry clear stale workflow step pins before re-execution. +category: fix +dev: Clears persisted workflow step instances on manual execution retry so parse-steps can repin the current plan. diff --git a/packages/dashboard/src/__tests__/routes-tasks-ops.test.ts b/packages/dashboard/src/__tests__/routes-tasks-ops.test.ts index 1d14f739eb..93c76052a0 100644 --- a/packages/dashboard/src/__tests__/routes-tasks-ops.test.ts +++ b/packages/dashboard/src/__tests__/routes-tasks-ops.test.ts @@ -216,6 +216,7 @@ function createMockStore(overrides: Partial = {}): TaskStore { getWorkflowStep: vi.fn(), updateWorkflowStep: vi.fn(), deleteWorkflowStep: vi.fn(), + clearWorkflowRunStepInstances: vi.fn(), getMissionStore: vi.fn().mockReturnValue({ listMissions: vi.fn().mockReturnValue([]), createMission: vi.fn(), @@ -808,6 +809,7 @@ describe("POST /tasks/:id/retry", () => { baseCommitSha: null, ...buildManualRetryResetPatch({ resetMergeRetries: true }), }); + expect(store.clearWorkflowRunStepInstances).toHaveBeenCalledWith("KB-001"); expect(store.moveTask).toHaveBeenCalledWith("KB-001", "todo"); }); @@ -896,6 +898,7 @@ describe("POST /tasks/:id/retry", () => { error: null, ...buildManualRetryResetPatch(), }); + expect(store.clearWorkflowRunStepInstances).toHaveBeenCalledWith("KB-001"); expect(store.moveTask).toHaveBeenCalledWith("KB-001", "todo", { preserveProgress: true }); expect(store.logEntry).toHaveBeenCalledWith( "KB-001", @@ -927,6 +930,7 @@ describe("POST /tasks/:id/retry", () => { error: null, ...buildManualRetryResetPatch(), }); + expect(store.clearWorkflowRunStepInstances).toHaveBeenCalledWith("KB-001"); expect(store.moveTask).toHaveBeenCalledWith("KB-001", "todo", { preserveProgress: true }); expect(store.logEntry).toHaveBeenCalledWith( "KB-001", @@ -963,6 +967,7 @@ describe("POST /tasks/:id/retry", () => { error: null, ...buildManualRetryResetPatch(), }); + expect(store.clearWorkflowRunStepInstances).toHaveBeenCalledWith("KB-001"); expect(store.moveTask).toHaveBeenCalledWith("KB-001", "todo", { preserveProgress: true }); expect(store.logEntry).toHaveBeenCalledWith( "KB-001", diff --git a/packages/dashboard/src/routes/register-task-workflow-routes.ts b/packages/dashboard/src/routes/register-task-workflow-routes.ts index bc59972961..fb762a7a5d 100644 --- a/packages/dashboard/src/routes/register-task-workflow-routes.ts +++ b/packages/dashboard/src/routes/register-task-workflow-routes.ts @@ -1683,6 +1683,11 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork // and merge failures (all steps done). if (isInReviewRetry) { if (isExecutionFailureInReview) { + /* + FNXC:WorkflowRetry 2026-06-29-02:18: + Dashboard retry for an in-review execution failure re-enters the workflow graph from parse/execution, so it must clear persisted foreach step-instance pins. Otherwise a stale pin from the failed run makes the retry hit the same parse pin-mismatch immediately. + */ + clearRebuiltSpecWorkflowPins(scopedStore, req.params.id); await scopedStore.updateTask(req.params.id, { status: null, error: null, @@ -1734,6 +1739,12 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork return; } + /* + FNXC:WorkflowRetry 2026-06-29-02:18: + Non-planning manual retry is also a fresh execution boundary. Clear graph step-instance rows before moving back to todo so parse-steps can repin the current PROMPT.md instead of inheriting failed foreach state. + */ + clearRebuiltSpecWorkflowPins(scopedStore, req.params.id); + // Reset steps if the branch has no unique commits (work was lost with worktree) const completedSteps = task.steps.filter( (s: { status: string }) => s.status === "done" || s.status === "in-progress",