OpenAI Codex · Tool dispatch

Tell the model it can go parallel — a lock keeps the floor

What the model sees, what actually runs, and what history records are three different orders. One flag lets a response emit many calls. One read-write lock decides who can stack in.

Course goalAfter this lesson you can explain three things. The parallel flag sent to the model only means one response may contain several tool calls. Each tool defaults to the write lock; parallel takes an override. When results land in the session, they still line up in the order the model originally emitted the calls.
Try it first · Several tools leave the blocks together
One read-write lock: who gets the menu, who waits outside the kitchen
Scene
Click a tile to flip read or write. Late reader shows a fair lock: once a writer is queued, later readers cannot cut in.
Menu · read lock
Parallel tools can stand here together
Kitchen · write lock
Exclusive — one at a time
Queued outside
Order the model emitted
Order that actually ran
Order booked into history
Waiting to start. Hit Play and watch the lock split traffic.
Logic trail · each animation step maps to a stretch of source
  1. build_prompt writes the parallel flag as trueturn.rs L1321
  2. AND it with the Lite flag when building the requestclient.rs L952
  3. The router looks up the registry; missing means falserouter.rs L137
  4. Hidden is serial even if it claims parallelregistry.rs L472
  5. Spawn first, wait ready, then take the lockparallel.rs L144
  6. Read if parallel, otherwise writeparallel.rs L152
  7. Results book in FuturesOrdered insert orderturn.rs L2135
Hit Play. After several tools leave together, see who can stack in and who must wait.
Who can run in parallelRead-badge tools enter the menu and can overlap. Write-badge tools take the kitchen alone. Once a writer is outside, later readers line up behind it.
Three ordersThe model emits 1, 2, 3, 4. Execution can overlap. History still books in emit order — first emitted, first booked, even if it finished later.
The boundary you can changeFlip the patch to a read badge and all four walk in together. Flip a reader to write and they block each other. Late reader is there to show a fair lock will not let you cut in.
Teaching demo: Tiles and timings are teaching fixtures, to show the lock split and that the three orders can disagree. Line numbers on the logic trail match openai/codex commit 4f39251a01.
Idea 1 · Tell the model it can go parallel, then split traffic yourself
What problem it solves

You ask the model to read src/main.rs and src/lib.rs at once, then apply a patch. On screen the two reads almost progress together. The patch beat pauses.

If the runtime treats “can go parallel” as these calls overlapping, two apply_patch hits would edit the shared diff ledger together and scramble it. If the calls go head-to-tail, even two file reads queue and you burn another wall-clock round. The request-side flag only answers whether the model is willing to emit several boxes at once. It cannot say who may overlap when those boxes land.

What the idea is

The flag sent to the model lives on Prompt. The field defaults to false. The main sample path walks build_prompt and writes it true. When the Responses body is built, it is ANDed with whether the model is Responses Lite. On Lite the flag turns off. The two compact pack paths also hard-code true, so they match the main sample request’s shape. Websocket reuse compares field by field, and this flag is one of them.

Source:codex-rs/core/src/session/turn.rs lines 1312–1328 · codex-rs/core/src/client.rs lines 946–953

The exec side has another table. ToolExecutor’s supports_parallel_tool_calls defaults to false. Miss the override and you take the write lock. exec_command, view_image, and tool_search override to true. apply_patch does not. The router looks up the registry first; missing means false. Hidden stays serial even if the handler itself returns true. MCP still needs a server switch or a read-only hint; the default is serial.

Source:codex-rs/tools/src/tool_executor.rs lines 122–124 · codex-rs/core/src/tools/registry.rs lines 470–473

The model only sees parallel_tool_calls as true, or false on Lite. It cannot see who can actually overlap. The tool list does not drop an item just because that tool will take the write lock.

Request side build_prompt writes true Lite ANDs it to false The model may emit several at once No promise they overlap on the floor Classify table Default false; override if you want parallel Hidden and unknown names are serial MCP needs opt-in or a read-only hint The model cannot see this table Gate Parallel takes the read lock Serial takes the write lock One RwLock for the whole floor Spawn first, then take the lock
Teaching diagram: One flag, one table, one lock — each owns a stretch.
Why it lasts

The promise to the model can split from host scheduling. That travels across languages. The request side only answers whether it is willing to emit several calls at once. The exec side only answers whether this one may overlap. Lite tests turn the flag off, so the authors accept losing that hint on some model paths. The gate stays. If the model still emits two calls in one response, both tasks still spawn and still take the lock from the local table.

Idea 2 · One floor-wide read-write lock as the gate
What problem it solves

Classify right and someone still has to watch the door. Two readers can coexist; one writer must be exclusive. Grab the lock first and then wait for an MCP server to connect, and one cold start will stall a neighboring exec_command that is already ready.

