openai / openai/codex

High-reasoning Responses WebSocket idle timeout is a false dead-stream (model resumed after 8.5 min of no text frames)

Open
#39,771 5 comments 3 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

app bug connectivity
Dominant language
Rust
Stars
125k
Forks
19.4k
PR merge metrics
PR metrics pending

Description

What version of the Codex App are you using (From “About Codex” dialog)?

ChatGPT desktop 26.814.41407 (bundled codex-cli 0.148.0-alpha.15)

What subscription do you have?

ChatGPT Pro (plan_type=pro in local rate-limit snapshots). Auth mode: ChatGPT (not API key).

What platform is your computer?
Darwin 24.6.0 arm64 arm
macOS 15.7.7 (24G720)
What issue are you seeing?

On gpt-5.6-sol with reasoning_effort=high, a turn that is still generating can go several minutes with no Responses text frames. The client treats that silence as a dead websocket:

WARN codex_core::responses_retry
stream disconnected - retrying sampling request (n/5) ...
sampling_error=stream disconnected before completion: idle timeout waiting for websocket

The UI stays active with frozen token counts. There is no user-visible disconnect. After 5 silent retries the turn is wasted. Interrupt shows turn_aborted.

This is not (only) a dropped TCP connection. Protocol ping/pong do not reset the idle timer (they are swallowed in the websocket pump and never reach ws_stream.next()). The watchdog is tokio::time::timeout(stream_idle_timeout, ws_stream.next()) with default stream_idle_timeout_ms = 300_000. Only a text frame resets it.

stream_idle_timeout_ms / supports_websockets cannot be changed for the built-in openai provider (model_providers.openai is reserved / or_insert ignores overrides).

What steps can reproduce the bug?
  1. Codex Desktop, ChatGPT login, model gpt-5.6-sol, reasoning high.
  2. Run a long turn: many exec calls, then a long think/write (large patch / gate file). Transport is wss://chatgpt.com/backend-api/codex/responses.
  3. After the last commentary + tool result, the model goes quiet. No agent_reasoning / tool / token snapshot advances for >5 minutes.
  4. Client logs idle timeout waiting for websocket and retries the entire sampling request (1/55/5). Chat still looks “working”.

Controlled experiment (same machine, same thread, same model): a localhost reverse proxy in front of https://chatgpt.com/backend-api/codex injected a text frame every 45s while upstream was silent:

{"type":"codex.client.keepalive"}

Codex accepted the frames and ignored them as unknown events:

DEBUG codex_api::sse::responses
unhandled responses event: "codex.client.keepalive"

No idle timeout fired. After ~8.5 minutes of no real model output (18:06:20Z18:14:56Z on 2026-08-20), the same in-flight sampling request emitted a real custom_tool_call / patch_apply_end and continued. Frozen token counts then moved.

That is the opposite of a dead stream: the websocket and the server were still in the turn; the 300s text-frame watchdog was a false positive.

Earlier incident on the same thread without the keepalive (same day):

UTC What happened
17:00:02 Last real commentary (“now freezing the … gate”)
17:07:34 / 17:13:11 / 18:18:50 idle timeout waiting for websocket retries 1/5, 2/5, 3/5
17:21:49 User interrupt; turn_aborted; promised file never written

Thread id: 01a02010-e5b0-75f1-b3b7-3e0c54879f59
Turn (keepalive run): 01a02059-3cbe-70d2-9c1b-881ae7f1cfa4
Context at last good request: ~99k / 258400 input tokens (~38%). rate_limit_reached_type was null (weekly usage was high, ~95%, but not marked reached).

Same idle timeout waiting for websocket pattern on other gpt-5.6-sol high/medium Desktop turns the same day (websocket closed by server before response.completed and Connection reset without closing handshake also occur, but those are distinct).

What is the expected behavior?
  • Do not treat in-progress high-reasoning silence (no Responses text frames, websocket still open) as a dead stream.
  • Server or client should emit a liveness text/event that the idle waiter counts, or the idle timer should not apply until the socket is actually closed / ping fails.
  • If you keep a 300s watchdog: surface it in the UI (“waiting for model, last frame Ns ago”) instead of silent retrying sampling request (n/5) with frozen tokens.
  • Allow stream_idle_timeout_ms (and/or websocket opt-out) on the built-in ChatGPT openai provider. Today the only documented knobs are on custom providers.

A faster dead-stream detector (#38638, #17003) would have killed this turn at 5 minutes. The model resumed at 8.5 minutes. Faster detection of true RSTs is still good; collapsing “no text frames” into “dead” is not.

Additional information

Related: #38638, #27625, #28579, #24260, #23807, #26696, #33051.

#23807 is a 300s stall between completed responses (turn state machine). This report is idle timeout during an open sampling stream while gpt-5.6-sol high is thinking and sending no text.

#38638 matches the log line and the “cannot override built-in provider” constraint; the keepalive experiment is extra evidence that the 300s timeout is often a false dead-stream, not only slow detection of a real drop.

Code references on main (behavior matches 0.148.0-alpha.15):

  • DEFAULT_STREAM_IDLE_TIMEOUT_MS = 300_000 in codex-rs/model-provider-info/src/lib.rs
  • timeout(idle_timeout, ws_stream.next())"idle timeout waiting for websocket" in codex-rs/codex-api/src/endpoint/responses_websocket.rs
  • Ping/Pong handled inside the pump and not forwarded to next()
  • Unknown JSON events: parse failure or unhandled responses eventcontinue (timer already reset)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with codex-rs/model-provider-info/src/lib.rs and codex-rs/codex-api/src/endpoint/responses_websocket.rs, focusing on DEFAULT_STREAM_IDLE_TIMEOUT_MS, the timeout around ws_stream.next(), and ping/pong handling in the websocket pump. Trace how silent but open streams and genuine disconnects are distinguished, then verify that a long reasoning pause can resume without retrying while true dead streams remain detectable.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
backend-api-design, networking
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.