remote-control daemon stays alive but device disappears from ChatGPT mobile app until app-server restart

Resolved 💬 15 comments Opened May 19, 2026 by TWIN-SuperSport Closed Jun 12, 2026
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

What version of Codex CLI is running?

0.131.0

What subscription do you have?

chatGPT plus

Which model were you using?

gpt-5.5 low

What platform is your computer?

Linux 6.18.29+rpt-rpi-2712 aarch64 unknown

What terminal emulator and version are you using (if applicable)?

chatGPT on iPhone (chatGPT version:1.2026.125)

Codex doctor report

{
  "schemaVersion": 1,
  "generatedAt": "1779160115s since unix epoch",
  "overallStatus": "warning",
  "codexVersion": "0.131.0",
  "checks": {
    "app_server.status": {
      "id": "app_server.status",
      "category": "app-server",
      "status": "ok",
      "summary": "background server is running",
      "details": {
        "control socket": "/home/twin_supersport/.codex/app-server-control/app-server-control.sock",
        "daemon state dir": "/home/twin_supersport/.codex/app-server-daemon",
        "mode": "persistent",
        "pid file": "/home/twin_supersport/.codex/app-server-daemon/app-server.pid (file)",
        "settings": "/home/twin_supersport/.codex/app-server-daemon/settings.json (file)",
        "status": "running",
        "update-loop pid file": "/home/twin_supersport/.codex/app-server-daemon/app-server-updater.pid (file)"
      },
      "remediation": null,
      "durationMs": 0
    },
    "auth.credentials": {
      "id": "auth.credentials",
      "category": "auth",
      "status": "ok",
      "summary": "auth is configured",
      "details": {
        "auth file": "/home/twin_supersport/.codex/auth.json",
        "auth storage mode": "File",
        "stored API key": "false",
        "stored ChatGPT tokens": "true",
        "stored agent identity": "false",
        "stored auth mode": "chatgpt"
      },
      "remediation": null,
      "durationMs": 0
    },
    "config.load": {
      "id": "config.load",
      "category": "config",
      "status": "ok",
      "summary": "config loaded",
      "details": {
        "CODEX_HOME": "/home/twin_supersport/.codex",
        "config.toml": "/home/twin_supersport/.codex/config.toml",
        "config.toml parse": "ok",
        "cwd": "/home/twin_supersport",
        "enabled feature flags": "shell_tool, unified_exec, shell_snapshot, terminal_resize_reflow, sqlite, memories, hooks, enable_request_compression, multi_agent, apps, tool_search, tool_suggest, plugins, plugin_hooks, in_app_browser, browser_use, browser_use_external, computer_use, plugin_sharing, image_generation, skill_mcp_dependency_install, steer, guardian_approval, goals, collaboration_modes, tool_call_mcp_elicitation, personality, fast_mode, tui_app_server, workspace_dependencies",
        "feature flag overrides": "memories=true, goals=true",
        "feature flags enabled": "30",
        "log dir": "/home/twin_supersport/.codex/log",
        "mcp servers": "0",
        "model": "gpt-5.5",
        "model provider": "openai",
        "sqlite home": "/home/twin_supersport/.codex"
      },
      "remediation": null,
      "durationMs": 0
    },
    "installation": {
      "id": "installation",
      "category": "install",
      "status": "ok",
      "summary": "installation looks consistent",
      "details": {
        "PATH codex #1": "/home/twin_supersport/.local/bin/codex",
        "PATH codex #2": "/usr/local/bin/codex",
        "PATH codex #3": "/home/twin_supersport/.local/bin/codex",
        "PATH codex entries": "3",
        "current executable": "/home/twin_supersport/.codex/packages/standalone/releases/0.131.0-aarch64-unknown-linux-musl/codex",
        "install context": "standalone (unix, release /home/twin_supersport/.codex/packages/standalone/releases/0.131.0-aarch64-unknown-linux-musl, resources /home/twin_supersport/.codex/packages/standalone/releases/0.131.0-aarch64-unknown-linux-musl/codex-resources)",
        "managed by bun": "false",
        "managed by npm": "false",
        "managed package root": "not set"
      },
      "remediation": null,
      "durationMs": 1
    },
    "mcp.config": {
      "id": "mcp.config",
      "category": "mcp",
      "status": "ok",
      "summary": "no MCP servers configured",
      "details": {},
      "remediation": null,
      "durationMs": 0
    },
    "network.env": {
      "id": "network.env",
      "category": "network",
      "status": "ok",
      "summary": "network-related environment looks readable",
      "details": {
        "proxy env vars": "none"
      },
      "remediation": null,
      "durationMs": 0
    },
    "network.provider_reachability": {
      "id": "network.provider_reachability",
      "category": "reachability",
      "status": "ok",
      "summary": "active provider endpoints are reachable over HTTP",
      "details": {
        "ChatGPT base URL": "https://chatgpt.com/backend-api/ reachable (HTTP 404)",
        "reachability mode": "ChatGPT auth"
      },
      "remediation": null,
      "durationMs": 106
    },
    "network.websocket_reachability": {
      "id": "network.websocket_reachability",
      "category": "websocket",
      "status": "ok",
      "summary": "Responses WebSocket handshake succeeded",
      "details": {
        "DNS": "2 IPv4, 2 IPv6, first IPv6",
        "auth mode": "chatgpt",
        "connect timeout": "15000 ms",
        "endpoint": "wss://chatgpt.com/backend-api/<redacted>",
        "handshake result": "HTTP 101 Switching Protocols",
        "model provider": "openai",
        "models etag present": "true",
        "provider name": "OpenAI",
        "proxy env vars": "none",
        "reasoning header": "false",
        "server model present": "false",
        "supports websockets": "true",
        "wire API": "responses"
      },
      "remediation": null,
      "durationMs": 806
    },
    "runtime.provenance": {
      "id": "runtime.provenance",
      "category": "runtime",
      "status": "ok",
      "summary": "running standalone on linux-aarch64",
      "details": {
        "commit": "unknown",
        "current executable": "/home/twin_supersport/.codex/packages/standalone/releases/0.131.0-aarch64-unknown-linux-musl/codex",
        "install method": "standalone (unix, release /home/twin_supersport/.codex/packages/standalone/releases/0.131.0-aarch64-unknown-linux-musl, resources /home/twin_supersport/.codex/packages/standalone/releases/0.131.0-aarch64-unknown-linux-musl/codex-resources)",
        "platform": "linux-aarch64",
        "version": "0.131.0"
      },
      "remediation": null,
      "durationMs": 0
    },
    "runtime.search": {
      "id": "runtime.search",
      "category": "search",
      "status": "ok",
      "summary": "search is OK (bundled)",
      "details": {
        "search command": "/home/twin_supersport/.codex/packages/standalone/releases/0.131.0-aarch64-unknown-linux-musl/codex-resources/rg",
        "search command readiness": "file exists",
        "search provider": "bundled"
      },
      "remediation": null,
      "durationMs": 0
    },
    "sandbox.helpers": {
      "id": "sandbox.helpers",
      "category": "sandbox",
      "status": "ok",
      "summary": "sandbox configuration is readable",
      "details": {
        "approval policy": "Never",
        "codex-linux-sandbox helper": "/home/twin_supersport/.codex/tmp/arg0/codex-arg0XLk5Ad/codex-linux-sandbox",
        "execve wrapper helper": "/home/twin_supersport/.codex/tmp/arg0/codex-arg0XLk5Ad/codex-execve-wrapper",
        "filesystem sandbox": "unrestricted",
        "network sandbox": "enabled"
      },
      "remediation": null,
      "durationMs": 0
    },
    "state.paths": {
      "id": "state.paths",
      "category": "state",
      "status": "ok",
      "summary": "state paths and databases are inspectable",
      "details": {
        "CODEX_HOME": "/home/twin_supersport/.codex (dir)",
        "active rollout files": "13 files, 93567037 total bytes, 7197464 average bytes",
        "archived rollout files": "1 files, 61797 total bytes, 61797 average bytes",
        "log DB": "/home/twin_supersport/.codex/logs_2.sqlite (file)",
        "log DB integrity": "ok",
        "log dir": "/home/twin_supersport/.codex/log (dir)",
        "sqlite home": "/home/twin_supersport/.codex (dir)",
        "standalone release cache": "1 entries in /home/twin_supersport/.codex/packages/standalone/releases",
        "state DB": "/home/twin_supersport/.codex/state_5.sqlite (file)",
        "state DB integrity": "ok"
      },
      "remediation": null,
      "durationMs": 174
    },
    "terminal.env": {
      "id": "terminal.env",
      "category": "terminal",
      "status": "warning",
      "summary": "width 57 cols - output may wrap (recommended >=80)",
      "details": {
        "SSH_CLIENT": "present",
        "SSH_CONNECTION": "present",
        "SSH_TTY": "present",
        "TERM": "xterm-256color",
        "color output": "disabled (stdout is not a terminal)",
        "effective locale": "en_GB.UTF-8",
        "stderr is terminal": "true",
        "stdin is terminal": "true",
        "stdout is terminal": "false",
        "terminal": "unknown",
        "terminal size": "57x28"
      },
      "issues": [
        {
          "severity": "warning",
          "cause": "width 57 cols - output may wrap (recommended >=80)",
          "measured": "57 x 28",
          "expected": ">= 80 columns",
          "remedy": "resize the window to at least 80 columns",
          "fields": [
            "terminal size"
          ]
        }
      ],
      "remediation": null,
      "durationMs": 0
    },
    "updates.status": {
      "id": "updates.status",
      "category": "updates",
      "status": "ok",
      "summary": "update configuration is locally consistent",
      "details": {
        "cached latest version": "0.131.0",
        "check for update on startup": "true",
        "dismissed version": "0.121.0",
        "last checked at": "2026-05-18T21:50:35.090578304Z",
        "latest version": "0.131.0",
        "latest version status": "current version is not older",
        "update action": "standalone installer",
        "version cache": "/home/twin_supersport/.codex/version.json"
      },
      "remediation": null,
      "durationMs": 87
    }
  }
}

