Waves, not queues, for parallel agent work
4 min read
The obvious way to run several coding agents is a work queue: put tasks in, let agents pull. It maximises utilisation — no agent is ever idle while work exists — and for coding work it is usually the wrong shape.
A queue optimises for agents being busy. A wave optimises for the work being coherent. Those pull in opposite directions more often than you would expect.
What a barrier buys
A wave is a set of tasks that run in parallel and a barrier: nothing in wave N+1 starts until everything in wave N finishes.
export async function autoAdvanceWave(
wavePlanId: string,
completedWaveIndex: number,
config: WaveExecutionConfig
): Promise<void>
That is deliberately restrictive. A pure queue would start a task the moment its own dependencies were met, which is strictly more parallel. The barrier gives up some of that, and gets three things back.
A coherent review point. After a wave completes there is a state of the repository that somebody can look at. In a continuous queue there is no such moment — the tree is always mid-flight, and "is this working" has no answer you can hold still long enough to check.
Blast radius. A bad plan discovered in wave two has done two waves of damage, not the whole graph. Agents that would have run on a now-invalid assumption have not started yet.
Contention you can reason about. File ownership only has to be disjoint within a wave. That is a far weaker property than global disjointness, and it is checkable — which is what makes validating a plan before dispatch tractable at all.
Auto-advance is what makes it usable
A barrier that a human has to release is a meeting. The advancement has to be automatic or the wave structure just adds latency to every step:
const isLastWave = completedWaveIndex >= wavePlan.totalWaves - 1;
Two branches. Advance to the next wave, or — on the last one — settle final metrics and finish the plan. The second branch matters more than it looks: without an explicit terminal case, a plan's final wave completes and the plan itself never reaches a finished state. It sits at 100% forever, which is a support ticket rather than a bug report, because everything worked.
Any barrier-based system needs that last-wave branch written explicitly. The loop that advances is not the same as the code that finishes.
When a queue is the right answer
The wave shape is not universal, and it is worth knowing where it costs you.
If your tasks are genuinely independent — a hundred files needing the same mechanical transform, no shared state, no ordering — a queue is better and the barrier is pure waste. Waves earn their cost when tasks have dependencies or contention, which is most feature work and almost no bulk-edit work.
The tell is whether "what should I look at after step one" has a meaningful answer. If it does, you want waves. If every task is interchangeable, you want a queue.
The width question
Given waves, the interesting lever is how wide each one is — how many tasks run in parallel inside it.
Wider is faster until it is not. The costs of widening are contention (more tasks touching adjacent code), review load (a wider wave produces more diff to judge at once), and concurrency limits that may throttle you anyway. The benefit is wall-clock.
The critical path sets the floor: a plan cannot finish faster than its longest dependency chain, however wide you make the waves. Widening a wave that is not on the critical path buys nothing at all — a genuinely common mistake, because the wide wave looks like progress while the chain that determines the finish time is untouched.
Delivery is still a row in a table
Waves are a coordination structure. They are not a delivery guarantee.
Each task in a wave still has to reach a machine and be acknowledged, and if that hand-off is lossy the barrier will wait forever on work that never started. That is why dispatch is a committed database row rather than a message — the wave planner asks "is everything in this wave done", and that question is only answerable if every task's state is durable.
The two mechanisms are independent and both are necessary. A perfect wave structure over an unreliable dispatch path is a system that hangs politely.
What is not proven
Wave width has never been measured against outcome quality here. The argument above — that wider waves trade review coherence for wall-clock — is reasoning from the failure modes we have hit, not from data.
The benchmark suite is built to answer exactly this question, running the same project under different decomposition strategies. It is not yet in CI and has no committed results, so treat the width guidance as a hypothesis with a plan to test it rather than a finding.
