Skip to content

codex: generic Bad Request frame is misclassified as WS transport failure; next continue succeeds #692

Description

@yxlyx

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:

  1. A prior request completes successfully.
  2. The next request reuses the WS and receives a first frame.
  3. Graff labels the close as a transport error and redials a fresh WS with the same full body.
  4. The fresh WS also receives a first frame and closes.
  5. Graff permanently falls back to SSE for the session.
  6. SSE rejects the identical request with a 25-byte response and the turn ends as codex api error: Bad Request.
  7. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions