Mid-turn interrupt: this turn, next turn, or refused
The Agent is editing a file when you add a constraint. Codex uses one entry, three modes, and a two-stage mailbox to decide start, steer, or refuse on the spot.
THE QUESTION THIS PAGE ANSWERS
ANSWER FIRSTWhat is the key idea behind “Mid-turn interrupt: this turn, next turn, or refused”?
The Agent is editing a file when you add a constraint. Codex uses one entry, three modes, and a two-stage mailbox to decide start, steer, or refuse on the spot.
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.
This turn
Next turn
Refused at the door
- Dispatch on TurnInputMode, default StartOrSteerturn_input.rs L141
- Try steer_input first; only NoActiveTurn starts workturn_input.rs L195
- Only Regular accepts. Review and Compact refuse on the spot.turn_input.rs L507
- If idle, apply_started then spawn_taskturn_input.rs L242
- An interrupt writes pending and flips the phase back to CurrentTurnturn_input.rs L558
- A final answer defers the delivery phase to NextTurninput_queue.rs L206
- A tool item accepts the phase back to CurrentTurnstream_events_utils.rs L302
- get_pending_input looks at the phase, then decides whether to empty the mailboxinput_queue.rs L297
Three everyday endings all sting. Interrupt now and the first two files may sit half-done on disk. Queue for the next turn and you watch it finish the rest the old way. Stuff it into the current context without waking the loop, and the model sees it only the next time it speaks. Your constraint arrives late.
Codex folds this into one entry and three modes. The caller picks StartOrSteer, StartIfIdle, or Steer — it does not shout start. Core judges from busy/idle and task kind, and returns Started, Steered, or NotSubmitted at once. The decision ends there. It waits for no user-prompt hook, no history persist, no model sampling.
Source: codex-rs/core/src/session/turn_input.rs lines 1–9; codex-rs/protocol/src/turn_input.rs lines 127–136
StartOrSteer follows its name. Try an interrupt first. Only NoActiveTurn leads to apply_started then spawn_task. Other refuse reasons wrap as NotSubmitted. It will not start work in secret. TUI live voice takes this path too, sharing the default entry’s verdict.
Source: codex-rs/core/src/session/turn_input.rs lines 141–156, lines 195–249
Settings cannot change before the verdict. prepare previews thread settings first. A failed preview is InvalidRequest. Real writes happen in apply_started or apply_steered. A refused input does not even change settings. A successful interrupt only persists settings. This turn’s TurnContext does not swap. Start-only options, like a structured-output schema, apply only on Started.
Source: codex-rs/core/src/session/turn_input.rs lines 58–80
Start and steer must finish under the same lock. Check for an active turn, then kind, then write pending. Midway, another submit must not swap the turn. Settings preview first, write second, because a refuse path must leave the thread unchanged. Verdict and enqueue are two lock takes. When Enter and a sub-mail arrive together, you can get a window that looked idle at judge time and occupied at enqueue time.
A review task opens its own one-shot child conversation. A compact task is swapping the window. If the user adds “use YAML” now, no current-turn tool loop can catch it. Open a regular turn for it and the review result and compact summary fight the new conversation for the same active_turn.
steer_input does every check under the active_turn lock. No active turn, or a slot with no task, is NoActiveTurn. TaskKind has three variants: Regular, Review, Compact. Review and compact return ActiveTurnNotSteerable. StartOrSteer will not start work because of that.
Source: codex-rs/core/src/session/turn_input.rs lines 507–519; codex-rs/core/src/state/turn.rs lines 67–72
After the checks pass, user input is pushed into pending_input and the mailbox phase flips back to CurrentTurn. Exhaustive match has a product cost here: add a new task kind and the compiler forces you to say whether it can take an interrupt.
Source: codex-rs/core/src/session/turn_input.rs lines 546–564
Internal tasks have their own lifetime. Weld a user interrupt into a review turn and two jobs fight one exec slot. The caller gets a refuse. This input will not be silently queued into a new conversation. Rewrite in another language and the rule stays: tasks that can take an interrupt and tasks that cannot must be split in the type system.
The main agent already printed something that looks like a final answer. A sub-agent sends progress at the same time. Merge them and the answer the user already saw gets continued. Always wait for the next turn and the sub-agent result may skip a sample before it reaches the model.
A user interrupt enters in-turn pending_input. A sub-agent letter goes to session-level mailbox_pending_mails. Whether the two stocks merge into this turn is MailboxDeliveryPhase. The phase starts at CurrentTurn. After the user has seen a final answer it flips to NextTurn. One more user interrupt, or another tool call, reopens the phase.
Source: codex-rs/core/src/state/turn.rs lines 37–56
Flipping to NextTurn has one exception: if pending_input still has a sub-mail that is not “queue and don’t wake,” the current phase stays. Taking mail looks at this card too. On NextTurn, in-turn pending is not taken, and the session mailbox is not emptied. On CurrentTurn, take in-turn pending first, then empty the session mailbox and append it. So after a final answer lands, a sub-mail can lie in the mailbox while the loop thinks there is no pending input, and this turn closes.
Source: codex-rs/core/src/session/input_queue.rs lines 206–227, lines 284–336
What counts as a user-visible final answer? Assistant body text. phase Commentary does not count, nor does a trim-empty body. An unlabeled assistant message is treated as a final answer. An unlabeled provider defaults to the safer path: close the mailbox to the next turn first. Approval, permission, ask, elicitation, and dynamic tools are five separate oneshot tables. They do not enter this mailbox. Each waits for its own receipt.
Source: codex-rs/core/src/stream_events_utils.rs lines 486–501; codex-rs/core/src/state/turn.rs lines 87–103
“The user already saw the answer” is a product boundary. It does not depend on Rust or a mailbox implementation. Rewrite in another language and you still need a flip-card: late sidecar messages do not continue an on-screen answer by default. Clear same-turn work — another user interrupt, another tool call — opens the door again.
DSH: two parameters, two tracks
DSH exposes three aliases on one send. Target queue and whether to wake are two orthogonal parameters. followup owns a turn and wakes. steer inserts at the next stop and wakes. inject boards without nudging the driver. Inbox is two durable lists, next-turn and next-step. claim empties next-step first, and takes one more when the target is next-turn.
Codex has no three public functions. StartOrSteer welds idle-start and busy-steer into one verdict. DSH can skip the flip-card because wait-for-the-turn and wait-for-the-next-stop are two lists. The cost: after the answer is on screen, a non-empty next-step still keeps the current Turn alive.
Claude Code: one queue, priority fills in the timing
In the reconstructed source, user input, task notices, and orphan permissions share one commandQueue. Priority is now > next > later, FIFO inside a level. User commands default to next, task notices to later, so user input is not starved by system messages. Consume happens after the current stream ends. There is no step-level interrupt. That YAML constraint you added waits for the current generator to finish before it reaches the model.
After the answer is on screen, when is this sub-mail seen?
After the final answer lands, enqueue a trigger_turn: false sub-mail, then feed a FunctionCall. On paper, walk what get_pending_input should return.
The path to check: when the body persists, the phase flips to NextTurn and this letter is invisible. After a tool item arrives the phase reopens, and only the next lap can take it into the same turn’s next model request.
The handoffs inside “Play first · The same sentence, four moments”
“Three everyday endings all sting.” 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 “Codex folds this into one entry and three modes.”, 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”.
- Dispatch on TurnInputMode, default StartOrSteer turn_input.rs L141
- Try steer_input first; only NoActiveTurn starts work turn_input.rs L195
- Only Regular accepts. Review and Compact refuse on the spot. turn_input.rs L507
A happy path is not reliability
Use “The path to check: when the body persists, the phase flips to NextTurn and this letter is invisible.” 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 · The same sentence, four moments” to “This turn”
“Play first · The same sentence, four moments” grounds the problem in “Mailbox sort desk: the same constraint, different drop times, different landings Play Step Reset Moment Idle Sampling Answer on screen In review Four moments share the same input. Idle starts work. Sampling ste…”. “This turn” then moves it toward “pending_input, taken when the phase is CurrentTurn Empty”. 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 · The same sentence, four moments”: Mailbox sort desk: the same constraint, different drop times, different landings Play Step Reset Moment Idle Sampling Answer on screen In review Four moments share the same input. Idle starts work. Sampling ste…
- “This turn”: pending_input, taken when the phase is CurrentTurn Empty
- “The closing point”: An interrupt writes pending and flips the phase back to CurrentTurn turn_input.rs L558
The final “The closing point” brings the discussion to “An interrupt writes pending and flips the phase back to CurrentTurn turn_input.rs L558”. 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.