Skip to content

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

Open

Description

@xjian2021

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
  2. Silent disconnections (Mode B) should produce diagnostic logs so the failure reason is traceable
  3. The remoteControl/status/read API should reflect the actual connection state rather than returning
    no response when the relay subsystem is dead
  4. 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

Activity

  1. added
    bugSomething isn't working
    CLIIssues related to the Codex CLI
    windows-osIssues related to Codex on Windows systems
    app-serverIssues involving app server protocol or interfaces
    connectivityIssues involving networking or endpoint connectivity problems (disconnections)
    on Jun 6, 2026
  2. github-actions commented on Jun 6, 2026

    @github-actions
    Contributor
  3. wananing commented on Jun 25, 2026

    @wananing

    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.

  4. mjzcng commented on Jul 8, 2026

    @mjzcng

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    CLIIssues related to the Codex CLIapp-serverIssues involving app server protocol or interfacesbugSomething isn't workingconnectivityIssues involving networking or endpoint connectivity problems (disconnections)remotewindows-osIssues related to Codex on Windows systems

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions