Problem
observe_turn_completed has no arm for Held{SystemError}, so a turn that ends in an error is
recorded as Held{ConflictingTurn} — two turns believed live at once. That is false, and it is
delivery-gating.
ConflictingTurn is then preserved by every other transition. observe_thread_status("active")
preserves it (src/codex_app_server.rs:773-781) and observe_turn_completed preserves it
(src/codex_app_server.rs:841-844). Only an idle status clears it.
maybe_request returns None for every Held{..} (src/codex_app_server.rs:415-421), so the agent
accepts no native delivery for as long as the wrong state stands.
Fleet evidence
We are a downstream consumer running one catalog with 627 seats (measured 2026-08-16, #261).
A usage limit is an account-level condition, so it hits every agent on that account at the same
moment. One recoverable error therefore converts a set of agents into agents that silently stop
accepting messages, and the state they report — conflicting turns — gives the operator no way to
tell why. The agents look busy. Nothing is running.
Stated as a mechanism, not as an incident count. We have not measured how many seats have hit
this. What is measured is the state machine: two independent captures, both ending in the same wrong
state, from an error payload that is ordinary.
Cause
src/codex_app_server.rs:826 (observe_turn_completed) enumerates arms for Idle,
Active{matching}, Held{Review|Compaction}, AwaitingStatus | Held{ActiveWithoutTurn}, and
Held{ConflictingTurn}. Held{SystemError} and Held{NotLoaded} match none of them and fall to the
trailing _ => Held{reason: ConflictingTurn, turn_id: None} (:845-848).
observe_thread_status mints Held{SystemError} on the "systemError" status word
(src/codex_app_server.rs:791-794), and a terminal error emits thread/status/changed → systemError
immediately before turn/completed. This is the ordinary path for a failed turn, not a corner case.
Separately, and worth fixing in the same pass: observe_thread_status's trailing _ arm
(:795-798) folds every unrecognized status word into SystemError. A status word Codex adds
tomorrow surfaces to an operator as a system error that did not happen.
Measured result
Two captured codex app-server streams from codex-cli 0.147.0, replayed through the actual
CodexControlState::observe in a cargo test harness. Both runs, independently captured, ended
identically:
t= 5655.3ms thread/status/changed -> Held { reason: SystemError, turn_id: None }
t= 5655.7ms turn/completed -> Held { reason: ConflictingTurn, turn_id: None }
The triggering error was ordinary:
{"error":{"message":"You've hit your usage limit…","codexErrorInfo":"usageLimitExceeded"},
"willRetry":false}
and turn/completed carried "status": "failed" with the same error attached.
Version scope. The captures are codex-cli 0.147.0.
SUPPORTED_CODEX_CLI_VERSIONS = ["codex-cli 0.145.0", "codex-cli 0.146.0"]
(src/codex_app_server.rs:38, enforced at :2101), so st2 rejects the version these frames came
from. The defect is in st2's own match arms and does not depend on the wire version; the capture does.
Confirming the same systemError → turn/completed ordering on 0.145 or 0.146 would remove that
caveat.
Required behavior
A turn that ends in a system error resolves to a state that reflects the error. It does not report
conflicting turns. Whether that state gates delivery is a deliberate decision, not a fall-through.
Acceptance
Problem
observe_turn_completedhas no arm forHeld{SystemError}, so a turn that ends in an error isrecorded as
Held{ConflictingTurn}— two turns believed live at once. That is false, and it isdelivery-gating.
ConflictingTurnis then preserved by every other transition.observe_thread_status("active")preserves it (
src/codex_app_server.rs:773-781) andobserve_turn_completedpreserves it(
src/codex_app_server.rs:841-844). Only anidlestatus clears it.maybe_requestreturnsNonefor everyHeld{..}(src/codex_app_server.rs:415-421), so the agentaccepts no native delivery for as long as the wrong state stands.
Fleet evidence
We are a downstream consumer running one catalog with 627 seats (measured 2026-08-16, #261).
A usage limit is an account-level condition, so it hits every agent on that account at the same
moment. One recoverable error therefore converts a set of agents into agents that silently stop
accepting messages, and the state they report — conflicting turns — gives the operator no way to
tell why. The agents look busy. Nothing is running.
Stated as a mechanism, not as an incident count. We have not measured how many seats have hit
this. What is measured is the state machine: two independent captures, both ending in the same wrong
state, from an error payload that is ordinary.
Cause
src/codex_app_server.rs:826(observe_turn_completed) enumerates arms forIdle,Active{matching},Held{Review|Compaction},AwaitingStatus | Held{ActiveWithoutTurn}, andHeld{ConflictingTurn}.Held{SystemError}andHeld{NotLoaded}match none of them and fall to thetrailing
_ => Held{reason: ConflictingTurn, turn_id: None}(:845-848).observe_thread_statusmintsHeld{SystemError}on the"systemError"status word(
src/codex_app_server.rs:791-794), and a terminal error emitsthread/status/changed → systemErrorimmediately before
turn/completed. This is the ordinary path for a failed turn, not a corner case.Separately, and worth fixing in the same pass:
observe_thread_status's trailing_arm(
:795-798) folds every unrecognized status word intoSystemError. A status word Codex addstomorrow surfaces to an operator as a system error that did not happen.
Measured result
Two captured
codex app-serverstreams from codex-cli 0.147.0, replayed through the actualCodexControlState::observein acargo testharness. Both runs, independently captured, endedidentically:
The triggering error was ordinary:
{"error":{"message":"You've hit your usage limit…","codexErrorInfo":"usageLimitExceeded"}, "willRetry":false}and
turn/completedcarried"status": "failed"with the same error attached.Version scope. The captures are codex-cli 0.147.0.
SUPPORTED_CODEX_CLI_VERSIONS = ["codex-cli 0.145.0", "codex-cli 0.146.0"](
src/codex_app_server.rs:38, enforced at:2101), so st2 rejects the version these frames camefrom. The defect is in st2's own match arms and does not depend on the wire version; the capture does.
Confirming the same
systemError→turn/completedordering on 0.145 or 0.146 would remove thatcaveat.
Required behavior
A turn that ends in a system error resolves to a state that reflects the error. It does not report
conflicting turns. Whether that state gates delivery is a deliberate decision, not a fall-through.
Acceptance
Held{SystemError}+turn/completedresolves to a state that names the error, notConflictingTurn.Held{NotLoaded}+turn/completedlikewise.observe_thread_status's unrecognized-status-word arm is distinguishable from a realsystemError.systemError→turn/completedsequence.