Then fairness. If a later reader can climb over a waiting writer, a patch may never get the lock. Swap in the stdlib RwLock and priority depends on the OS — writers can starve.

What the idea is

One sample shares a single tokio::sync::RwLock<()>. The guarded value is the unit type — no business data, just a gate. Tasks spawn first, optionally wait ready, then take the lock. Parallel takes read; otherwise write. Readiness waits outside the lock. An MCP server that has not connected yet will not sit on the write lock and stall a neighboring shell.

Source:codex-rs/core/src/tools/parallel.rs lines 144–156

This lock’s priority is fair, also called write-preferring. Until a queued write is acquired and released, later read locks are not issued. A view_image is still running, apply_patch is already outside, and a new exec_command could overlap with view_image — yet it must line up behind the patch. Fairness buys “writers do not starve.” The cost is later readers being separated by the writer.

Classify grain is the tool instance; it cannot see this call’s arguments. exec_command takes the read lock whether it runs ls or rm. apply_patch takes the write lock no matter how large the patch is. Two unrelated serial tools also block each other. Search the business code: there is no capacity cap. Ten shells can walk in together. Capacity is left to the process, the sandbox, and the OS.

Menu · already holding the read lock view_image exec_command Whoever already entered keeps running Two people can read the menu together Outside · write lock queued apply_patch Waiting for readers to drop the lock Writers enter a FIFO Late reader exec_command Cannot cut in Line up behind the writer
Teaching sketch: Readers already inside keep running. A later reader sees a writer queued and lines up behind.
Why it lasts

The question is whether anyone is exclusive right now. A restaurant can have many people at the menu; only one enters the kitchen. Fairness lives in the lock implementation. Business code only asks parallel or exclusive. Swap in a lock that lets new readers cut in, and writers can starve. Waiting for ready outside the lock is the same judgment: if you are not ready, do not occupy the door.

Idea 3 · Finish order does not decide bookkeeping order
What problem it solves

Two exec_commands overlap and the later emit may finish first. Book history by finish order and the next round’s results will not match the calls the model emitted. Building a future on each OutputItemDone in the stream governs when work starts. This layer governs who may overlap after start, and in what order results book.

What the idea is

The sample loop pushes tool_future into FuturesOrdered in arrival order. drain pops in insert order, then writes the session. The read lock lets two shells overlap; booking still follows emit order. First emitted, first booked — even if it actually finished later.

Source:codex-rs/core/src/session/turn.rs lines 2130–2140

The order the model thinks, the order that happens on the lock, and the order written into history can disagree.
Why it lasts

Observation order and execution overlap are split in the structure. Parallel only changes wall-clock, not the ledger. When you build an Agent, write those two queues separately. Tell the model it can go parallel, consult the local table when tools run, and still accept results in the original call-list order.

Side-by-side · Another answer to the same question

DeepSeek Harness: classify by args, exclusive as a barrier

DSH lets each tool provide isConcurrencySafe(args). Only a literal true joins the parallel set. Missing declaration, bad args, or a throwing classifier: exclusive. bash has no classifier, so the whole tool is exclusive. The scheduler waits for the full message, packs consecutive parallel calls into a group, and treats each exclusive call as its own barrier group. Inside a group it rolls a pool, default cap 10.

Codex can skip that grouping because it compresses the decision into a per-tool boolean, then simulates a barrier with one lock. DSH can keep even read-only bash serial and give up some concurrency. Codex can overlap two ls — and two rm as well.

Source checked on both sides · 2026-08-22

Claude Code: batch by args; only read-only bash goes parallel

Claude’s isConcurrencySafe defaults to false. BashTool hands parallel to isReadOnly: the command returns true only if it passes a read-only constraint. ls can join a parallel batch; commands with write side effects join a serial batch. Consecutive safe calls pack into one concurrent batch; each unsafe call is its own batch, and even inside a batch they go one after another. The cap comes from an env var; parse failure yields 10.

Codex’s exec_command skips command parsing; even write commands take the read lock. All three hide scheduling metadata in the host; they close the door in different places. Codex closes on the default and Hidden, and is loosest on shell. DSH closes on the classifier, so bash is fully serial. Claude sits in the middle.

Source checked on both sides · 2026-08-22
Classroom Exercise
01

Can a late reader cut in?

view_image is still running; apply_patch is already queued outside. The model emits another exec_command. In the demo switch to Late reader, step through, and match the three questions below.

Can this exec_command overlap with the still-running view_image?
In what order do the three results book into history?
If you flip the apply_patch tile to a read badge, will the gate still hold it alone?
Takeaway:Telling the model it can go parallel is the request-side flag. Who may overlap depends on whether the tool overrode the default; Hidden and unknown names stay exclusive. After they finish, history books in emit order. The three orders can disagree.