fix(events): carry lanes on the archive and completion task:moved emits (#3120)
Follow-up to **#3109** (merged). Independent of my other branches. ## The gap #3109 put resolved lanes on `task:moved` and wired the main move path in `moves.ts`. **Two other emit paths still fired lane-less** — and to a listener, a lane-less emit is not a *missing* answer, it is the **legacy** answer, which on a renamed board is wrong. Concretely: `archiveTaskBackendImpl` emits its own `task:moved`. The executor's archive branch releases the task's active-session registry entry, and **that entry is what blocks a successor task from acquiring the same path**. Fixing the listener alone (#3112) leaves the leak reachable *through this emitter*, because the listener still falls back to the literal when the payload carries nothing. That's the part worth noting for the pattern generally: **a payload-carrying design is only as good as its emitters.** Converting consumers without sweeping producers leaves a hole that looks fixed at the call site. ## Scope | Emit path | Action | |---|---| | `archive-lifecycle-2.ts:373` (`archiveTaskBackendImpl`) | carries lanes | | `task-artifacts-ops.ts:549` (`moveToDoneImpl`) | carries lanes | | `lifecycle-ops.ts:715` | **untouched** — fires from a watcher callback | | `task-update.ts:962` | **untouched** — sync path | Both converted sites are already `async` and already import `resolveWorkflowIrForTask`, so this costs one IR read on a transition that has just done database work. Fail-soft to `undefined`, matching `moves.ts` — "unknown", never a wrong answer. The two untouched ones each need their own look rather than a blanket sweep; flagged, not guessed. ## Verification - `@fusion/core` archive/lifecycle suites — **156 green** - **`pnpm test:gate` green**; `tsc` clean - Changeset added; `check:changesets` passes ## Census **No change**, and that's expected — this converts emit *payloads*, not comparison literals. The effect is that guards already converted in #3109/#3112 receive a correct answer on these paths instead of a legacy fallback. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Task move events now include accurate board lane information when tasks are archived or marked complete. * Improved handling for renamed workflows, ensuring task transitions use the correct lane details. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
7
.changeset/emitters-carry-lanes.md
Normal file
7
.changeset/emitters-carry-lanes.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Archive and completion transitions now report the board's own lanes to engine listeners.
|
||||
category: fix
|
||||
dev: `archiveTaskBackendImpl` and `moveToDoneImpl` attach `lanes` to their `task:moved` emits, matching `moves.ts`.
|
||||
@@ -9,6 +9,7 @@
|
||||
import {TaskStore, storeLog} from "../store.js";
|
||||
import { columnsWithFlag, declaresAnyLifecycleTrait } from "../workflow-lifecycle-traits.js";
|
||||
import { resolveWorkflowIrForTask } from "../workflow-ir-resolver.js";
|
||||
import { toTaskMoveLanes } from "../workflow-lifecycle-traits.js";
|
||||
import {getFeatureByTaskId as getMissionFeatureByTaskId, unlinkFeatureFromTaskId as unlinkMissionFeatureFromTaskId, recordGeneratedFixOperatorStop} from "../async-mission-store-queries.js";
|
||||
import {TaskHasLineageChildrenError, TaskNotFoundError, TaskSelfDeleteError} from "./errors.js";
|
||||
import {mkdir, writeFile} from "node:fs/promises";
|
||||
@@ -370,7 +371,18 @@ export async function archiveTaskBackendImpl(store: TaskStore, id: string, optio
|
||||
task.updatedAt = archivedAt;
|
||||
task.deletedAt = archivedAt;
|
||||
|
||||
store.emit("task:moved", { task, from: fromColumn, to: "archived" as Column, source: "engine" });
|
||||
/*
|
||||
FNXC:WorkflowEvents 2026-07-31-00:40 (fleet):
|
||||
Carry the resolved lanes, like the main move path in `moves.ts`. Listeners read `task:moved`
|
||||
synchronously and cannot resolve for themselves, so an emit WITHOUT lanes hands every consumer
|
||||
its legacy fallback — which on a renamed board is the wrong answer, not a missing one.
|
||||
|
||||
Concretely: the executor's archive branch releases the task's active-session registry entry, and
|
||||
that entry is what blocks a SUCCESSOR task from acquiring the same path. Emitting this transition
|
||||
lane-less left that leak reachable through this path even after the listener itself was fixed.
|
||||
*/
|
||||
const movedLanes = toTaskMoveLanes(await resolveWorkflowIrForTask(store, task.id).catch(() => undefined));
|
||||
store.emit("task:moved", { task, from: fromColumn, to: "archived" as Column, source: "engine", lanes: movedLanes });
|
||||
|
||||
// Best-effort near-duplicate cleanup.
|
||||
await store.clearNearDuplicateReferencesToFailSoft(id, {
|
||||
|
||||
@@ -13,6 +13,7 @@ import { TaskStore } from "../store.js";
|
||||
import { resolveProjectColumnsForRoles } from "../project-lane-vocabulary.js";
|
||||
import {declaresAnyLifecycleTrait, resolveReviewColumns, resolveTaskLifecycleColumns} from "../workflow-lifecycle-traits.js";
|
||||
import {resolveWorkflowIrForTask} from "../workflow-ir-resolver.js";
|
||||
import {toTaskMoveLanes} from "../workflow-lifecycle-traits.js";
|
||||
import { countAgentLogEntries, readAgentLogEntries } from "../agent-log-file-store.js";
|
||||
import { toJsonNullable } from "../db.js";
|
||||
import { DbTransaction, recordRunAuditEventWithinTransaction } from "../postgres/data-layer.js";
|
||||
@@ -563,7 +564,18 @@ export async function moveToDoneImpl(store: TaskStore, task: Task, dir: string):
|
||||
// Update cache if watcher is active
|
||||
if (store.isWatching) store.taskCache.set(task.id, { ...task });
|
||||
|
||||
store.emit("task:moved", { task, from: fromColumn, to: completeColumn as Column, source: "engine" });
|
||||
/*
|
||||
FNXC:WorkflowEvents 2026-07-31-00:40 (fleet):
|
||||
Carry the resolved lanes, like the main move path in `moves.ts`. Listeners read `task:moved`
|
||||
synchronously and cannot resolve for themselves, so an emit WITHOUT lanes hands every consumer
|
||||
its legacy fallback — which on a renamed board is the wrong answer, not a missing one.
|
||||
|
||||
Concretely: the executor's archive branch releases the task's active-session registry entry, and
|
||||
that entry is what blocks a SUCCESSOR task from acquiring the same path. Emitting this transition
|
||||
lane-less left that leak reachable through this path even after the listener itself was fixed.
|
||||
*/
|
||||
const movedLanes = toTaskMoveLanes(await resolveWorkflowIrForTask(store, task.id).catch(() => undefined));
|
||||
store.emit("task:moved", { task, from: fromColumn, to: completeColumn as Column, source: "engine", lanes: movedLanes });
|
||||
}
|
||||
|
||||
export function clearDoneTransientFieldsImpl(store: TaskStore, task: Task): boolean {
|
||||
|
||||
Reference in New Issue
Block a user