codex exec --json drops typed codexErrorInfo from terminal errors
Summary
codex exec --json drops the existing typed codexErrorInfo from both top-level error events and nested turn.failed.error events. Downstream automation therefore receives only a human-readable message and cannot reliably distinguish an authentication failure that requires login from transport, runtime, quota, or other failures without parsing prose.
The Core -> App Server path already carries CodexErrorInfo. The information is lost only in the codex-exec JSONL projection.
Current behavior
This is present in both:
rust-v0.146.0(e363b08c9175ac1cbe5893615dd2cb9ddf95043b)- current
maininspected at2b5bdcf67547860f2e5c5a605009a70026796b2b
Relevant source path:
codex-rs/protocol/src/error.rsmaps a permanent refresh-token failure toCodexErrorInfo::Unauthorized.- App Server v2
TurnErrorretainscodex_error_info: Option<CodexErrorInfo>. codex-rs/exec/src/exec_events.rsdefinesThreadErrorEventwith onlymessage.codex-rs/exec/src/event_processor_with_jsonl_output.rsprojects bothErrorNotification.errorandTurnCompleted.turn.errorinto that message-only type.
As a result, a consumer sees output shaped like:
{"type":"turn.failed","error":{"message":"..."}}
even when App Server supplied codexErrorInfo: "unauthorized".
Expected behavior
Preserve the existing typed classification as an optional additive field:
{"type":"turn.failed","error":{"message":"...","codex_error_info":"unauthorized"}}
Events without typed error information should keep their current serialized shape.
This would let deterministic consumers treat unauthorized as a coarse login-required signal while keeping absent or other classifications on their normal failure paths. It would not require consumers to infer expired/revoked/invalid subreasons from the message; the current unit Unauthorized variant does not expose those subreasons.
Minimal repair outline
- Add optional
codex_error_infotoThreadErrorEventincodex-rs/exec/src/exec_events.rs, usingskip_serializing_if = "Option::is_none"so untyped events remain byte-shape compatible. - Preserve the field in both error projections in
codex-rs/exec/src/event_processor_with_jsonl_output.rs; default fallback errors toNone. - Update the public TypeScript event types in
sdk/typescript/src/events.ts. - Extend
codex-rs/exec/tests/event_processor_with_json_output.rsto cover: - top-level
error; - nested
turn.failed.errorthrough the saved critical error; - direct
TurnCompleted.turn.error; - serialized JSON containing
"codex_error_info":"unauthorized"only when present.
This remains on the existing codex exec --json transport and only exposes information already carried by Core and App Server.
If this approach aligns with the intended public JSONL contract, an invitation to submit the focused patch would be welcome.
1 Comment
Production evidence from
codex-cli 0.146.0-alpha.9.2confirms this affects deterministic recovery, not only auth handling.In two separate one-page/full-batch calls, the API request was accepted (HTTP 200), native OTEL observed exactly one failed
response.completed, JSONL closed asturn.failed, and there were no API/HTTP/timeout/parser/cleanup failures or output tokens. The same frozen audit input later succeeded, while a different phase then reproduced the same terminal shape. BecauseThreadErrorEventretains onlymessage, our content-free telemetry can classify this only asother; it cannot distinguishresponseStreamDisconnected,responseStreamConnectionFailed,internalServerError, or another typed variant.This matters for fail-closed automation: sealed runs must stop, while routine runs may retry only a proven transient. An optional typed field on both top-level
errorand nestedturn.failed.errorwould let consumers make that decision without retaining or parsing raw messages.Downstream evidence and acceptance boundary: https://github.com/snyk/a11y-agent/issues/221#issuecomment-5225621234