Repository navigation
Remote-control transport couples iOS latency to shared queues and backpressure #27592
Description
Summary
Remote control between ChatGPT iOS and Codex Desktop is currently internet-mediated, and the app-server transport layer has at least two queue-coupling points that can turn ordinary WAN jitter into very visible mobile lag.
During investigation and local testing, I found one concrete root cause for slow iOS -> desktop steering and one remaining root cause for desktop -> iOS latency / reliability under load.
A prototype fix for the inbound path exists locally on branch codex/remote-control-reliability-fixes at commit 782f5ea585.
Related broad symptom report: #22773
Current architecture note
This path is not currently a direct LAN/local-network transport. The iOS <-> desktop control path rides the remote-control internet/websocket flow, so transport stalls are amplified by WAN variance.
That does not explain the full severity of the lag by itself. There are also transport-layer blocking points in the desktop host.
Root cause 1: inbound iOS -> desktop steering was serialized behind app-server queue waits
Before the local fix, the remote-control websocket reader held the ClientTracker lock for the whole read loop and then awaited handle_message(...), which could await transport_event_tx.send(...) for up to 30 seconds.
That meant:
- one blocked
IncomingMessageforward could stop the reader from consuming subsequent websocket frames - steering messages from iOS could sit behind a bounded app-server queue instead of being read immediately
- pong traffic was also delayed behind the same serialized path
Relevant code paths:
codex-rs/app-server-transport/src/transport/remote_control/websocket.rscodex-rs/app-server-transport/src/transport/remote_control/client_tracker.rs
Observed effect
After splitting websocket frame reads from transport-event forwarding with an internal inbound inbox, steering from iOS to desktop became effectively immediate in local testing.
Root cause 2: outbound desktop -> iOS routing can still head-of-line block unrelated traffic
The remote-control reliability change switched remote-control outbound delivery from disconnect-on-full to wait-on-full. That is directionally correct for reliability, but it is currently awaited inline from the single outbound routing loop.
That means one slow remote-control writer can hold up the shared app-server outbound router instead of just slowing that one mobile client.
Relevant code paths:
codex-rs/app-server/src/transport.rscodex-rs/app-server/src/lib.rs
Why this matters
This turns per-client backpressure into global latency. Under load, a slow iOS remote-control stream can delay notifications/responses for unrelated connections.
Remaining design risk: message-slot accounting instead of byte accounting
Remote-control queue/buffer capacity is now much larger, which helps avoid hard disconnects, but capacity is still counted in message slots rather than bytes.
That means a burst of large envelopes can still:
- consume a lot of memory
- create bursty latency instead of clean flow control
- hide overload until the process is already buffering too much work
Relevant code paths:
codex-rs/app-server-transport/src/transport/remote_control/mod.rscodex-rs/app-server-transport/src/transport/remote_control/websocket.rs
Suggested follow-up
- Keep the inbound reader/forwarder split so websocket reads are never blocked on
transport_event_txbackpressure. - Change remote-control outbound routing so a full remote-control writer does not block the shared outbound router for unrelated clients.
- Consider byte-based accounting (or at least hybrid byte + message caps) for remote-control outbound buffering.
- Add field metrics/logging for:
- transport-event queue depth
- remote-control writer queue depth
- unacked envelope count
- ACK latency
- disconnect / timeout reason
- Decide explicitly whether the product should eventually support a LAN-short-circuit transport for local iOS <-> desktop control, since the current architecture always pays internet-path latency.
Validation
The inbound prototype fix passed focused tests in codex-app-server-transport, and local manual testing showed a clear improvement in iOS -> desktop steering latency.
Activity
github-actions commentedon Jun 11, 2026
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
hlky commentedon Jun 11, 2026
Note: I am currently using a build including the branch mentioned, so it is tested in practice, and I can confirm that both inbound and outbound latency is massively reduced.
disco32r commentedon Aug 4, 2026
Additional concrete outbound failure mode (0.146.0; present on current
main)I reproduced a narrower mechanism behind the unresolved outbound head-of-line blocking described above. This is separate from the inbound reader/forwarder issue.
In current
websocket.rs:BoundedOutboundBuffer::insertincrements one global replay-occupancy counter for every retained envelope; occupancy is released by ACK processing.run_server_writer_innerreceiver drainsserver_event_rxonly while that global occupancy is belowCHANNEL_CAPACITY(128).(ClientId, StreamId), but the receive gate is global: one client that stops acknowledging can prevent the shared receiver from serving unrelated clients and streams.thread/readandthread/resume. The transport segments generic server envelopes with a 100 KiB target and 150 KiB maximum; those methods are examples, not transport special cases.ClientClosedcleanup runs afterclient_tracker.handle_message(...)succeeds and invalidates inbound reassembly/deduplication state for the specified stream, or for the whole client when no stream is supplied. Neither branch purges outbound replay entries.The trigger is workload-dependent: remote control, a history-heavy response, multiple clients, and one stalled ACK stream. That explains why a clean install or a light thread may not reproduce it.
A local 0.146.0 mitigation restored normal concurrent-client progress by decoupling live delivery from replay ACK occupancy, imposing global and per-stream replay bounds, and purging replay on explicit client/stream closure. I would not submit that local patch verbatim: current
mainshould also cover worker-exit, idle-expiry, and same-stream reinitialization paths, and segmented-message eviction should remove a complete sequence rather than retain unusable trailing chunks.A focused upstream change should:
server_event_rxregardless of replay ACK latency, while preserving producer-channel and WebSocket-sink backpressure.ClientClosedand every other client/stream teardown path.If this direction aligns with the maintainers' intended solution, would a Codex maintainer explicitly invite a focused PR against
main? I would scope it tocodex-app-server-transportand exclude local updater/pinning hardening.