Corrected after filing. The original text claimed the canvas runtime had no parallel execution. That was wrong — it has had bounded-parallel levels for some time. The real gap is narrower and is described below.
What
The canvas runtime (src/lib/swarmRuntime.ts) already runs a level's nodes through a bounded worker pool (MAX_PARALLEL_NODES = 4). Two things separate it from the headless executor (src/utils/swarmExecute.server.ts), and both are worth closing:
1. Parallel writes are committed in completion order, not level order.
Each node mutates ctx[outVar] and lastOutput directly as it finishes. When two nodes in the same level write the same variable, the winner is whichever model replied first — so the same swarm on the same input can produce different output on different days. lastOutput has the same problem: an unconnected downstream node sees whichever parallel node happened to finish last.
2. A level containing a condition, router or approval drops to fully sequential.
if (hasCondition || hasApproval || level.length === 1) { /* sequential */ }
The reasoning in the comment is sound — those nodes mutate skippedNodes/deadEdges, and two of them racing on the tracker is a real hazard. But it means a level with four analysts and one router loses parallelism for all five.
Why it matters
(1) is a correctness bug, not a performance one, and it is the harder kind to notice: nothing errors, the numbers are just occasionally different. (2) is a missed speed-up on exactly the graphs that fan out most.
The fix already exists on the other side
The headless executor solved both when it gained parallel levels. Reuse it rather than reinventing:
src/lib/swarmGraph.ts — commitLevelWrites(ctx, staged, reducers) merges a level's staged writes in level order, so the result cannot depend on completion order. With the default last reducer it reproduces sequential semantics exactly, which is what makes it safe to adopt without changing existing swarms' results.
src/utils/swarmExecute.server.ts — the reference implementation: per-node staging buffers, branch decisions collected into pendingSkips and applied together after the level, failures collected and the first in level order rethrown.
Acceptance criteria
Worth knowing before you start
swarmRuntime.ts is ~2000 lines and streams UI events per node. Read how the headless executor was converted first (git log --oneline -- src/utils/swarmExecute.server.ts) — two bugs surfaced there only once the node body became a function rather than a loop body, and the same shape of mistake is available here.
Size: medium. Type: correctness first, performance second.
What
The canvas runtime (
src/lib/swarmRuntime.ts) already runs a level's nodes through a bounded worker pool (MAX_PARALLEL_NODES = 4). Two things separate it from the headless executor (src/utils/swarmExecute.server.ts), and both are worth closing:1. Parallel writes are committed in completion order, not level order.
Each node mutates
ctx[outVar]andlastOutputdirectly as it finishes. When two nodes in the same level write the same variable, the winner is whichever model replied first — so the same swarm on the same input can produce different output on different days.lastOutputhas the same problem: an unconnected downstream node sees whichever parallel node happened to finish last.2. A level containing a condition, router or approval drops to fully sequential.
The reasoning in the comment is sound — those nodes mutate
skippedNodes/deadEdges, and two of them racing on the tracker is a real hazard. But it means a level with four analysts and one router loses parallelism for all five.Why it matters
(1) is a correctness bug, not a performance one, and it is the harder kind to notice: nothing errors, the numbers are just occasionally different. (2) is a missed speed-up on exactly the graphs that fan out most.
The fix already exists on the other side
The headless executor solved both when it gained parallel levels. Reuse it rather than reinventing:
src/lib/swarmGraph.ts—commitLevelWrites(ctx, staged, reducers)merges a level's staged writes in level order, so the result cannot depend on completion order. With the defaultlastreducer it reproduces sequential semantics exactly, which is what makes it safe to adopt without changing existing swarms' results.src/utils/swarmExecute.server.ts— the reference implementation: per-node staging buffers, branch decisions collected intopendingSkipsand applied together after the level, failures collected and the first in level order rethrown.Acceptance criteria
commitLevelWrites()in level orderlastOutputafter a level is the last producer in level order, not arrival orderWorth knowing before you start
swarmRuntime.tsis ~2000 lines and streams UI events per node. Read how the headless executor was converted first (git log --oneline -- src/utils/swarmExecute.server.ts) — two bugs surfaced there only once the node body became a function rather than a loop body, and the same shape of mistake is available here.Size: medium. Type: correctness first, performance second.