fix(FN-8498): stop legacy-adoption sweep from clearing live needs-replan signal

The KTD-8 startup adoption sweep mapped needs-replan to resume-graph, clearing
it on every engine restart. But needs-replan is not un-migrated legacy: post-U3
it is written live by the graph's plan-replan seam and is the exact status
triage's todo-rediscovery keys on to re-admit a planned todo task. Clearing it
stranded replan-loop tasks in todo forever (FN-8498 sat "ready" for 80 minutes
after a restart). Map it to preserve — the status is self-resuming as-is.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-07-22 15:58:10 -07:00
parent 9002fca9de
commit f389a64901
3 changed files with 46 additions and 5 deletions

View File

@@ -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.

View File

@@ -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]);

View File

@@ -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<Record<string, LegacyAdoptionAction>> = {
// ── 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" },