What issue are you seeing?

Environment:

  • Raspberry Pi 5 (ARM64 Linux)
  • Codex standalone install (rust-v0.131.0)
  • ChatGPT iOS app
  • Remote Control enabled
  • Previously migrated from npm-managed Codex to standalone-managed Codex

Problem:
After some time, the Codex node disappears from the ChatGPT mobile app connection list, even though the local daemon and app-server are still running.

Observed behavior:

  • codex remote-control returns:

{
"status": "alreadyRunning"
}

  • ps aux | grep app-server still shows:

codex app-server --remote-control
codex app-server daemon pid-update-loop

  • Unix socket remains alive:

~/.codex/app-server-control/app-server-control.sock

However, the device no longer appears in the ChatGPT mobile app’s Remote Connections list.

Workaround:
The node becomes visible again only after restarting the app-server:

pkill -f app-server
codex remote-control

After restart, the output changes to:

{
"status": "bootstrapped",
"remoteControlEnabled": true
}

and the device immediately reappears in the ChatGPT mobile app.

Observation:
This appears to be a mismatch between:

  • local daemon/app-server liveness
  • remote relay/discovery registration state

alreadyRunning may not be re-advertising the node to the relay/discovery layer after the registration expires or disconnects.

Additional notes:

  • The issue was reproduced multiple times.
  • Local CLI functionality continues to work normally even while the device is missing from the mobile app.
  • This was observed after migrating from npm-managed Codex to standalone-managed Codex using the official installer.

