Skip to content

Course · Codex · Subagents

Fan the audit out across parallel subagents

Stage one came back with a short findings list, and one worker paid for all of it in sequence. The rest of the audit does not have to run that way. A verdict about the ledger’s sums does not depend on whatever the worker reading main.py concludes; the categorizer’s position on the import route is independent of both. When work decomposes like that, running it serially leaves speed on the table - at this repo’s size, honestly, only a little. The procedure is still worth learning at full price.

Before you fan out, two questions decide whether parallelism is leverage or theater.

The independence test. If two workers finished in a different order, would the final findings change? For these audit slices, no - each verdict depends on one file and the cents rule, never on another worker’s output. That is your license to parallelize. The moment the answer is yes, you have a hidden dependency, not independent work. The hardening ahead fails this test honestly - its test slice asserts its parse slice - and the answer there is a merge order, not parallel hope.

Write the aggregation first. Before you spawn anything, you should be able to say in one sentence how the slices come back together: here, “concatenate the verdicts into one findings list.” If the merge cannot be described that simply, your decomposition is wrong, not your tooling. Fan-out is an aggregation problem wearing a spawning costume - decide how the answers reduce before you scatter the questions.

A test you fail here doesn’t cost you a wrong answer - it costs you a fan-out that looks like leverage and isn’t.

The natural split is one worker per path: the sums in app/services/ledger.py, the endpoints in app/main.py, and the categorizer’s keyword table with its call site inside the import path (app/services/categorise.py, wired into parse_csv by the models chapter’s landed edit). You describe the slices and ask for parallel subagents - a subagent being a separate Codex thread with its own context window, which reads and works in private and hands back a short report (the delegate lesson covers the mechanics). Spawning stays prompt-driven - plain language in, workers out.

> Audit the remaining money paths in parallel - one subagent each:
sums (app/services/ledger.py), endpoints (app/main.py), and the
categorizer (app/services/categorise.py) plus its call site in
parse_csv. Same rule as before: any float on a money value is a
finding; no-write probes allowed; no edits. Each returns file:line
findings plus a one-line verdict. Keep the returns terse - counts
and findings only.

Each worker starts as cold as the lone auditor did - subagents share no memory - so each pays its own orientation: its slice plus the auto-loaded rules chain, a few hundred tokens apiece here. Three workers, three orientations. None of it lands in your window, but it is real spend, and at this scale the multiplication is pocket change. On a codebase where a slice is a package, the same multiplication is the line item that decides whether fanning out was worth it.

That isolation is worth counting once, not just trusting. Toggle the same shape of job between running inline and fanning it out - the widget models a bigger repo’s slices; the bars are illustrative, the shape is the point:

Your window136k / 200k (68%)

Three domain sweeps, sequentially, in your window. Every file read and dead-end grep lands next to the migration work you actually came here to do - illustrative bars, but the shape holds: past halfway before any of it is done, and the sweeps ran one at a time.

Delegation does not reduce total tokens spent; it buys parent coherence and parallelism. Your window holds the brief and the short returns; the sweeps burn in windows you never open, side by side.

Codex caps concurrently open spawned threads through agents.max_concurrent_threads_per_session (legacy alias agents.max_threads); leave it unset and Codex chooses the default. Start narrow - three or four workers gives you a way to measure speedup and rate-limit headroom - and widen only with evidence. Every worker draws on your account’s rate budget, so concurrency is a design constraint, not free throughput.

The multiplier that doesn’t come with the win

Section titled “The multiplier that doesn’t come with the win”

Here is the anti-hype half. Parallel reading and parallel verdicts are real, and wall-clock time on that part genuinely drops with the worker count. What it does not do is divide your total time by the number of workers, because the slices still have to come home. Workers finish at roughly the same time - but their results, or in the hardening’s case their branches, merge back one at a time, through a gate, because nothing about parallel work makes the reduce step parallel too.

The merge tail scales with how many slices you have to bring home, not with how many workers produced them. The lesson after next prices that tail in real turns.

Once the fan-out works, the temptation is to reach for it everywhere. Resist that. It is overkill, or worse, when:

  • The slices depend on each other. If clearing one path required another worker’s verdict, they cannot run at once - you would be guessing. Chain them: one worker’s result feeds the next. The audit passes this test; the hardening’s parse and test slices do not, which is why their merge order gets fixed in advance two lessons from now.
  • The work is light. Fresh workers that each re-read the rules chain and re-orient can be slower than one thread that already knows the codebase. Fan-out has a fixed startup cost - the orientation read you just watched multiply - and tiny jobs do not earn it back.
  • You fan out wider than you can aggregate. When workers finish, results return to the parent - and many workers each handing back a detailed report is its own flood, arriving at once. Twenty terse returns at 300 tokens apiece is 6,000 tokens landing in one turn: noticeable, but manageable. Twenty essays is five times that. Brief every worker terse - counts and file:line findings - or you have not avoided the flood, only relocated it.

One more guardrail, stated the way the machinery actually behaves: a failed worker is not automatically retried. If three of twenty workers die - rate limit, refusal, a crash - each failure surfaces a terminal error to the parent. Nothing keeps the seventeen good verdicts while selectively retrying the three bad ones on a cap; that mechanism does not exist. You get the seventeen results plus errors naming the failures.

Follow-up is your explicit call: route another instruction to the failed worker’s thread, or spawn a fresh one for that slice.

This is why the independence test pays twice. With genuinely independent slices, one dead worker leaves the others’ verdicts intact, and your aggregation simply notes the missing slice and moves on. A fan-out that returns nineteen-of-twenty with the gap named is a fine outcome; one that loops silently forever on a stuck worker is not.

So the shape that works: a handful of independent, heavy slices; terse returns; the merge defined before the spawn; failures surfaced, not retried in the dark. That is leverage with a known price, not a free multiplier.

The audit slices only read shared files, which is why fanning them out was safe. The hardening edits - and the moment two agents write to the same working tree, the problem stops being scheduling and starts being truth: one worker reads the other’s half-written file and believes it. Next, the constructed projection begins: one worktree per worker.