What version of Codex CLI is running?
v0.121.0
What subscription do you have?
None, API Keys
Which model were you using?
gpt-5.4
What platform is your computer?
Darwin 25.4.0 arm64 arm
What terminal emulator and version are you using (if applicable)?
Warp
What issue are you seeing?
The app-server's WebSocket transport disconnects remote TUI clients instantly when its 128-message outbound queue fills, instead of applying backpressure. This makes --remote mode unusable for any turn that produces moderate output volume.
The TUI exits with:
ERROR: remote app server at `ws://127.0.0.1:<port>/` transport failed:
WebSocket protocol error: Connection reset without closing handshake
The app-server logs:
disconnecting slow connection after outbound queue filled: ConnectionId(0)
This reproduces on stock upstream Codex with a single remote TUI client and no third-party tooling.
What steps can reproduce the bug?
Terminal 1 — start the app-server:
codex app-server --listen ws://127.0.0.1:0
# note the printed ws:// URL
Terminal 2 — attach the remote TUI:
codex --remote ws://127.0.0.1:<port>
Then send a prompt that produces moderate streaming output:
Run `rg -n '120|timeout' ~/.codex` and answer briefly.
The TUI disconnects within seconds. Smaller prompts sometimes survive, but any turn with realistic tool output triggers it.
What is the expected behavior?
The remote TUI should remain connected during normal turns. A client that falls 128 messages behind during streaming is momentarily slower than the producer, not stuck. it should not be terminated instantly.
Stdio clients already handle this correctly: they use blocking .send().await (stdio.rs:35 sets disconnect_sender: None), so they never hit the disconnect path. WebSocket clients should get equivalent resilience, either through a larger queue, a send timeout, or per-connection send tasks.
Additional information
root cause is in send_message_to_connection() at codex-rs/app-server/src/transport/mod.rs line 308-316. WebSocket clients are marked disconnectable (websocket.rs:176 sets disconnect_sender: Some(...)), so when try_send() returns TrySendError::Full on the 128-slot bounded channel, the connection is terminated immediately.
The outbound router is a single select! loop (lib.rs:643), so blocking on a slow WebSocket is not safe. But the remote_control transport already solves this differently, remote_control/websocket.rs uses per-stream BoundedOutboundBuffer with backpressure instead of instant disconnect.
Design options that preserve the non-blocking router:
- Per-connection send tasks. Structurally similar to
remote_control's existing approach.
- Larger queue + send_timeout. Simplest fix, small blast radius.
- Priority-based notification dropping under queue pressure.
opt_out_notification_methods already has the right shape.
The existing test broadcast_does_not_block_on_slow_connection (line 878) uses channel capacity 1 and validates instant disconnect as expected behavior. That test encodes the wrong contract — it should validate a grace period, not instant termination.
Related: #13949 (static analysis of same path), #15355 (local ingress feature request).
Reproduced on codex-cli 0.120.0 and 0.121.0, both stock upstream and with a third-party wrapper using --remote mode.
What version of Codex CLI is running?
v0.121.0
What subscription do you have?
None, API Keys
Which model were you using?
gpt-5.4
What platform is your computer?
Darwin 25.4.0 arm64 arm
What terminal emulator and version are you using (if applicable)?
Warp
What issue are you seeing?
The app-server's WebSocket transport disconnects remote TUI clients instantly when its 128-message outbound queue fills, instead of applying backpressure. This makes
--remotemode unusable for any turn that produces moderate output volume.The TUI exits with:
The app-server logs:
This reproduces on stock upstream Codex with a single remote TUI client and no third-party tooling.
What steps can reproduce the bug?
Terminal 1 — start the app-server:
codex app-server --listen ws://127.0.0.1:0 # note the printed ws:// URLTerminal 2 — attach the remote TUI:
Then send a prompt that produces moderate streaming output:
The TUI disconnects within seconds. Smaller prompts sometimes survive, but any turn with realistic tool output triggers it.
What is the expected behavior?
The remote TUI should remain connected during normal turns. A client that falls 128 messages behind during streaming is momentarily slower than the producer, not stuck. it should not be terminated instantly.
Stdio clients already handle this correctly: they use blocking
.send().await(stdio.rs:35setsdisconnect_sender: None), so they never hit the disconnect path. WebSocket clients should get equivalent resilience, either through a larger queue, a send timeout, or per-connection send tasks.Additional information
root cause is in
send_message_to_connection()atcodex-rs/app-server/src/transport/mod.rsline 308-316. WebSocket clients are marked disconnectable (websocket.rs:176setsdisconnect_sender: Some(...)), so whentry_send()returnsTrySendError::Fullon the 128-slot bounded channel, the connection is terminated immediately.The outbound router is a single
select!loop (lib.rs:643), so blocking on a slow WebSocket is not safe. But theremote_controltransport already solves this differently,remote_control/websocket.rsuses per-streamBoundedOutboundBufferwith backpressure instead of instant disconnect.Design options that preserve the non-blocking router:
remote_control's existing approach.opt_out_notification_methodsalready has the right shape.The existing test
broadcast_does_not_block_on_slow_connection(line 878) uses channel capacity 1 and validates instant disconnect as expected behavior. That test encodes the wrong contract — it should validate a grace period, not instant termination.Related: #13949 (static analysis of same path), #15355 (local ingress feature request).
Reproduced on
codex-cli 0.120.0and0.121.0, both stock upstream and with a third-party wrapper using--remotemode.