What steps can reproduce the bug?

  1. Install standalone Codex (rust-v0.131.0) on Raspberry Pi 5 (ARM64 Linux) using the official installer.
  1. Enable remote control:

codex remote-control

  1. Confirm the node appears in the ChatGPT iOS app under Remote Connections.
  1. Leave the daemon running for some time (the disconnect happened multiple times during normal use).
  1. Eventually, the device disappears from the ChatGPT mobile app connection list.
  1. On the Raspberry Pi, verify that the daemon is still alive:

ps aux | grep app-server

The following processes are still running:

  • codex app-server --remote-control
  • codex app-server daemon pid-update-loop
  1. Running:

codex remote-control

returns:
{
"status": "alreadyRunning"
}

but the device still does not appear in the mobile app.

  1. Restarting the app-server fixes the issue:

pkill -f app-server
codex remote-control

  1. After restart, the output changes to:

{
"status": "bootstrapped"
}

and the device immediately reappears in the ChatGPT mobile app.

What is the expected behavior?

The remote-control node should remain visible and reconnectable from the ChatGPT mobile app as long as the local app-server daemon is still running.

If the relay/discovery registration expires or disconnects, the daemon should automatically re-advertise itself without requiring a manual app-server restart.

Running:
codex remote-control

while the daemon is already alive should also restore remote discoverability if the node has disappeared from the mobile app.

