CLI reuses dead Responses WebSocket after network loss during idle, breaking the next turn
What version of Codex CLI is running?
codex-cli 0.144.4
The same liveness logic is still present on the current main branch as of 2026-07-15.
What subscription do you have?
ChatGPT account authentication. The specific subscription tier does not appear material to this transport-liveness failure.
Which model were you using?
gpt-5.6-sol
What platform is your computer?
Linux 6.8.0-124-generic x86_64 x86_64
Ubuntu 22.04, npm installation.
What terminal emulator and version are you using (if applicable)?
VTE 6800, TERM=xterm-256color. No terminal multiplexer was involved.
Codex doctor report
A full report is not included because the available diagnostic run occurred inside a network-restricted harness after applying a local prototype patch, so its reachability result is not representative of the original failure.
Relevant redacted fields:
{
"schemaVersion": 1,
"codexVersion": "0.144.4",
"authMode": "chatgpt",
"model": "gpt-5.6-sol",
"modelProvider": "openai",
"proxyEnvVars": "none",
"platform": "linux-x86_64",
"installMethod": "npm"
}
What issue are you seeing?
An already-open interactive CLI chat can stop continuing normally after this sequence:
- A turn completes.
- The chat remains open but idle.
- Internet connectivity is interrupted long enough for the Responses WebSocket pump to exit.
- Connectivity returns before the user sends another prompt.
- The next prompt is sent in the same open chat.
This is not about codex resume, restoring a process, or reconnecting during an active streamed response. The process and TUI remain open throughout; the failure occurs on the first ordinary turn after an idle network interruption.
The cached cross-turn Responses WebSocket is treated as live even though its background pump task has already terminated. The next turn therefore attempts to reuse a dead connection and can fail instead of transparently opening a new connection and continuing with the existing conversation history.
The source-level failure path appears to be:
ModelClientSessioncaches aResponsesWebsocketConnectionacross turns.WsStreamowns a command channel and a backgroundpump_task.- The pump exits when the idle socket encounters a transport error/EOF.
ResponsesWebsocketConnection::is_closed()currently only checks whetherstream: Option<WsStream>isNone.- A finished pump can remain stored as
Some(WsStream), so the cached connection is incorrectly reported as open.
Current implementation:
pub async fn is_closed(&self) -> bool {
self.stream.lock().await.is_none()
}
What steps can reproduce the bug?
- Start an interactive Codex CLI session using the Responses WebSocket transport.
- Send a prompt and wait until the turn completes.
- Leave the CLI open and idle.
- Disconnect the machine from the internet long enough for the underlying WebSocket pump to observe EOF or a transport error and exit.
- Restore internet connectivity without closing or resuming the Codex process.
- Send another prompt in the same open chat.
- Observe that the next turn can fail/break continuity because the cached dead WebSocket is reused.
A deterministic regression test can avoid manipulating the host network:
- Script a WebSocket server to accept the first connection and complete one response.
- Close that connection while the client is idle.
- Wait for the client pump task to finish.
- Submit a second turn through the same
ModelClientSession. - Assert that the client creates a second WebSocket connection.
- Assert that the new request does not reuse a
previous_response_idtied to the dead connection and instead carries the full prompt history needed to continue the conversation.
What is the expected behavior?
When the background WebSocket pump has exited, the cached connection should be considered closed.
The next prompt in the still-open chat should transparently establish a fresh Responses WebSocket and continue the same conversation using the existing retry/full-history path. The user should not need to restart or resume the session.
Additional information
A minimal fix is to include pump/channel liveness in the cached-connection check:
impl WsStream {
fn is_closed(&self) -> bool {
self.tx_command.is_closed() || self.pump_task.is_finished()
}
}
pub async fn is_closed(&self) -> bool {
self.stream
.lock()
.await
.as_ref()
.is_none_or(WsStream::is_closed)
}
A local prototype with this change compiles successfully as a 0.144.4 release and passes CLI --version / --help smoke checks. A regression test was also prepared for the two-turn/two-connection behavior, but the test suite was not run locally.
Duplicate searches performed before filing included:
- “websocket idle internet disconnect conversation continuity”
- “network disconnect inactivity stale websocket next turn”
- “chat session breaks after internet reconnect idle”
- source identifiers
ResponsesWebsocketConnection,pump_task, andis_closed
No matching issue or pull request was found. Related reports about remote-control enrollment, app-server client state, active-stream disconnect loops, or IPv6 handshake fallback concern different transports or failure timing.