The stream is still going. Tools already started.
The model is still streaming words, and a tool call is already recognized. Codex writes history on OutputItemDone, then pins exec — every stream exit drains first.
THE QUESTION THIS PAGE ANSWERS
ANSWER FIRSTWhat is the key idea behind “The stream is still going. Tools already started.”?
The model is still streaming words, and a tool call is already recognized. Codex writes history on OutputItemDone, then pins exec — every stream exit drains first.
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.
- An SSE frame first becomes a generic event, no business meaning yetresponses.rs L164
- kind output_item.done yields OutputItemDoneresponses.rs L352
- The sampling loop hands it to handle_output_item_done immediatelyturn.rs L2384
- Write the function_call to history and rollout firststream_events_utils.rs L316
- Then pin the tool and push the ordered queuestream_events_utils.rs L320
- Stream end, break, or cancel only leaves the receive loopturn.rs L2282
- drain writes results to history in insert orderturn.rs L2135
- Only then check the cancel token; Stream is retryableturn.rs L2760
You ask the model to read three files and write a summary. The screen is still typing. The sound of reading a file already started. Then you hit Esc. The UI stops. History still keeps that request, sometimes the result too. You thought cancel meant nothing happened. The runtime does not book it that way.
The more common case: the model already emitted two function_calls, a third is still on the way, and SSE closes before response.completed. Should the next retry see empty history, or calls and results already persisted?
Wait for Completed to write, and an early close throws away calls that were already complete. A retry makes the model emit the same calls again. Skip the write on cancel and history keeps a half request. Model and UI both see an unclosed call.
OutputItemDone is the parse layer folding one response.output_item.done frame into a business event. The moment it arrives, sampling writes it to session history and rollout, then wraps tool exec as a future on the ordered queue. Cancel uses a child token: when the parent lights, this tool stops with it. However fast cancel arrives, this function_call is already in history. At most you write one more aborted by user.
Source: codex-rs/core/src/stream_events_utils.rs lines 190–192; lines 316–327. The comment above the type alias locks the contract: finished model output is recorded immediately. If the turn later cancels, history and rollout stay in sync.
History only appends. It does not rewrite. The tool already read disk. That fact happened. A cancel tree can interrupt exec. It cannot unwrite a request already on disk. Write the request first, the result second, and the transcript stays closed. The contract is not Rust-specific. In TypeScript it is still append then push a promise.
Source: AGENTS.md lines 91–100, Model visible context, first rule: No history rewrite.
The model often emits a file read, then keeps writing an explanation. Wait for the closing whistle to start work, and you serialize file-read latency with typing latency. Starting inside the stream overlaps those two. The cost: cancel and a broken stream must claim futures already running. No owner, and you get orphans — tools still running, history out of sync.
The receive loop — clean Completed, early close, or or_cancel — only leaves the loop. The function has not returned. It always calls drain_in_flight, waits for each future in insert order, writes history, and only then checks the cancel token.
A broken stream takes Stream and is retryable. A retry rebuilds the prompt from clone_history; calls and results already written stay. Esc takes TurnAborted and is not retryable. Running tools write abort copy. drain writes that as ordinary output.
Source: codex-rs/core/src/session/turn.rs lines 2282–2284; lines 2744–2762. codex-rs/protocol/src/error.rs lines 88–93, lines 364–390.
Start mid-stream and every exit must wait for the set. Ownership stays in the sampling function’s locals. There is no second background reaper. Success, error, and cancel share this wrap-up. Same in another language: leave the async loop, allSettled first, then decide retry or abort.
Three tools may run at once. Whoever finishes first, history still writes results in the order the model emitted them. Write in completion order and the same session replayed twice may diverge; the prompt cache gets more brittle too. The ordered queue splits observation order from exec order. The concurrency gate lives on another layer. Here, just this: pin order is later drain order.
Source: codex-rs/core/src/session/turn.rs lines 2130–2154; lines 2391–2397.
Claude Code: wait for the stream by default, plus one in-stream gate
On the default path the stream loop only collects tool_use, and runTools starts after for await ends. A broken stream just drops collected blocks. You skip local ownership that drains at every exit. The cost: tool latency and typing latency serialize.
When streamingToolExecution is on, behavior moves toward Codex: addTool inside the stream, start immediately. Failure rollback must discard tools already running so old ids do not leak into a retry. Codex has no matching discard, because it persists first and retries read history.
DSH: a three-stage waterfall plus a monotone Guard — who may refuse
DSH’s entry is an already-formed tool call. pre / guard / around / post answer who may refuse, and whether a result remains after refusal. Guard has a refuse reason or an abstain — no “allow” option. Its drained is wrap-up inside one execute. While SSE is still in flight, this waterfall has not started.
The words look close. The exits differ. One guards monotone permission. One guards a closed streaming transcript. Move Guard into Codex and it will not stop a broken stream from dropping transcript. Move persist-then-drain into DSH and it will not answer whether a plugin can turn refuse into allow.
Source-checked on both sides · tools/src/index.ts lines 1–4, lines 703–711, lines 1328–1337 · DSH · three-stage waterfall and monotone GuardSwap write and pin
In the tool branch of handle_output_item_done, swap record_completed_response_item and Box::pin(handle_tool_call). Cancel happens before pin, before persist. What do the next sample and session restore see?
Pin the answer on history only appending, and on when drain_in_flight writes.
OutputItemDone, write first, then start. Every stream exit drains first. Results write in emit order. Cancel interrupts exec. The transcript already written stays.
The handoffs inside “Play first · Drag the progress. Watch which frame starts the run”
“You ask the model to read three files and write a summary.” 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 “The more common case: the model already emitted two function_call s, a third is still on the way, and SSE closes before response.completed .”, 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”.
- An SSE frame first becomes a generic event, no business meaning yet responses.rs L164
- kind output_item.done yields OutputItemDone responses.rs L352
- The sampling loop hands it to handle_output_item_done immediately turn.rs L2384
A happy path is not reliability
Use “Pin the answer on history only appending, and on when drain_in_flight writes” 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 “Play first · Drag the progress. Watch which frame starts the run” to “Idea 1 · Stamp the request first. Then start the tool”
“Play first · Drag the progress. Watch which frame starts the run” grounds the problem in “Same SSE stream: drag the progress, watch when tools start Play Step Reset Exit Clean finish Close the stream early Esc Drag the slider or tap an event cell. Left runs inside the stream. Right waits for the str…”. “Idea 1 · Stamp the request first. Then start the tool” then moves it toward “You ask the model to read three files and write a summary. The screen is still typing. The sound of reading a file already started. Then you hit Esc. The UI stops. History still keeps that request, sometimes th…”. 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.
- “Play first · Drag the progress. Watch which frame starts the run”: Same SSE stream: drag the progress, watch when tools start Play Step Reset Exit Clean finish Close the stream early Esc Drag the slider or tap an event cell. Left runs inside the stream. Right waits for the str…
- “Idea 1 · Stamp the request first. Then start the tool”: You ask the model to read three files and write a summary. The screen is still typing. The sound of reading a file already started. Then you hit Esc. The UI stops. History still keeps that request, sometimes th…
- “The closing point”: Then pin the tool and push the ordered queue stream_events_utils.rs L320
The final “The closing point” brings the discussion to “Then pin the tool and push the ordered queue stream_events_utils.rs L320”. 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.