turn/steer can succeed without persisting input while sampling is in flight
What issue are you seeing?
turn/steer can return a successful response for the active turn while the submitted input remains only in the pending-input queue. If the current sampling request does not complete, Codex emits neither the correlated steered-user item/started nor item/completed lifecycle before the run is stopped.
I observed this through app-server v2 with rust-v0.148.0 (3ba0f711642a888aec92a611a3f3b2211157ff89) on macOS arm64. Source inspection shows the relevant acceptance and sampling ordering is unchanged in the latest rust-v0.149.1 release and current main (3e4707b34b16e139fcb7ad11ab8445993b62bba1).
In a bounded reproduction:
turn/startentered a regular active turn.- Exactly one
turn/steerrequest was sent withexpectedTurnIdandclientUserMessageId. - The server returned success with the expected active
turnId. - The active sampling request remained in flight for more than six minutes.
- No correlated steered-user
item/startedoritem/completedwas emitted before the run was stopped. - The client therefore could not distinguish durable acceptance from volatile queueing and treated the result as ambiguous/no-retry.
No raw prompt, model output, credential, workspace path, or identifier is included in this report.
What steps can reproduce the bug?
A deterministic model-server test can reproduce this without relying on provider timing:
- Start app-server and a regular turn against a controlled model server.
- Hold the turn's first sampling response open.
- While that sampling request is in flight, send one
turn/steerrequest with fixedexpectedTurnIdandclientUserMessageIdvalues. - Observe that
turn/steerreturns the activeturnIdsuccessfully. - Keep the first sample blocked and observe that no correlated user-message lifecycle is persisted or emitted.
- Release the sample and verify the steer is eventually observed exactly once.
The current source ordering explains the behavior:
turn_input.rsdocuments that the RPC replies after the start/steer decision and does not wait for hooks, model-context update, rollout persistence, or sampling.steer_inputextends pending input and returns the active turn ID.- In the normal turn loop,
can_drain_pending_inputbegins false for a non-empty turn, Core awaitsrun_sampling_request, and only afterward enables pending-input draining (same file).
This permits a successful response with no durable user-item lifecycle for as long as the current sampling request remains in flight.
What is the expected behavior?
An accepted steer should not remain indefinitely volatile behind one long-running sampling request. A successful turn/steer response should provide a durable, reconcilable acceptance boundary. For example, Codex could either:
- return success only after the steered input is durably recorded and its correlated lifecycle can be observed; or
- durably expose a queued-steer state keyed by
clientUserMessageId, while ensuring the active turn reaches a safe steer-processing boundary without waiting indefinitely for the current provider request.
The fix should avoid cancelling tool side effects, duplicating delivery, or manufacturing a user-item lifecycle before persistence.
Additional information
Related but distinct work:
- #32254 requests durable/idempotent acceptance keyed by
clientUserMessageId. - #30341 preserves a steer accepted during turn finalization.
- #29582 concerns model semantics after pending steer input is drained.
This report is specifically the pre-persistence liveness case: the RPC succeeds, but a long in-flight sample prevents the accepted input from reaching its correlated durable lifecycle.
A regression should also cover interrupt, finalization, retry with the same ID and payload, and conflicting reuse of the same ID with different input. I would be happy to prepare a focused patch if maintainers agree on the intended durable-acceptance contract and safe sampling boundary.