Skip to content

Canvas parallel execution: commit in level order, and stop bailing out on condition levels #20

Description

@rohan044

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.tscommitLevelWrites(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

  • Parallel nodes stage their writes; the level commits through commitLevelWrites() in level order
  • lastOutput after a level is the last producer in level order, not arrival order
  • Condition/router decisions are staged and applied after the level, so those levels no longer force sequential execution
  • Approval nodes keep whatever ordering they need — call this out explicitly in the PR rather than assuming
  • A swarm with two parallel nodes writing the same variable produces the same result on repeated runs
  • A swarm produces the same final output as before the change — test this hardest

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    help wantedExtra attention is neededswarmsSwarm canvas and execution

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions