Skip to content

fix(codex): preserve terminal WebSocket API errors - #693

Closed
yxlyx wants to merge 2 commits into
mainfrom
fix/692-codex-ws-api-error
Closed

fix(codex): preserve terminal WebSocket API errors#693
yxlyx wants to merge 2 commits into
mainfrom
fix/692-codex-ws-api-error

Conversation

@yxlyx

@yxlyx yxlyx commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

What changed

  • Treat every authoritative Responses WebSocket type:error frame as a terminal API response, retain its accumulated SSE-shaped body, and mark the server-closing socket dead instead of waiting for EOF.
  • Parse and expose a single bounded, single-line provider/code/message diagnostic in last_api_error and the default trace without copying the raw frame envelope, request, headers, or auth-adjacent fields.
  • Rebuild full input exactly once when an error rejects an existing previous_response_id chain; deterministic generic API errors return immediately without consuming the WS retry/SSE fallback ladder.
  • Preserve that diagnostic through the fullscreen TUI turn bridge rather than replacing it with the misleading generic API-key failure row.
  • Extend the loopback WS mock and interactive PTY suite with immediate-close generic-error and stale-chain trajectories, plus headless fullscreen TUI recovery coverage.

Why

Problem / failure mode

Codex sends a useful type:error frame and then closes the WebSocket. Graff accumulated the frame but recognized only two special error codes as terminal. For a generic error it waited for another frame, interpreted the expected close as transport loss, discarded the API body, redialed a fresh WS, and finally replayed the unchanged deterministic request over SSE. Users saw only Bad Request, the trace lost the actual code/message, and the session unnecessarily latched off WS. The fullscreen TUI then replaced API failures with an API-key troubleshooting row.

Reason for this approach

The error frame is an authoritative provider response, so it belongs on the existing Responses parser/API-error path rather than the transport ladder. That keeps one shared parser for WS and SSE, preserves structured recovery such as stale-chain detection, and gives diagnostics one explicit privacy/size boundary before they reach traces or UI. Capturing whether a chain existed before retiring the dead socket allows a safe one-shot full-input rebuild without retrying generic 400 bodies.

Constraints and trade-offs

  • Error frames are parsed only after a cheap error candidate check; this adds allocation only on rare candidate frames and avoids brittle substring classification when JSON contains whitespace or quoted prose.
  • The diagnostic intentionally keeps only bounded provider, code, and message fields. Additional raw envelope detail is omitted to avoid leaking echoed request/auth material.
  • A stale-chain error on a fresh full-input request is surfaced instead of retried indefinitely. Automatic recovery is limited to a request that actually used an existing chain.
  • The fullscreen UI regression uses the repository's headless TUI/sim.zig path; the provider/socket half is exercised independently through the real interactive PTY runtime and loopback backend.

Rejected alternatives

  • Retrying every generic 400 over another WS or SSE was rejected because the request body is unchanged and the issue traces show that ladder deterministically fails.
  • Waiting for a WebSocket close frame was rejected because Codex closes the transport after the error event; EOF is expected protocol aftermath, not evidence that the API response was lost.
  • Logging the raw error frame was rejected because envelopes may contain request or authentication-adjacent data and can be unbounded.
  • Injecting a synthetic user continue was rejected because it changes conversation semantics; a recognized stale chain can be recovered safely by rebuilding the same full input instead.

Verification

  • scripts/eval-tier1.sh — green: format, line ceiling, spec, reachability, build, 1,760 tests passed (1 skipped), 460/460 TUI tests, 17/17 TUI guard probes, invariants, SDK.
  • python3 scripts/test-pty-codex-ws.py ./zig-out/bin/graff — all eight healthy/retry/fallback/error/chain/compaction trajectories passed.
  • Five independent subagents separately validated frame parsing edges, immediate-close classification, stale-chain recovery and request counts, fullscreen simulator recovery, and transport/compaction regressions.

Fixes #692

yxlyx and others added 2 commits August 31, 2026 00:04
Codex closes its Responses socket immediately after a type:error frame. Graff previously waited for another frame, discarded the accumulated API body on EOF, and retried an unchanged deterministic request across fresh WS and SSE transports.

Classify authoritative error frames as terminal API responses, retire the closing socket without charging the transport ladder, and allow only recognized stale chains to rebuild once with full input. Bound parsed code/message diagnostics before tracing or displaying them, including through the fullscreen TUI.

Add loopback, interactive PTY, stale-chain, diagnostic-redaction, and headless TUI regressions while retaining the existing fallback and compaction trajectories.

Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
The local Zig 0.16 compiler accepted a repeated string expression in the new diagnostic test, but the repository's pinned Zig 0.17 CI compiler rejects it during parsing on both Linux and Windows. Build the same long message with the repository's established @Splat pattern so the test remains portable across both toolchains.

Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
@justrach

Copy link
Copy Markdown
Owner

Folded into release/v0.0.282 (#696). Closing as superseded.

@justrach justrach closed this Aug 31, 2026
@cursor
cursor Bot deleted the fix/692-codex-ws-api-error branch August 31, 2026 01:30
yermakoffivan pushed a commit to yermakoffivan/codegraff that referenced this pull request Aug 31, 2026
…API errors

yxlyx: treat Responses type:error as a terminal API response, keep a
bounded last_api_error, and rebuild full input once on a stale chain.
yermakoffivan pushed a commit to yermakoffivan/codegraff that referenced this pull request Aug 31, 2026
…justrach#694

Record the three product merges on the new release branch, remap the
TLS-generation ADR to 0048 so 0042 stays TUI claims, and leave justrach#277 /
justrach#200 parked.
yermakoffivan pushed a commit to yermakoffivan/codegraff that referenced this pull request Aug 31, 2026
justrach#693 pushed the request loop to 602 lines. Move keep-alive retry and
scratch-arena reset into agent_request_scratch.zig and ratchet the
suite floor to 1817 for the yxlyx paste / WS / TLS tests.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants