diff --git a/.changeset/emitters-carry-lanes.md b/.changeset/emitters-carry-lanes.md new file mode 100644 index 0000000000..e55954b13b --- /dev/null +++ b/.changeset/emitters-carry-lanes.md @@ -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`. diff --git a/packages/core/src/task-store/archive-lifecycle-2.ts b/packages/core/src/task-store/archive-lifecycle-2.ts index 42a80c6f4e..b32012a520 100644 --- a/packages/core/src/task-store/archive-lifecycle-2.ts +++ b/packages/core/src/task-store/archive-lifecycle-2.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, { diff --git a/packages/core/src/task-store/task-artifacts-ops.ts b/packages/core/src/task-store/task-artifacts-ops.ts index aa06f79356..200d3e8829 100644 --- a/packages/core/src/task-store/task-artifacts-ops.ts +++ b/packages/core/src/task-store/task-artifacts-ops.ts @@ -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 {