diff --git a/.changeset/preserve-needs-replan-adoption.md b/.changeset/preserve-needs-replan-adoption.md new file mode 100644 index 0000000000..3fb855ea4e --- /dev/null +++ b/.changeset/preserve-needs-replan-adoption.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix engine restarts stranding replan-loop tasks in To Do by clearing their needs-replan signal. +category: fix +dev: The KTD-8 legacy-adoption table now maps `needs-replan` to `preserve` instead of `resume-graph`; it is a live graph signal written by the plan-replan seam and consumed by triage todo-rediscovery, not un-migrated legacy state. diff --git a/packages/core/src/__tests__/legacy-adoption.test.ts b/packages/core/src/__tests__/legacy-adoption.test.ts index e166870ba4..b70197931e 100644 --- a/packages/core/src/__tests__/legacy-adoption.test.ts +++ b/packages/core/src/__tests__/legacy-adoption.test.ts @@ -121,11 +121,21 @@ describe("KTD-8 adoption table — write-site census completeness (build-failing describe("resolveLegacyStatusAdoption — every legacy (status) resumes owned", () => { it("triage plan-review statuses resume the graph (writers deleted in U3)", () => { - for (const s of ["planning", "needs-replan", "plan-review-unavailable"]) { + for (const s of ["planning", "plan-review-unavailable"]) { expect(resolveLegacyStatusAdoption(s)?.kind).toBe("resume-graph"); } }); + /* + FNXC:LegacyAdoption 2026-07-22-15:55 (FN-8498 incident): + needs-replan is written LIVE by the graph's plan-replan seam and is the exact key + triage's todo-rediscovery uses to re-admit a planned todo task. Adoption must never + clear it — the resume-graph mapping stranded FN-8498 in `todo` across a restart. + */ + it("needs-replan is preserved — it is the graph's live replan signal, not legacy", () => { + expect(resolveLegacyStatusAdoption("needs-replan")?.kind).toBe("preserve"); + }); + it("live human/terminal gates are preserved (never disturbed)", () => { for (const s of ["awaiting-approval", "failed", "error", "blocked", "done", "cancelled"]) { expect(resolveLegacyStatusAdoption(s)?.kind).toBe("preserve"); @@ -184,7 +194,7 @@ describe("planLegacyAdoption (U9b consumers)", () => { const NOW = "2026-07-19T04:40:00.000Z"; it("clears every resume-graph status so the graph re-enters at its owning node", () => { - for (const status of ["planning", "needs-replan", "plan-review-unavailable", "queued", "triaged"]) { + for (const status of ["planning", "plan-review-unavailable", "queued", "triaged"]) { const plan = planLegacyAdoption({ status }, NOW); expect(plan.action, status).toBe("resume-graph"); // Clearing the legacy status IS the re-entry: the graph owns the node again. @@ -210,6 +220,20 @@ describe("planLegacyAdoption (U9b consumers)", () => { } }); + /* + FNXC:LegacyAdoption 2026-07-22-15:55 (FN-8498 incident): + A post-cutover todo row in the plan-replan loop carries status "needs-replan" and no + legacyAdoptedAt stamp. The startup sweep used to clear it (resume-graph), stranding the + task: triage's todo-rediscovery only re-admits a planned todo task on that exact status. + The plan must skip it entirely — no status clear, no patch, no stamp — so the replan + signal survives any number of engine restarts. + */ + it("survives a restart mid-replan-loop: needs-replan is skipped, never cleared (FN-8498)", () => { + const plan = planLegacyAdoption({ status: "needs-replan" }, NOW); + expect(plan.action).toBe("skip"); + expect(plan.patch).toBeUndefined(); + }); + it("backfills reviewLevel-only rows and never writes both fields", () => { const plan = planLegacyAdoption({ reviewLevel: 1 }, NOW); expect(plan.patch?.enabledWorkflowSteps).toEqual([CODE_REVIEW_GROUP_ID]); diff --git a/packages/core/src/legacy-adoption.ts b/packages/core/src/legacy-adoption.ts index c899857ceb..c88cdb05a4 100644 --- a/packages/core/src/legacy-adoption.ts +++ b/packages/core/src/legacy-adoption.ts @@ -11,8 +11,9 @@ cutover window fails the build instead of mass-parking rows `paused` at upgrade. Adoption action per legacy status (KTD-8), for the FOUNDATIONAL targets (U9 scope A): - resume-graph : clear the legacy triage-owned status so the graph re-enters cleanly at the owning node (planning → planning node, - needs-replan → plan-replan, plan-review-unavailable → - plan-review retry, queued/triaged → scheduler re-pickup). + plan-review-unavailable → plan-review retry, + queued/triaged → scheduler re-pickup). NOT needs-replan — + that is a LIVE graph signal, preserved (see its table row). - preserve : a live human/terminal gate the graph must NOT disturb (awaiting-approval, awaiting-user-input, failed, error, blocked, done, cancelled). Pausing is NOT a status: it is @@ -56,7 +57,16 @@ export interface LegacyAdoptionAction { export const LEGACY_STATUS_ADOPTION: Readonly> = { // ── Triage plan-review statuses whose writers U3 deleted → graph re-entry ── "planning": { kind: "resume-graph", note: "re-enter planning node" }, - "needs-replan": { kind: "resume-graph", note: "re-enter plan-replan node" }, + /* + FNXC:LegacyAdoption 2026-07-22-15:55 (FN-8498 incident): + `needs-replan` is NOT legacy — post-U3 it is written live by the graph's own plan-replan + seam (executor.ts / scheduler.ts) and CONSUMED by triage's todo-rediscovery, which only + re-admits a planned todo task when status === "needs-replan". The original resume-graph + mapping cleared it on every engine restart, deleting the exact signal its consumer keys + on and stranding replan-loop tasks in `todo` forever (FN-8498 sat "ready" for 80 minutes + after a restart). Preserve it: the status is self-resuming — triage picks it up as-is. + */ + "needs-replan": { kind: "preserve", note: "live graph replan signal — triage todo-rediscovery consumes it" }, "plan-review-unavailable": { kind: "resume-graph", note: "plan-review retry (leased)" }, // ── Scheduler / dispatch transient states → re-pickup ───────────────────── "queued": { kind: "resume-graph", note: "scheduler re-queue" },