Additional information

Additional observations:

  • The issue started after migrating from npm-managed Codex to the standalone-managed install using the official installer.
  • The old npm-managed app-server had to be manually terminated before standalone remote-control worked correctly.
  • The issue appears to affect only remote discoverability from the ChatGPT mobile app. Local CLI functionality continues to work normally.
  • The daemon and unix socket remain alive even while the node disappears from the mobile app.
  • Running inside tmux was initially used, but the same issue was later observed outside tmux as well.
  • The environment uses Tailscale and mobile app remote access, so network transitions (WiFi/mobile network changes, idle reconnects, etc.) may possibly contribute.
  • The issue was reproduced multiple times.

2026/05/27 postscript
The current iOS Add SSH Host flow appears to require password authentication.
This prevents connecting to hosts where PasswordAuthentication is disabled,
which is standard for hardened SSH setups.

Even when the host is reachable only through a VPN/Tailscale network,
I do not want to enable password authentication just to use Codex mobile.

The macOS Codex App already supports key-based SSH workflows, so it would be
very useful if the iOS app supported either:

  1. importing/selecting a private key,
  2. using iOS Keychain / Files-provided SSH keys,
  3. reusing SSH hosts configured on the paired macOS Codex App,
  4. or showing SSH remote projects from the paired desktop host in the mobile app.

View original on GitHub ↗

15 Comments

github-actions[bot] contributor · 2 months ago

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

  • #22851
  • #22898

Powered by Codex Action

jshaofa-ui · 2 months ago

Solution: openai/codex #23403 — Remote-Control Daemon Stays Alive but Device Disappears from ChatGPT Mobile App

Issue

After some time, the Codex node disappears from the ChatGPT mobile app's Remote Connections list, even though the local daemon and app-server are still running. Only restarting app-server restores visibility.

Root Cause Analysis

Primary Cause: Relay/Discovery Registration Expiry Without Re-Advertise

The codex remote-control architecture has three components:

  1. Local daemon (app-server --remote-control) — runs on the device
  2. Relay/discovery service — OpenAI's cloud service that maintains device registry
  3. ChatGPT mobile app — queries relay for available devices

The bug: the device registration with the relay expires (likely due to WebSocket disconnect, token refresh failure, or idle timeout), but the local daemon's alreadyRunning check prevents re-advertising:

codex remote-control → { "status": "alreadyRunning" } → NO re-registration

The daemon assumes it's already registered and doesn't attempt to re-advertise.

Evidence Supporting This:

  1. Restart fixes it: pkill -f app-server && codex remote-control{ "status": "bootstrapped" } → device reappears
  2. Daemon stays alive: ps aux | grep app-server shows processes running
  3. Socket stays alive: Unix socket remains functional
  4. Local CLI works: Only remote discoverability is affected
  5. Network transitions: Tailscale + mobile network changes may trigger relay disconnect

Fix Options

Option A: Heartbeat + Re-Advertise on Registration Expiry (Recommended)

