fix(FN-7224): clear stale graph pins on reset
This commit is contained in:
@@ -67,6 +67,9 @@ 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.
|
||||
|
||||
FNXC:WorkflowReset 2026-06-29-10:02:
|
||||
User reset/retry is also a hard graph-run boundary. Clear all persisted foreach step-instance rows for the task, not only rows outside a keep-run id, because stale rows can be written by an old aborting graph after the first cleanup and then make the next parse fail immediately.
|
||||
*/
|
||||
const maybeStore = store as unknown as {
|
||||
clearWorkflowRunStepInstances?: (taskId: string) => void;
|
||||
@@ -1797,6 +1800,10 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
||||
|
||||
const task = await scopedStore.getTask(req.params.id);
|
||||
|
||||
engine?.clearTaskPauseAbortState?.(req.params.id);
|
||||
await releaseExecutionAgentBindings(engine, req.params.id);
|
||||
clearRebuiltSpecWorkflowPins(scopedStore, req.params.id);
|
||||
|
||||
// Reset all steps to pending
|
||||
for (let i = 0; i < task.steps.length; i++) {
|
||||
if (task.steps[i].status !== "pending") {
|
||||
@@ -1812,7 +1819,7 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
||||
);
|
||||
|
||||
await scopedStore.moveTask(req.params.id, "todo");
|
||||
await releaseExecutionAgentBindings(engine, req.params.id);
|
||||
clearRebuiltSpecWorkflowPins(scopedStore, req.params.id);
|
||||
let updated = await scopedStore.getTask(req.params.id);
|
||||
if (!updated) {
|
||||
throw notFound(`Task ${req.params.id} not found after reset`);
|
||||
|
||||
@@ -7666,6 +7666,9 @@ export class TaskExecutor {
|
||||
await this.persistTokenUsage(task.id);
|
||||
return;
|
||||
}
|
||||
if (failedNode === "parse" && failureValue === "pin-mismatch" && await this.routeResetParsePinMismatchToRetry(live)) {
|
||||
return;
|
||||
}
|
||||
if (live.column !== "in-progress") {
|
||||
const benignMessage = `Workflow graph run ended after task already advanced to '${live.column}' — no further action needed`;
|
||||
executorLog.log(`${task.id}: ${benignMessage}`);
|
||||
@@ -7727,6 +7730,45 @@ export class TaskExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
private async routeResetParsePinMismatchToRetry(live: TaskDetail): Promise<boolean> {
|
||||
/*
|
||||
FNXC:WorkflowReset 2026-06-29-10:04:
|
||||
A user reset/retry can race an aborting graph-owned foreach instance that persists after the route cleared pins. If the next run reaches parse and sees only stale foreach pins while the task has no implementation progress, recover by deleting all graph instance rows and requeueing to todo. Do not hand the task to in-review, because parse has not executed work or produced mergeable output.
|
||||
*/
|
||||
if (live.deletedAt) return false;
|
||||
if (live.paused || live.userPaused === true) return false;
|
||||
if (live.column === "done" || live.column === "archived") return false;
|
||||
const hasImplementationProgress =
|
||||
(live.currentStep ?? 0) > 0
|
||||
|| (live.steps ?? []).some((step) => step.status === "done" || step.status === "in-progress" || step.status === "skipped");
|
||||
if (hasImplementationProgress) return false;
|
||||
|
||||
const maybeStore = this.store as unknown as {
|
||||
clearWorkflowRunStepInstances?: (taskId: string) => void;
|
||||
clearWorkflowRunBranches?: (taskId: string, keepRunId: string) => void;
|
||||
};
|
||||
try {
|
||||
maybeStore.clearWorkflowRunStepInstances?.(live.id);
|
||||
} catch {
|
||||
// Legacy stores may not persist graph step instances.
|
||||
}
|
||||
this.clearPausedAborted(live.id);
|
||||
this.activeWorktrees.delete(live.id);
|
||||
await this.store.updateTask(live.id, {
|
||||
status: null,
|
||||
error: null,
|
||||
graphResumeRetryCount: 0,
|
||||
}, this.getRunContextFor(live.id));
|
||||
if (live.column !== "todo") {
|
||||
await this.store.moveTask(live.id, "todo", { preserveProgress: false });
|
||||
}
|
||||
const message = "Auto-recovered: cleared stale workflow parse pins after reset/retry — task requeued before execution";
|
||||
executorLog.warn(`${live.id}: ${message}`);
|
||||
await this.store.logEntry(live.id, message, undefined, this.getRunContextFor(live.id));
|
||||
await this.persistTokenUsage(live.id);
|
||||
return true;
|
||||
}
|
||||
|
||||
private async maybeDispatchWorkflowWorkEngine(task: Task): Promise<boolean> {
|
||||
let detail: TaskDetail;
|
||||
let workflow: WorkflowIr;
|
||||
|
||||
Reference in New Issue
Block a user