Tell the model it can go parallel — a lock keeps the floor
The model is allowed to emit several tool calls at once. A request flag, a per-tool boolean, and one read-write lock work separately; history still books in emit order
THE QUESTION THIS PAGE ANSWERS
ANSWER FIRSTWhat is the key idea behind “Tell the model it can go parallel — a lock keeps the floor”?
The model is allowed to emit several tool calls at once. A request flag, a per-tool boolean, and one read-write lock work separately; history still books in emit order
Follow the handoffs, not the demo. A system becomes dependable at the boundaries between model, tools, state, permissions, and people. Read each handoff as a place where you can observe, test, and recover.
Name the input, owner, approval, and recovery action for one automated step.
A successful run that cannot explain what happened or be safely repeated.
- build_prompt writes the parallel flag as trueturn.rs L1321
- AND it with the Lite flag when building the requestclient.rs L952
- The router looks up the registry; missing means falserouter.rs L137
- Hidden is serial even if it claims parallelregistry.rs L472
- Spawn first, wait ready, then take the lockparallel.rs L144
- Read if parallel, otherwise writeparallel.rs L152
- Results book in FuturesOrdered insert orderturn.rs L2135
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.
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.
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.
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.
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.
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.
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.
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
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.
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.
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.
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.
exec_command overlap with the still-running view_image?apply_patch tile to a read badge, will the gate still hold it alone?The handoffs inside “Try it first · Several tools leave the blocks together”
“You ask the model to read src/main.rs and src/lib.rs at once, then apply a patch.” shows that an Agent is not defined by the model alone. Each handoff between model, context, tools, state, permissions, and people affects both progress and recovery.
Write the state before adding capability
Starting from “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.”, split the workflow into starting state, next action, tool result, state update, and stop condition. Debugging then means finding the first lost piece of information or authority instead of saying vaguely that the model “got worse”.
- build_prompt writes the parallel flag as true turn.rs L1321
- AND it with the Lite flag when building the request client.rs L952
- The router looks up the registry; missing means false router.rs L137
A happy path is not reliability
Use “view_image is still running;” to replay one successful and one failed run. Record the context, tool result, and owner at each turn; the workflow is maintainable when a second person can follow it without the original builder.
From “Try it first · Several tools leave the blocks together” to “Idea 1 · Tell the model it can go parallel, then split traffic yourself”
“Try it first · Several tools leave the blocks together” grounds the problem in “One read-write lock: who gets the menu, who waits outside the kitchen Play Step Reset Scene Default four Late reader Two exclusives Click a tile to flip read or write. Late reader shows a fair lock: once a writ…”. “Idea 1 · Tell the model it can go parallel, then split traffic yourself” then moves it toward “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”. Together, they show that the lesson is not just a conclusion to remember, but a claim with conditions.
Carry the judgment into the next situation
When analyzing an Agent, trace state, action, tool result, and next step in order. Each handoff should explain where information came from, who confirmed it, and where failure stops.
- “Try it first · Several tools leave the blocks together”: One read-write lock: who gets the menu, who waits outside the kitchen Play Step Reset Scene Default four Late reader Two exclusives Click a tile to flip read or write. Late reader shows a fair lock: once a writ…
- “Idea 1 · Tell the model it can go parallel, then split traffic yourself”: 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
- “The closing point”: Spawn first, wait ready, then take the lock parallel.rs L144
The final “The closing point” brings the discussion to “Spawn first, wait ready, then take the lock parallel.rs L144”. The useful thing to carry forward is knowing which judgments must be revisited when input, scale, or risk changes.
I turned one judgment from this article into a small experiment I could run today. Knowing what to observe next is more useful than simply remembering the conclusion.
After reading this, I first looked for the conditions behind the idea instead of copying the method into a project. That order made the later trade-offs much clearer.
When this judgment reaches real work, which constraint should be added first? I am curious which step matters most between reading and the first practical attempt.
No discussion on this article yet.