File: codex-rs/app-server/src/remote_control.rs (approximate)

Add a periodic heartbeat that validates relay registration:

async fn remote_control_heartbeat(
    relay_client: Arc<RelayClient>,
    device_id: DeviceId,
) {
    let mut interval = tokio::time::interval(Duration::from_secs(60));
    loop {
        interval.tick().await;
        
        match relay_client.ping(device_id).await {
            Ok(PongResponse { registered: true }) => {
                tracing::debug!(device_id = %device_id, "Relay registration confirmed");
            }
            Ok(PongResponse { registered: false }) | Err(_) => {
                tracing::warn!(
                    device_id = %device_id,
                    "Relay registration lost — re-advertising device"
                );
                // Re-advertise to relay
                if let Err(e) = relay_client.register(device_id).await {
                    tracing::error!(error = ?e, "Failed to re-advertise device");
                } else {
                    tracing::info!(device_id = %device_id, "Device re-advertised successfully");
                }
            }
        }
    }
}

Pros: Fixes root cause, automatic recovery without restart
Cons: Adds periodic network calls to relay

Option B: Make codex remote-control Idempotent with Re-Advertise

File: codex-rs/cli/src/commands/remote_control.rs

When alreadyRunning is detected, also check relay registration:

async fn cmd_remote_control() -> Result<()> {
    if daemon_is_running().await {
        // Check if relay registration is still valid
        let relay_ok = check_relay_registration().await;
        
        if !relay_ok {
            // Re-advertise even though daemon is running
            println!("{{\"status\": \"re_advertised\", \"remoteControlEnabled\": true}}");
            re_advertise_to_relay().await?;
        } else {
            println!("{{\"status\": \"alreadyRunning\"}}");
        }
    } else {
        start_daemon_and_bootstrap().await?;
    }
    Ok(())
}

Pros: Simple change, makes CLI command more robust
Cons: Requires user to manually run codex remote-control to trigger re-advertise

Option C: WebSocket Reconnect with State Recovery

File: codex-rs/app-server/src/relay_connection.rs

Implement proper WebSocket reconnect logic that re-registers on reconnect:

async fn relay_websocket_loop(mut ws: WebSocket) {
    loop {
        match ws.recv().await {
            Ok(msg) => handle_message(msg),
            Err(WebSocketError::Disconnected) => {
                tracing::warn!("Relay WebSocket disconnected — reconnecting");
                ws = reconnect_relay().await;
                // After reconnect, re-register device
                register_device().await;
            }
            Err(e) => {
                tracing::error!(error = ?e, "Relay WebSocket error");
            }
        }
    }
}

Pros: Handles the network transition case properly
Cons: More complex, requires understanding of relay protocol

Recommendation

Implement Option A (heartbeat + re-advertise) as the primary fix, with Option B (idempotent CLI) as an immediate improvement. Option C should be implemented if WebSocket-level reconnect is needed.

Impact

  • Users affected: All codex remote-control users, especially on mobile/unstable networks
  • Severity: High — complete loss of remote access until manual restart
  • Fix complexity: Medium
  • Estimated effort: 2-3 days

Cross-Selling Opportunities

  • Related to codex #23404 (retry limit), #23399 (resume scroll), #23383 (MCP elicitation)
  • Bundle codex remote-control fixes: $3,000-8,000
AzureDominus · 2 months ago

I’m seeing the same behavior on a Linux x86_64 box with Codex 0.132.0.

Setup:

  • installed via npm global @openai/codex
  • CODEX_HOME=/home/admin/.codex
  • app-server launched under a user systemd service as:
  • codex app-server --remote-control --listen unix://

Observed behavior:

  • local service stays active
  • Unix socket remains present
  • codex app-server daemon version continues to return successfully
  • no crash/OOM/segfault in journald
  • ChatGPT mobile/app remote connection disappears or becomes unavailable
  • restarting the app-server immediately makes it show up again

So this looks like the remote-control relay/discovery/websocket state can go stale independently of local daemon liveness.

