fix(FN-7221): clear stale workflow pins on spec rebuild
This commit is contained in:
7
.changeset/fn-7221-clear-rebuilt-workflow-pins.md
Normal file
7
.changeset/fn-7221-clear-rebuilt-workflow-pins.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Prevent rebuilt stepwise workflow tasks from failing at parse on stale step pins.
|
||||
category: fix
|
||||
dev: Spec rebuild and AI replan handoff now clear persisted workflow foreach instances before reparsing PROMPT.md.
|
||||
@@ -213,6 +213,7 @@ function createMockStore(overrides: Partial<TaskStore> = {}): TaskStore {
|
||||
getWorkflowStep: vi.fn(),
|
||||
updateWorkflowStep: vi.fn(),
|
||||
deleteWorkflowStep: vi.fn(),
|
||||
clearWorkflowRunStepInstances: vi.fn(),
|
||||
getMissionStore: vi.fn().mockReturnValue({
|
||||
listMissions: vi.fn().mockReturnValue([]),
|
||||
createMission: vi.fn(),
|
||||
@@ -1821,6 +1822,7 @@ describe("POST /tasks/:id/spec/rebuild", () => {
|
||||
"Specification rebuild requested by user"
|
||||
);
|
||||
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "triage");
|
||||
expect((store as unknown as { clearWorkflowRunStepInstances: ReturnType<typeof vi.fn> }).clearWorkflowRunStepInstances).toHaveBeenCalledWith("FN-001");
|
||||
expect(existsSync(join(taskDir, "PROMPT.md"))).toBe(false);
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: "needs-replan" });
|
||||
} finally {
|
||||
@@ -1840,6 +1842,7 @@ describe("POST /tasks/:id/spec/rebuild", () => {
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "triage");
|
||||
expect((store as unknown as { clearWorkflowRunStepInstances: ReturnType<typeof vi.fn> }).clearWorkflowRunStepInstances).toHaveBeenCalledWith("FN-001");
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: "needs-replan" });
|
||||
});
|
||||
|
||||
@@ -1855,6 +1858,7 @@ describe("POST /tasks/:id/spec/rebuild", () => {
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "triage");
|
||||
expect((store as unknown as { clearWorkflowRunStepInstances: ReturnType<typeof vi.fn> }).clearWorkflowRunStepInstances).toHaveBeenCalledWith("FN-001");
|
||||
});
|
||||
|
||||
it("allows rebuild for task already in triage", async () => {
|
||||
@@ -1880,6 +1884,7 @@ describe("POST /tasks/:id/spec/rebuild", () => {
|
||||
"Specification rebuild requested by user"
|
||||
);
|
||||
expect(store.moveTask).not.toHaveBeenCalled();
|
||||
expect((store as unknown as { clearWorkflowRunStepInstances: ReturnType<typeof vi.fn> }).clearWorkflowRunStepInstances).toHaveBeenCalledWith("FN-001");
|
||||
expect(existsSync(join(taskDir, "PROMPT.md"))).toBe(false);
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: "needs-replan" });
|
||||
} finally {
|
||||
@@ -1898,6 +1903,7 @@ describe("POST /tasks/:id/spec/rebuild", () => {
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "triage");
|
||||
expect((store as unknown as { clearWorkflowRunStepInstances: ReturnType<typeof vi.fn> }).clearWorkflowRunStepInstances).toHaveBeenCalledWith("FN-001");
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: "needs-replan" });
|
||||
});
|
||||
|
||||
|
||||
@@ -63,6 +63,21 @@ const DUPLICATE_STOPWORDS = new Set(["a", "an", "the", "and", "or", "of", "to",
|
||||
const ARTIFACT_TYPES = new Set<ArtifactType>(["document", "image", "video", "audio", "other"]);
|
||||
const ADDRESS_PR_FEEDBACK_PROMPT = "Run /ce-resolve-pr-feedback to resolve open PR review feedback: evaluate each thread, fix valid issues, and reply.";
|
||||
|
||||
function clearRebuiltSpecWorkflowPins(store: TaskStore, taskId: string): void {
|
||||
/*
|
||||
FNXC:WorkflowReplan 2026-06-29-00:33:
|
||||
Spec rebuild intentionally invalidates the planned step source, so persisted graph foreach pins from the previous PROMPT.md must be cleared before the next parse-steps node runs. Keeping the stale pins makes rebuilt tasks fail closed with pin-mismatch at parse instead of executing the fresh plan.
|
||||
*/
|
||||
const maybeStore = store as unknown as {
|
||||
clearWorkflowRunStepInstances?: (taskId: string) => void;
|
||||
};
|
||||
try {
|
||||
maybeStore.clearWorkflowRunStepInstances?.(taskId);
|
||||
} catch {
|
||||
// Legacy stores may not have workflow-run instance persistence; rebuild must still proceed.
|
||||
}
|
||||
}
|
||||
|
||||
function isArtifactType(value: string): value is ArtifactType {
|
||||
return ARTIFACT_TYPES.has(value as ArtifactType);
|
||||
}
|
||||
@@ -3071,6 +3086,7 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
||||
if (task.column === "triage") {
|
||||
// Log the rebuild request
|
||||
await scopedStore.logEntry(task.id, "Specification rebuild requested by user");
|
||||
clearRebuiltSpecWorkflowPins(scopedStore, task.id);
|
||||
|
||||
// Remove the existing spec so rebuilds produce a fresh PROMPT.md instead
|
||||
// of asking triage to revise whatever was already on disk.
|
||||
@@ -3099,6 +3115,7 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
||||
|
||||
// Log the rebuild request
|
||||
await scopedStore.logEntry(task.id, "Specification rebuild requested by user");
|
||||
clearRebuiltSpecWorkflowPins(scopedStore, task.id);
|
||||
|
||||
// Move to triage for replanning
|
||||
const updated = await scopedStore.moveTask(task.id, "triage");
|
||||
|
||||
@@ -2636,6 +2636,21 @@ export class TriageProcessor {
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.isReplan) {
|
||||
/*
|
||||
FNXC:WorkflowReplan 2026-06-29-00:33:
|
||||
AI spec revision replaces the task's step-source PROMPT.md, so graph foreach instance pins from the previous plan must be discarded before execution reparses steps. Otherwise rebuilt tasks can fail at parse with a stale pin-mismatch even though the new plan is valid.
|
||||
*/
|
||||
const maybeStore = this.store as unknown as {
|
||||
clearWorkflowRunStepInstances?: (taskId: string) => void;
|
||||
};
|
||||
try {
|
||||
maybeStore.clearWorkflowRunStepInstances?.(task.id);
|
||||
} catch {
|
||||
// Older stores may not persist graph step instances; replanning remains valid without cleanup.
|
||||
}
|
||||
}
|
||||
|
||||
await this.store.moveTask(task.id, "todo");
|
||||
|
||||
if (shouldApplyPromptDeclaredTitle && promptDeclaredTitle) {
|
||||
|
||||
Reference in New Issue
Block a user