From 3c531d984c7ea1c4ac0bd1ec428bf75e622159c4 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 31 Jul 2026 03:48:44 -0700 Subject: [PATCH] =?UTF-8?q?fix(engine):=20self-healing=20lane=20cluster=20?= =?UTF-8?q?round=202=20=E2=80=94=2038=20=E2=86=92=2026=20(two=20sweeps=20c?= =?UTF-8?q?ould=20disturb=20live=20work)=20(#3078)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The largest census cluster became **unclaimed again** when #3055 closed conflicting. I had closed my own #3050 an hour earlier expecting #3055 to land, so this re-applies the conversions #3047 and #3049 did not cover. **Re-applied from current main rather than rebasing the closed branch.** The conversions are small; the conflict archaeology is what went wrong last time — nine conflicts against #3049, several on variable names identical to mine, and my mechanical fixup corrupted the file badly enough that I aborted. Starting from main cost less than resolving that and carries no risk of resurrecting a stale line. ## Census | | before | after | |---|---|---| | `packages/engine/src/self-healing.ts` | **38** | **26** | | repo-wide column guards | 84 | **72** | ## Three sweeps, existing role helpers only | sweep | roles | what it did on a renamed board | |---|---|---| | worktree metadata | terminal + wip + review | rebound finished cards every pass, **and the FN-5256 liveness guard went silent** | | orphaned pending step results | wip | **could rewrite `pending` results under a live executor run** | | agent-link drift | wip + review + terminal | evaluated agents whose task was plainly still executing | Two of these disturb **live** work, which is why they were worth redoing now rather than leaving for the next fleet round: - The worktree-metadata sweep clears `worktree`/`branch` metadata. Its liveness guard is the thing standing between that and a running shell (FN-5256). Keyed on ids, it matched nothing on a renamed board. The scope-override safety condition beside it now reads the **same resolved sets**, so the two cannot disagree about which lanes are live — previously they were two independent literal lists. - The orphaned-step-results sweep's own header says it must never touch an executor-owned row. The id-keyed skip made it do exactly that. Resolved once per sweep, outside the paging loop, so a large board still pays one resolve. ## Flagged, not guessed — the 26 that remain Unchanged from my earlier audit and re-verified on this base: - **Sync predicates** (`isWorkspaceOwnerLive`, the pause-abort classifier, the phantom-binding check, the `task:moved` listener guards). No store handle; converting means a signature change or making a synchronous event listener async, which reorders handlers against a synchronous emitter. - **Already-converted fallbacks** — `own.length > 0 ? own.includes(...) : task.column === "in-review"`. The resolved answer wins; the literal is the documented no-metadata path. - **The notification-route `fresh.column === "todo"` sites** — measured previously: any `await` before the wedge resolve drops an operator notification. Needs the wedge-episode contract, not a column pass. ## Verification self-healing suites **204 passed** · agent-link-drift + query-filter-blindness **83 passed** · `pnpm test:gate` 161 + 13 + 487 + 71 · lint · census `--strict` · lane-wiring — green. --- .changeset/self-healing-cluster-round2.md | 7 ++++ packages/engine/src/self-healing.ts | 41 +++++++++++++++---- .../lib/lifecycle-column-census-baseline.json | 2 +- 3 files changed, 41 insertions(+), 9 deletions(-) create mode 100644 .changeset/self-healing-cluster-round2.md diff --git a/.changeset/self-healing-cluster-round2.md b/.changeset/self-healing-cluster-round2.md new file mode 100644 index 0000000000..67675476b9 --- /dev/null +++ b/.changeset/self-healing-cluster-round2.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Three more self-healing repairs use your board's own column names, including one that could disturb a running task. +category: fix +dev: Converts the worktree-metadata reconcile, orphaned-pending-step-results, and agent-link-drift sweeps in `self-healing.ts` to `resolveProjectColumnsForRoles`. diff --git a/packages/engine/src/self-healing.ts b/packages/engine/src/self-healing.ts index cf43f1f8ca..f65fa1cd94 100644 --- a/packages/engine/src/self-healing.ts +++ b/packages/engine/src/self-healing.ts @@ -5359,9 +5359,19 @@ export class SelfHealingManager extends SelfHealingGitEvidence { } let repaired = 0; + /* + FNXC:WorkflowResolvedColumns 2026-07-31-16:40 (fleet, self-healing round 2): + TERMINAL/WIP/REVIEW roles for this sweep's three lane questions. Keyed on ids the terminal skip + never fired on a renamed board (finished cards were rebound every pass) and — the dangerous half — + the FN-5256 liveness guard below went silent, so the sweep could clear worktree metadata out from + under a running shell. + */ + const worktreeReconcileTerminalColumns = await resolveProjectColumnsForRoles(this.store, TERMINAL_ROLES); + const worktreeReconcileWipColumns = await resolveProjectColumnsForRoles(this.store, ["countsTowardWip"]); + const worktreeReconcileReviewColumns = await resolveProjectColumnsForRoles(this.store, REVIEW_ROLES); for (const task of allTasks) { if (!task.worktree) continue; - if (!options?.includeTaskIds?.has(task.id) && (task.column === "done" || task.column === "archived")) { + if (!options?.includeTaskIds?.has(task.id) && worktreeReconcileTerminalColumns.has(task.column)) { continue; } @@ -5398,8 +5408,9 @@ export class SelfHealingManager extends SelfHealingGitEvidence { const scopeOverrideMergeActiveSafe = task.scopeOverride === true - && task.column !== "in-progress" - && (task.column !== "in-review" || (typeof task.status === "string" && RECONCILE_SCOPE_OVERRIDE_MERGE_ACTIVE_STATUS_SET.has(task.status))); + /* Same resolved sets as the liveness guard below, so the two cannot disagree about live lanes. */ + && !worktreeReconcileWipColumns.has(task.column) + && (!worktreeReconcileReviewColumns.has(task.column) || (typeof task.status === "string" && RECONCILE_SCOPE_OVERRIDE_MERGE_ACTIVE_STATUS_SET.has(task.status))); if (scopeOverrideMergeActiveSafe) { /* FNXC:MissingWorktreeRecovery 2026-07-10-18:23: @@ -5427,7 +5438,7 @@ export class SelfHealingManager extends SelfHealingGitEvidence { // live task's worktree looks stale here (and we couldn't rebind to a live // fusion/), the executor's own recovery paths will detect and recreate // it. Clearing here yanks the worktree from a still-running shell. - if (task.column === "in-progress" || task.column === "in-review") { + if (worktreeReconcileWipColumns.has(task.column) || worktreeReconcileReviewColumns.has(task.column)) { await this.emitWorktreeMetadataAuditEvent({ taskId: task.id, mutationType: "task:auto-recover-worktree-metadata-skipped-active", @@ -7397,6 +7408,8 @@ export class SelfHealingManager extends SelfHealingGitEvidence { async reconcileOrphanedPendingStepResults(): Promise { try { + /* Resolved once per sweep, outside the paging loop, so a large board still pays one resolve. */ + const orphanedPendingWipColumns = await resolveProjectColumnsForRoles(this.store, ["countsTowardWip"]); const pageSize = 500; let offset = 0; let recovered = 0; @@ -7416,15 +7429,20 @@ export class SelfHealingManager extends SelfHealingGitEvidence { for (const task of tasks) { // An operator park is authoritative; this sweep must not reach through it. if (task.userPaused === true) continue; - // Executor-owned rows: resume is deferred at startup, liveness unprovable here. - if (task.column === "in-progress") continue; + /* + FNXC:WorkflowResolvedColumns 2026-07-31-16:45 (fleet, self-healing round 2): + The executor-owned skip is a WIP-ROLE question. Keyed on the id it stopped firing on a renamed + board, so this sweep would rewrite `pending` step results out from under a LIVE executor run — + the one thing its own header says it must never do. + */ + if (orphanedPendingWipColumns.has(task.column)) continue; if (!task.workflowStepResults?.some((result) => result.status === "pending")) continue; if (isSessionLive(task.id)) continue; // Re-read the live row before mutating: the page snapshot can be stale against // a merger/planner that wrote a fresh pending lease after the page was fetched. const fresh = await this.store.getTask(task.id); - if (!fresh || fresh.userPaused === true || fresh.column === "in-progress") continue; + if (!fresh || fresh.userPaused === true || orphanedPendingWipColumns.has(fresh.column)) continue; /* FNXC:WorkflowReviewGates 2026-07-26-15:50: Honor a LIVE review-gate lease, not just in-process session liveness. @@ -12988,6 +13006,11 @@ export class SelfHealingManager extends SelfHealingGitEvidence { const now = Date.now(); const recoveredAgentIds = new Set(); const runningAgents = await agentStore.listAgents({ state: "running", includeEphemeral: true }); + const agentLinkTerminalColumns = await resolveProjectColumnsForRoles(this.store, TERMINAL_ROLES); + const agentLinkLiveColumns = new Set([ + ...await resolveProjectColumnsForRoles(this.store, ["countsTowardWip"]), + ...await resolveProjectColumnsForRoles(this.store, REVIEW_ROLES), + ]); for (const agent of runningAgents) { if (isEphemeralAgent(agent) || !agent.taskId) { @@ -12995,7 +13018,9 @@ export class SelfHealingManager extends SelfHealingGitEvidence { } const linkedTask = await this.store.getTask(agent.taskId); - if (linkedTask && (linkedTask.column === "in-progress" || linkedTask.column === "in-review" || linkedTask.column === "done" || linkedTask.column === "archived")) { + /* FNXC:WorkflowResolvedColumns 2026-07-31-16:50 (fleet, round 2): WIP u REVIEW u TERMINAL. Keyed on + ids none matched on a renamed board, so this sweep evaluated agents whose task was still executing. */ + if (linkedTask && (agentLinkLiveColumns.has(linkedTask.column) || agentLinkTerminalColumns.has(linkedTask.column))) { continue; } diff --git a/scripts/lib/lifecycle-column-census-baseline.json b/scripts/lib/lifecycle-column-census-baseline.json index 615f83c650..bc4571ba48 100644 --- a/scripts/lib/lifecycle-column-census-baseline.json +++ b/scripts/lib/lifecycle-column-census-baseline.json @@ -1,7 +1,7 @@ { "generatedFrom": "node scripts/lifecycle-column-census.mjs --strict --update-baseline", "byFile": { - "packages/engine/src/self-healing.ts": 38, + "packages/engine/src/self-healing.ts": 26, "packages/engine/src/notification/notification-service.ts": 5, "packages/engine/src/executor.ts": 4, "packages/core/src/task-store/moves.ts": 2,