Problem / failure mode
Codex tool-heavy turns repeatedly end with codex api error: Bad Request. Re-prompting with continue succeeds immediately, but graff spends a WS redial and SSE fallback first and loses the useful error-frame detail.
This is a TUI runtime/provider-path issue, not presentation-only behavior.
Evidence
Six independent trajectories contain this same failure, always on the first root turn; recorded follow-up continue turns succeed:
.graff/trajectories/8997e379e8f1e9a6.jsonl:6-9
.graff/trajectories/a3334f40111960cd.jsonl:6-9
.graff/trajectories/78e55567de819be8.jsonl:6-9
.graff/trajectories/21cc986aa0d5f15a.jsonl
.graff/trajectories/5e7420d48dfb2b6d.jsonl
.graff/trajectories/188d2d8042e4e325.jsonl:6-9
The wire sequence is effectively identical across request sizes from ~38 KB to ~149 KB. The current run shows it directly at .graff/traces/188d2d8042e4e325.jsonl:84-104:
- A prior request completes successfully.
- The next request reuses the WS and receives a first frame.
- Graff labels the close as a transport error and redials a fresh WS with the same full body.
- The fresh WS also receives a first frame and closes.
- Graff permanently falls back to SSE for the session.
- SSE rejects the identical request with a 25-byte response and the turn ends as
codex api error: Bad Request.
- The user's next prompt succeeds on the same process/session/history.
The same sequence appears at .graff/traces/8997e379e8f1e9a6.jsonl:122-132, .graff/traces/a3334f40111960cd.jsonl:42-52, and .graff/traces/78e55567de819be8.jsonl:49-59.
Why this needs a graff fix
postResponsesWs appends every frame to the accumulated response, but errorFrameAction only recognizes two specific error codes (websocket_connection_limit_reached and previous_response_not_found). A generic {"type":"error",...} frame is not terminal, so graff waits for another frame; when the server closes, the accumulated API error is discarded and the event is treated as a transport reset (src/agent_ws.zig:579-599, src/agent_ws_signal.zig:135-142).
That explains the misleading WS retry/fallback and lost diagnostics. The fact that a fresh full-history WS and SSE both reject the exact same body shows this is a request/API rejection, not a flaky socket. The remaining root cause—invalid request shape versus a transient Codex backend rejection—cannot be determined because the generic WS error frame and 25-byte SSE body are not preserved in the trace.
Expected behavior
- Treat any terminal Codex WS
type:error frame as an API response and preserve a bounded, redacted code/message in the trace/last_api_error; do not call it a transport failure merely because the server closes after the error frame.
- Once the actual code/message is visible, identify and fix the malformed or stale request shape. If adding a bare user
continue is sufficient to recover, graff should perform the equivalent safe re-anchor/rebuild automatically rather than fail the turn.
- Do not retry deterministic 400 bodies over two WS connections plus SSE without changing the request.
Regression coverage
Extend the Codex WS mock to send a generic error frame and close. Assert that graff returns the API error once, retains bounded diagnostics, does not burn the transport-failure ladder, and that any recognized recoverable chain error rebuilds full input automatically. Exercise this through the fullscreen TUI simulator/PTY path as well as the lower-level WS parser.
Problem / failure mode
Codex tool-heavy turns repeatedly end with
codex api error: Bad Request. Re-prompting withcontinuesucceeds immediately, but graff spends a WS redial and SSE fallback first and loses the useful error-frame detail.This is a TUI runtime/provider-path issue, not presentation-only behavior.
Evidence
Six independent trajectories contain this same failure, always on the first root turn; recorded follow-up
continueturns succeed:.graff/trajectories/8997e379e8f1e9a6.jsonl:6-9.graff/trajectories/a3334f40111960cd.jsonl:6-9.graff/trajectories/78e55567de819be8.jsonl:6-9.graff/trajectories/21cc986aa0d5f15a.jsonl.graff/trajectories/5e7420d48dfb2b6d.jsonl.graff/trajectories/188d2d8042e4e325.jsonl:6-9The wire sequence is effectively identical across request sizes from ~38 KB to ~149 KB. The current run shows it directly at
.graff/traces/188d2d8042e4e325.jsonl:84-104:codex api error: Bad Request.The same sequence appears at
.graff/traces/8997e379e8f1e9a6.jsonl:122-132,.graff/traces/a3334f40111960cd.jsonl:42-52, and.graff/traces/78e55567de819be8.jsonl:49-59.Why this needs a graff fix
postResponsesWsappends every frame to the accumulated response, buterrorFrameActiononly recognizes two specific error codes (websocket_connection_limit_reachedandprevious_response_not_found). A generic{"type":"error",...}frame is not terminal, so graff waits for another frame; when the server closes, the accumulated API error is discarded and the event is treated as a transport reset (src/agent_ws.zig:579-599,src/agent_ws_signal.zig:135-142).That explains the misleading WS retry/fallback and lost diagnostics. The fact that a fresh full-history WS and SSE both reject the exact same body shows this is a request/API rejection, not a flaky socket. The remaining root cause—invalid request shape versus a transient Codex backend rejection—cannot be determined because the generic WS error frame and 25-byte SSE body are not preserved in the trace.
Expected behavior
type:errorframe as an API response and preserve a bounded, redacted code/message in the trace/last_api_error; do not call it a transport failure merely because the server closes after the error frame.continueis sufficient to recover, graff should perform the equivalent safe re-anchor/rebuild automatically rather than fail the turn.Regression coverage
Extend the Codex WS mock to send a generic error frame and close. Assert that graff returns the API error once, retains bounded diagnostics, does not burn the transport-failure ladder, and that any recognized recoverable chain error rebuilds full input automatically. Exercise this through the fullscreen TUI simulator/PTY path as well as the lower-level WS parser.