High-reasoning Responses WebSocket idle timeout is a false dead-stream (model resumed after 8.5 min of no text frames)
Nobody has claimed this yet.
- 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?
- Codex Desktop, ChatGPT login, model
gpt-5.6-sol, reasoning high. - Run a long turn: many
execcalls, then a long think/write (large patch / gate file). Transport iswss://chatgpt.com/backend-api/codex/responses. - After the last commentary + tool result, the model goes quiet. No
agent_reasoning/ tool / token snapshot advances for >5 minutes. - Client logs
idle timeout waiting for websocketand retries the entire sampling request (1/5…5/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:20Z → 18: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 ChatGPTopenaiprovider. 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_000incodex-rs/model-provider-info/src/lib.rstimeout(idle_timeout, ws_stream.next())→"idle timeout waiting for websocket"incodex-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 event→continue(timer already reset)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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