Snailflyer · 2 months ago

This looks like a good example of why remote-control liveness needs two separate states:

  • local process/server liveness
  • remote discovery/relay registration liveness

alreadyRunning only proves the first. For mobile users, the second is what matters operationally. A robust status surface probably needs to expose last successful relay heartbeat/registration time, remote-visible device id, and a "re-advertise" path that is safe to run while the daemon is alive.

In practice the UX should distinguish "the CLI process is healthy but not discoverable from mobile" from "the local app-server is down". Otherwise users will keep treating a dead discovery registration as a local daemon crash and restarting more than necessary.

asheriif · 1 month ago

+1 this is currently unusable. Device disappears after a few idle minutes despite the daemon being up and reporting healthy

TWIN-SuperSport · 1 month ago

Additional isolation result:

I deleted and reinstalled the ChatGPT iOS app, then logged in again.

However, the same transient behavior still occurs:

  1. The Codex tab first shows an incomplete/stale-looking session list.
  2. Raspberry Pi initially appears with a loading spinner.
  3. The workspace/project section initially shows "no workspace".
  4. Several entries appear as generic "new chat".
  5. A moment later, Raspberry Pi turns green/online.
  6. The codex-cli project, pinned sessions, and full session list are restored.

This suggests the issue is unlikely to be caused only by stale local cache on the iPhone.

Additional local environment isolation:

  • The Raspberry Pi environment is now standalone-only Codex CLI 0.134.0.
  • The old global npm-installed Codex entry under /usr/local/bin/codex was removed.
  • codex doctor reports:
  • install method: standalone
  • managed by npm: no
  • install: consistent
  • state DB integrity: ok
  • readlink -f $(which codex) points to:

~/.codex/packages/standalone/releases/0.134.0-aarch64-unknown-linux-musl/bin/codex

So if the device still disappears from the ChatGPT iOS app, npm/standalone installation mixing and stale iPhone local app cache are unlikely to be the root cause.

This points more toward server-side/account-level session/device snapshot state, or remote-control relay/discovery/presence reconciliation, rather than only local iOS app cache.

asheriif · 1 month ago

This was actually fixed for me with 0.134.0

vladtsap · 1 month ago

It was not fixed for me in 0.134.0 version, this issue still happens for me

ajma · 1 month ago

I'm on 135 and still seeing this.

AzureDominus · 1 month ago

Still getting this on 135

uniQta · 1 month ago

0.137.0 CLI, ChatGPT authenticated, refresh -> WebSocket connection -> immediate close frame from server. Works on the app side. Possibility of an issue specific to the CLI's WebSocket implementation.

colonelpanic8 · 1 month ago

Things seem fixed in the latest version to me

uniQta · 1 month ago

I'm envious. On my mac, it works in the app (Electron/Chromium WebSocket), but in the CLI (tungstenite/rustls), the connection is immediately dropped by the server. It looks like it's working on the surface, but if you follow the logs and processes, it's clear there is a problem. There is nothing I can do about it on the user side.

markrowan · 1 month ago

I tested a narrow fix for the reconnect case described here, specifically the case where the remote-control daemon/app-server still appears alive locally, but the mobile app remains stuck waiting for the desktop after the mobile app is backgrounded or loses its session.

Branch:
https://github.com/markrowan/codex/tree/fix/remote-control-reenable-reconnect

Commit:
https://github.com/markrowan/codex/commit/c2933b2064

The change makes remoteControl/enable request a fresh websocket connection when remote control is already Connected, instead of treating that call as a no-op. In my local testing, this allowed the mobile client to recover the environment without manually stopping and restarting remote control.

A couple of caveats:

  • This was tested on Linux, which I understand is not officially supported for this flow.
  • I could not open a PR against openai/codex because GitHub requires collaborator permission.

So this appears to resolve the specific reconnect/no-op case we reproduced, but I have not validated broader remote-control lifecycle cases beyond that.

ax-openai · 1 month ago

Closing as a duplicate of #22898.