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(),
|
getWorkflowStep: vi.fn(),
|
||||||
updateWorkflowStep: vi.fn(),
|
updateWorkflowStep: vi.fn(),
|
||||||
deleteWorkflowStep: vi.fn(),
|
deleteWorkflowStep: vi.fn(),
|
||||||
|
clearWorkflowRunStepInstances: vi.fn(),
|
||||||
getMissionStore: vi.fn().mockReturnValue({
|
getMissionStore: vi.fn().mockReturnValue({
|
||||||
listMissions: vi.fn().mockReturnValue([]),
|
listMissions: vi.fn().mockReturnValue([]),
|
||||||
createMission: vi.fn(),
|
createMission: vi.fn(),
|
||||||
@@ -1821,6 +1822,7 @@ describe("POST /tasks/:id/spec/rebuild", () => {
|
|||||||
"Specification rebuild requested by user"
|
"Specification rebuild requested by user"
|
||||||
);
|
);
|
||||||
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "triage");
|
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(existsSync(join(taskDir, "PROMPT.md"))).toBe(false);
|
||||||
expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: "needs-replan" });
|
expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: "needs-replan" });
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1840,6 +1842,7 @@ describe("POST /tasks/:id/spec/rebuild", () => {
|
|||||||
|
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "triage");
|
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" });
|
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(res.status).toBe(200);
|
||||||
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "triage");
|
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 () => {
|
it("allows rebuild for task already in triage", async () => {
|
||||||
@@ -1880,6 +1884,7 @@ describe("POST /tasks/:id/spec/rebuild", () => {
|
|||||||
"Specification rebuild requested by user"
|
"Specification rebuild requested by user"
|
||||||
);
|
);
|
||||||
expect(store.moveTask).not.toHaveBeenCalled();
|
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(existsSync(join(taskDir, "PROMPT.md"))).toBe(false);
|
||||||
expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: "needs-replan" });
|
expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: "needs-replan" });
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1898,6 +1903,7 @@ describe("POST /tasks/:id/spec/rebuild", () => {
|
|||||||
|
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "triage");
|
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" });
|
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 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.";
|
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 {
|
function isArtifactType(value: string): value is ArtifactType {
|
||||||
return ARTIFACT_TYPES.has(value as ArtifactType);
|
return ARTIFACT_TYPES.has(value as ArtifactType);
|
||||||
}
|
}
|
||||||
@@ -3071,6 +3086,7 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
|||||||
if (task.column === "triage") {
|
if (task.column === "triage") {
|
||||||
// Log the rebuild request
|
// Log the rebuild request
|
||||||
await scopedStore.logEntry(task.id, "Specification rebuild requested by user");
|
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
|
// Remove the existing spec so rebuilds produce a fresh PROMPT.md instead
|
||||||
// of asking triage to revise whatever was already on disk.
|
// 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
|
// Log the rebuild request
|
||||||
await scopedStore.logEntry(task.id, "Specification rebuild requested by user");
|
await scopedStore.logEntry(task.id, "Specification rebuild requested by user");
|
||||||
|
clearRebuiltSpecWorkflowPins(scopedStore, task.id);
|
||||||
|
|
||||||
// Move to triage for replanning
|
// Move to triage for replanning
|
||||||
const updated = await scopedStore.moveTask(task.id, "triage");
|
const updated = await scopedStore.moveTask(task.id, "triage");
|
||||||
|
|||||||
@@ -2636,6 +2636,21 @@ export class TriageProcessor {
|
|||||||
return;
|
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");
|
await this.store.moveTask(task.id, "todo");
|
||||||
|
|
||||||
if (shouldApplyPromptDeclaredTitle && promptDeclaredTitle) {
|
if (shouldApplyPromptDeclaredTitle && promptDeclaredTitle) {
|
||||||
|
|||||||
Reference in New Issue
Block a user