The merge node could not observe a graph abort. WorkflowPrimitiveContext carried no signal, so requestMerge raced the merge only against its own 30-minute GRAPH_MERGE_TIMEOUT_MS using a controller it owned. A hard-cancel (user cancel, engine restart, pause/resume) aborted the graph controller and the walk kept sitting inside the merge node for the full timeout. When the timeout finally fired it aborted the still-running AI merge -- surfacing as "Manual-merge failed: Request was aborted" -- and the walk reported value=merge-timeout for a cancellation it had missed half an hour earlier. An abort landing between merger-ai's `worktree: null` write and mergeConfirmed then stranded the card as no-worktree-no-merge-confirmed. Thread the graph AbortSignal from WorkflowNodeExecutionContext (where it already existed) through primitiveNodeContext/primitiveContextForNode into the primitives, and honor it on both merge surfaces: - requestMerge fails fast when the walk is already cancelled, before ensureWorkflowMergeBoundaryTask mutates the row or the requester enqueues a merge, and links the graph signal into its timeout controller via AbortSignal.any -- raced separately so the walk returns on the abort rather than waiting on a requester that may never settle. - The legacy merge seam had the identical unguarded race and gets the same treatment. The timeout stays: it bounds a wedged merge queue, which is a different failure from cancellation. Both signals must stay live -- dropping either silently restores the stall with no type error. Cancellation returns a distinct `merge-cancelled` rather than reusing merge-timeout. Returning `data.status: "failed"` would let classifyMergeFailure read the unknown reason as merge-failed and route the cancellation into bounded auto-merge retry, re-requesting the merge the operator just cancelled. Regression test covers both merge surfaces, both cancel timings (pre-flight and mid-flight), the no-signal back-compat path, the signal plumbing itself, and the classification boundary. Verified by removing the fix: 7 of 9 cases fail, with the mid-flight cases hanging until timeout. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Changeset Format Guide
Each changeset file in this directory describes one user-facing change for release notes.
Required body format
---
"@runfusion/fusion": minor
---
summary: Add a Command Center productivity control for LOC backfills.
category: feature
dev: Uses the new `fn_backfill_loc` tool; settings key `commandCenter.locBackfill`.
Fields
| Field | Required | Description |
|---|---|---|
summary |
Yes | One line, user-facing, max 120 chars. Describe what changed for the operator. |
category |
Yes | One of: feature, fix, breaking, security, performance, internal. |
dev |
No | Developer or migration detail. Preserved in per-package CHANGELOGs but excluded from distilled release notes. |
Audience
The summary is the only content that appears in end-user release notes by default. Write for Fusion operators — describe behavior, fixes, and what changed. Avoid internal class names, file paths, and implementation detail.
Bump types
patch— bug fixes, internal changesminor— new features, CLI additions, toolsmajor— breaking changes
Validation
Run pnpm check:changesets to validate. The linter runs in the PR-check gate and test:gate. Legacy freeform changesets pass with a warning during the transition period.