remote-control relay connection silently disconnects after 20–55 minutes without auto-reconnect (WSL2)

Open 💬 3 comments Opened Jun 6, 2026 by xjian2021
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

Environment

┌─────────────────────┬────────────────────────────────────────────────────────────────────┐
│ Item │ Detail │
├─────────────────────┼────────────────────────────────────────────────────────────────────┤
│ Codex CLI version │ 0.137.0 │
├─────────────────────┼────────────────────────────────────────────────────────────────────┤
│ app-server version │ 0.137.0 │
├─────────────────────┼────────────────────────────────────────────────────────────────────┤
│ OS │ Ubuntu 20.04 on WSL2 (Windows 11) │
├─────────────────────┼────────────────────────────────────────────────────────────────────┤
│ WSL networking mode │ NAT (default) │
├─────────────────────┼────────────────────────────────────────────────────────────────────┤
│ mobile client │ ChatGPT Android app (Codex remote-control) │
├─────────────────────┼────────────────────────────────────────────────────────────────────┤
│ Daemon mode │ codex app-server daemon bootstrap --remote-control │
├─────────────────────┼────────────────────────────────────────────────────────────────────┤
│ TCP keepalive │ net.ipv4.tcp_keepalive_time=60, intvl=10, probes=6 (already tuned) │
└─────────────────────┴────────────────────────────────────────────────────────────────────┘

Steps to reproduce

  1. Start app-server daemon with remote-control: codex remote-control start
  2. In ChatGPT Android app, connect to the WSL2 Codex environment → works immediately
  3. Wait 20–55 minutes without any interaction
  4. Try to access Codex from ChatGPT app again → cannot connect, environment shows as unavailable

Current behavior

The remote-control relay connection dies silently. After disconnection:

  • codex app-server daemon version → reports {"status":"running"} (daemon thinks everything is fine)
  • ps aux → app-server process still running (same PID)
  • remoteControl/status/read API via Unix socket → NO_RESPONSE (socket accepts connection but sends no

data)

  • app-server.stderr.log → completely empty in some cases; in other cases shows:

rmcp::transport::worker worker quit with fatal: Transport channel closed,
when Client(HttpRequest("http/request failed: error sending request for url
(https://chatgpt.com/backend-api/wham/apps)"))

  • The relay connection never auto-recovers until a full codex remote-control stop && codex

remote-control start is performed

Two distinct failure modes observed

Mode A — Fatal error (with logs):
Stderr shows rmcp::transport::worker fatal exit with Transport channel closed when the HTTP request to
chatgpt.com/backend-api/wham/apps fails. The relay worker crashes but the app-server process stays
alive, leaving the Unix socket in a zombie state.

Mode B — Silent disconnect (no logs):
No errors in app-server.stderr.log at all. ss -tnp shows the only TCP connection is 127.0.0.1 →
127.0.0.1:7090 (internal proxy), with zero outbound connections to chatgpt.com. The upstream relay
connection has been dropped without any logging.

Pattern observed over 6 days (login-start.log)

Every single day required a fresh daemon restart to recover — the relay never auto-recovered on its
own:

2026-05-31: timedOut=true → restart → connected
2026-06-01: timedOut=true → restart → connected
2026-06-02: timedOut=true → restart → connected
2026-06-03: timedOut=true → restart → connected
2026-06-04: timedOut=true → restart → connected
2026-06-05: timedOut=true → restart → connected
2026-06-06: timedOut=true → restart → connected (then disconnected again after ~55 min)

Expected behavior

  1. The relay connection should auto-reconnect when the upstream connection to

chatgpt.com/backend-api/wham/apps is lost

  1. Silent disconnections (Mode B) should produce diagnostic logs so the failure reason is traceable
  2. The remoteControl/status/read API should reflect the actual connection state rather than returning

no response when the relay subsystem is dead

  1. The daemon health check should detect when the relay worker has exited and restart it

Additional context

  • chatgpt.com is reachable from WSL2 (curl gets HTTP 405, ~0.8s latency)
  • WSL2 DNS errors (getaddrinfo() failed: -5) appear in dmesg occasionally but do not correlate with all

disconnections — the most recent disconnect happened with no DNS errors present

  • This is NOT a WSL2 TCP keepalive issue — keepalive is already configured and the failure is at the

application layer (relay worker exits / upstream connection drops silently)

---
Labels: bug, app, cli

View original on GitHub ↗

3 Comments

github-actions[bot] contributor · 2 months ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #25984

Powered by Codex Action

wananing · 2 months ago

I looked at this against current upstream/main (f2f80ef442ff84612004bad064991283eb17cdb5) and I think the first PR, if maintainers want one, should stay narrowly scoped to Remote Control relay transport recovery inside the existing app-server process.

I do not want to claim this covers every symptom in this issue yet. In particular, if remoteControl/status/read times out and an unrelated local RPC such as config/read or thread/list also times out at the same time, that points above the relay WebSocket layer, likely in the app-server RPC dispatcher/local transport path, and should be debugged separately.

For the transport/liveness side, I have a deterministic local fake-relay regression test that exercises this sequence:

  1. Start Remote Control against a local fake relay.
  2. Complete enrollment and establish the first WebSocket.
  3. Observe connected.
  4. Keep the WebSocket/TCP connection open but stop producing pong frames.
  5. Advance past the pong timeout.
  6. Assert status leaves connected and becomes connecting.
  7. Assert a second relay WebSocket handshake is established.
  8. Assert a remote initialize message on the second generation still reaches the transport.

I also added a smaller unit test for the lifecycle edge case where one connection worker stops while the sibling worker is still stuck during generation drain. The important assertion there is that Remote Control publishes a non-connected state before drain completes, so local status does not remain stale as connected after the generation is already known to be dead.

The local patch is intentionally limited to codex-app-server-transport Remote Control code:

  • do not restart app-server or the daemon;
  • do not touch daemon ownership/unmanaged-process behavior;
  • do not delete or recreate enrollment;
  • do not rotate installation_id;
  • do not add a public RPC;
  • preserve long-lived RemoteControlWebsocket state across reconnects;
  • when a connection worker stops unexpectedly, publish connecting, cancel only the current WebSocket generation, and rely on the existing reconnect loop to create the next generation.

Validation on f2f80ef442ff84612004bad064991283eb17cdb5 plus the local patch:

just test -p codex-app-server-transport \
  remote_control_transport_reconnects_after_disconnect \
  remote_control_transport_reconnects_after_pong_timeout \
  run_connection_marks_status_connecting_when_worker_stops_before_drain

3 tests run: 3 passed
just fix -p codex-app-server-transport

passed

cargo fmt -- --config imports_granularity=Item completed locally; stable rustfmt warns that imports_granularity is nightly-only.

I also ran the full crate test:

just test -p codex-app-server-transport

125 tests run: 124 passed, 1 failed

The one failure is transport::remote_control::tests::remote_control_waits_for_account_id_before_enrolling, timing out in the auth-change wakeup path:

auth change should wake remote control before the retry delay: Elapsed(())

That failure is reproducible for me outside the new pong-timeout/connection-generation path, so I am treating it as a separate timing issue rather than evidence against the relay-generation recovery test.

Would a maintainer be willing to invite a focused PR for this transport-scoped approach? If not, the next diagnostic data I would collect for this issue is the failure-time split between remoteControl/status/read and an unrelated local RPC such as config/read, to determine whether the stuck state is confined to Remote Control or affects the app-server dispatcher more broadly.

mjzcng · 1 month ago

I do not think this solves the real transport auto-reconnect bug described here, but for day-to-day use I found it helpful to have a tiny wrapper that makes the CLI remote-control app-server easier to restart and keep around as a background process:

https://github.com/mjzcng/codex-cli-mobile-pairing

The main flow is:

npx codex-cli-mobile-pairing start
npx codex-cli-mobile-pairing status
npx codex-cli-mobile-pairing restart
npx codex-cli-mobile-pairing logs

It uses the experimental app-server pairing APIs to generate a manual pairing code, then leaves the backing codex app-server running. For this issue specifically, restart is just a workaround when the mobile side goes stale; the proper fix still needs to be in the relay transport / app-server recovery path you outlined.