Skip to content

remote-control 409 stale enrollment needs supported takeover or repair path #28862

Open

Description

@EazyReal

Summary

I hit what looks like the stale remote-control enrollment / 409 conflict failure mode. I was able to recover locally, but the recovery path points to a deeper issue: Codex appears to need a supported way for a new app-server process to take over from a stale or ghost remote-control connection.

This looks related to several existing remote-control stale-state reports, but the key difference here is that clearing remote_control_enrollments alone was not sufficient; the backend continued returning 409 until the local host identity was rotated.

Environment

  • Codex CLI entrypoint: codex-cli 0.136.0
  • Managed remote-control app-server: 0.141.0
  • Latest npm package checked at report time: @openai/codex 0.141.0
  • Install note from codex doctor: the CLI entrypoint and npm update target are from different npm roots, but the managed remote-control app-server was already 0.141.0.
  • Platform: Linux 6.17.0-1016-gcp x86_64 x86_64
  • OS: Ubuntu 24.04 GCP Linux VM
  • Terminal/multiplexer: SSH + tmux 3.4
  • Auth mode: ChatGPT auth
  • Remote surface: ChatGPT mobile Codex remote-control view

Relevant current health after workaround:

{
  "mode": "daemon",
  "status": "connected",
  "daemon": {
    "status": "alreadyRunning",
    "backend": "pid",
    "cliVersion": "0.136.0",
    "appServerVersion": "0.141.0"
  }
}

What issue are you seeing?

Codex remote control can report that remote control is enabled for a host, but the connection is errored:

Remote control is enabled on <host> but the connection is errored.
HTTP error: 409 Conflict
{"detail":"Remote app server already online"}

A related mobile symptom is that the remote host can appear green/reachable, but the mobile app and remote host appear to be attached to diverged environments or session lists. Both sides can still call/use Codex, but recent sessions are not reflected consistently.

What steps can reproduce the bug?

I do not have a minimal deterministic reproducer yet, but the observed path was:

  1. Run Codex remote control on a long-lived Linux VM.
  2. Accumulate a stale/ghost remote-control state where the backend believes an old app-server is still online.
  3. Attempt to restart/reconnect remote control.
  4. Observe 409 Conflict with {"detail":"Remote app server already online"}.
  5. Stop remote control and kill lingering codex remote-control / codex app-server processes.
  6. Clear remote_control_enrollments from ~/.codex/state_5.sqlite.
  7. Attempt to start remote control again.
  8. In this case, the backend still returned 409 until the local installation_id was rotated.

Expected behavior

Remote control should provide a supported recovery path when the backend believes an older app-server is still online but the local host is trying to reconnect.

At minimum, the CLI should distinguish this condition from a generic errored connection and guide the user through a safe repair flow. Ideally, the system should support authenticated takeover by the new app-server process without requiring manual SQLite edits or host identity rotation.

Actual behavior

The practical recovery required local state surgery:

  1. Stop codex remote-control.
  2. Kill lingering codex remote-control, codex app-server, and related remote-control processes.
  3. Back up ~/.codex/state_5.sqlite and ~/.codex/logs_2.sqlite.
  4. Clear only Codex remote-control enrollment state from remote_control_enrollments.
  5. Start remote control again so Codex creates a fresh enrollment.

In my case, clearing remote_control_enrollments alone was not sufficient. The backend still returned 409 until the local installation_id was rotated. After that, codex remote-control start --json returned connected, the local enrollment table had one current row, and the managed app-server held the active websocket.

That suggests the backend can continue associating an old installation identity with an online app-server even after local enrollment state has been deleted.

Root-cause hypothesis

The current implementation appears to conflate three concepts that should be modeled separately:

stable host identity
  installation_id

registered remote host
  server_id
  environment_id

current live connection
  lease_id / connection_epoch

A persisted enrollment should identify the registered host/environment, but it should not itself be the authority for whether a particular app-server process is the current live connection.

The backend seems to reject a new connection with 409 when it believes an older app-server for the same host/enrollment is still online. If that older connection is a ghost, stale lease, or no longer user-visible, the CLI has no supported way to take over or repair the condition other than local state surgery and, in some cases, host identity rotation.

Proposed fix direction

Introduce backend-backed connection leases with fencing/takeover semantics.

Suggested invariant:

For a given authenticated host identity, exactly one app-server lease is current, and all older leases are explicitly superseded.

Suggested behavior:

  1. Each app-server process generates a fresh lease_id or monotonic connection_epoch when remote control starts.
  2. WebSocket connect or server refresh includes installation_id, server_id, and the current lease.
  3. If the backend sees an existing online connection for the same authenticated host identity, it allows an authenticated takeover after a short grace period or with an explicit takeover=true path.
  4. The backend marks the older lease superseded and closes or rejects traffic from it.
  5. Mobile/remote clients route only to the newest active lease for the host/environment.
  6. The CLI treats 409 Remote app server already online as a recoverable stale-lease condition, not a generic terminal connection error.

CLI-side recovery improvements

Even before or alongside backend lease work, the CLI could improve the user-facing recovery path:

  • Detect 409 Remote app server already online specifically.
  • Retry briefly in case the old websocket is genuinely shutting down.
  • If still 409, attempt a bounded re-enroll/takeover flow if supported by the backend.
  • If takeover is unavailable, surface a specific repair action instead of a generic errored connection state.
  • Add a supported codex remote-control repair or codex remote-control reset command that:
    • stops the managed remote-control app-server,
    • verifies no ghost process remains,
    • backs up local Codex state,
    • clears only remote-control enrollment state,
    • optionally regenerates host identity as an explicit last-resort step.

Test cases worth covering

  • Persisted enrollment exists, but backend returns 409 because an older app-server is believed online.
  • codex remote-control stop reports success while a long-lived app-server process still exists.
  • Local enrollment row is deleted but the same installation_id still receives 409.
  • Mobile host list shows a reachable host while local session/environment state is stale or diverged.
  • Revoked or stale remote-control client state is reported distinctly from stale app-server lease state.

Related reports

Contribution boundary

If maintainers agree with the diagnosis, I can help with the public CLI-side part: 409 detection, clearer recovery messaging, repair/reset command behavior, and tests. The lease/takeover model likely crosses backend infrastructure and may need to be implemented internally.

Activity

  1. added
    bugSomething isn't working
    CLIIssues related to the Codex CLI
    app-serverIssues involving app server protocol or interfaces
    connectivityIssues involving networking or endpoint connectivity problems (disconnections)
    on Jun 18, 2026
  2. fibo-blip commented on Jul 1, 2026

    @fibo-blip

    Additional Windows Desktop + WSL evidence for the same stale/ghost app-server shape.

    Environment shape:

    • Windows Codex Desktop from MSIX / WindowsApps
    • App package observed locally: OpenAI.Codex_26.623.11225.0_x64__2p2nqsd0c76g0
    • Desktop configured for WSL agent runtime
    • WSL app-server launched from the Windows-bundled WSL Codex binary
    • WSL app-server environment had CODEX_HOME=/home/<wsl-user>/.codex-app

    User-visible symptom:

    • A specific existing Desktop chat initially showed Unable to send message / Loading....
    • A simple message in a different chat succeeded.
    • After that, returning to the previously blocked chat allowed sending again.

    Local process evidence:

    • Two codex app-server --analytics-default-enabled processes were alive at the same time.
    • Both were Desktop-origin WSL app-servers with the same CODEX_HOME=/home/<wsl-user>/.codex-app.
    • The newer process handled the successful turn.
    • The older process was still alive until manually terminated.

    Relevant local log pattern from the older process:

    failed to connect to app-server remote control websocket
    websocket_url=wss://chatgpt.com/backend-api/wham/remote/control/server
    error=HTTP error: 409 Conflict
    body={"detail":"Remote app server already online"}
    

    Related local Electron/UI log pattern seen earlier in the same family of failures:

    Request failed ... method=remoteControl/status/read ...
    error={"code":-32000,"message":"Connection for host ID remote-control:<env_id> not found"}
    

    Manual cleanup that reduced the broken state:

    • Identify the duplicate Desktop WSL app-server processes.
    • Keep the newer/active one that is handling turns.
    • Terminate only the stale older Desktop WSL app-server.
    • Do not delete Codex profiles or session data.

    This looks like the same core issue described here: there is no supported lease/takeover/fencing path for a stale previous app-server connection. In Desktop WSL mode it can leave two same-home app-server processes alive, and the UI symptom may surface as Unable to send message / Loading... in an existing chat rather than only as a remote-control device-list failure.

    Private workspace paths, session ids, and prompts intentionally omitted.

  3. MaxFabian25 commented on Aug 19, 2026

    @MaxFabian25

    macOS Desktop confirmation: I hit the same 409, and rotating the local installation identity fixed it.

    Environment

    • ChatGPT/Codex desktop 26.814.41407 (6720)
    • macOS 26.6.2 on Apple silicon
    • bundled codex-cli 0.148.0-alpha.15

    The UI showed:

    Could not enable remote control. Please ensure only one instance of ChatGPT is running
    

    I initially suspected account switching. I signed back into the previous account, disabled Remote Control there, returned to the target account, and retried. The error remained.

    Local inspection found no second authenticated ChatGPT app-server and no running codex remote-control daemon. The desktop logs repeatedly showed:

    HTTP error: 409 Conflict
    {"detail":"Remote app server already online"}
    

    Workaround verified on this Mac

    1. Quit ChatGPT and verify that its bundled app-server and managed Remote Control process have stopped.
    2. Back up ~/.codex/installation_id, .codex-global-state.json, state_5.sqlite with its WAL/SHM files, and the daemon settings.
    3. Remove only electron-local-remote-control-installation-id and electron-local-remote-control-environment-id from .codex-global-state.json.
    4. Delete only the rows from remote_control_enrollments in state_5.sqlite.
    5. Set remoteControlEnabled to false in the inactive daemon settings, if that file exists.
    6. Move ~/.codex/installation_id into the backup instead of permanently deleting it.
    7. Reopen ChatGPT, sign in to the target account, and enable Settings → Connections → Control this Mac → Allow connections.

    Result

    ChatGPT created a different installation identity and one enabled Remote Control enrolment. The phone connected successfully, and the new desktop process produced no further Remote app server already online errors.

    This is unsupported local state repair and is only verified on one Mac. The result supports the stale installation identity or backend lease diagnosis. It does not establish account switching as the trigger. A supported repair or authenticated takeover path would avoid this local state surgery.

  4. nemf-agent commented on Oct 6, 2026

    @nemf-agent

    Another macOS reproduction, this time with a clear trigger: a single pong timeout on a healthy connection. No second app-server, account switch, or migration was involved.

    Environment

    • macOS 27.0.1, Apple Silicon (always-on Mac mini used remotely from iOS)
    • Managed daemon codex app-server --remote-control --listen unix:// --managed-daemon at 0.160.1; CLI 0.160.0
    • Codex desktop 26.930.21537 (12776) also running; its bundled app-server only forwards and does not open its own remote-control websocket
    • ChatGPT auth. Auth is healthy: /models requests succeed with the auth header attached, and remote/control/server/refresh token refreshes succeed during the outage.

    Sequence (local time, from ~/.codex/logs_2.sqlite)

    09:49:04 INFO connected to app-server remote control websocket ... has_enrollment=true
    10:07:34 WARN remote control websocket reader disconnected, err: remote control websocket pong timeout
    10:07:34 WARN remote control websocket writer was stopped
    10:07:34 INFO status changed Connected -> Connecting
    10:07:36 INFO status changed Connecting -> Errored
             WARN failed to connect ... HTTP error: 409 Conflict ... body: {"detail":"Remote app server already online"}
             ... repeats every 1-30s with backoff, same server_id / environment_id / installation_id ...
    10:12:48 INFO refreshing remote control server token (succeeds)
    10:17:56 INFO refreshing remote control server token (succeeds)
    10:32    `codex app-server daemon disable-remote-control` then `enable-remote-control`: still 409 immediately
    

    The ghost registration was still blocking reconnects 25+ minutes after the client-side pong timeout. The only process trying to connect is the same daemon process (same PID) using the same enrollment and server_id that owned the dead socket. So the backend is rejecting the rightful owner's reconnect in favor of a socket that is already dead.

    Sample request IDs for backend correlation:

    • 1e3d3a09-7cfa-4171-9774-cbe2829ce16f (cf-ray a46657bcca53552b-SJC)
    • 5ab2b791-e480-49f2-a230-6385d3a0a826 (cf-ray a46657243c64303f-SJC)

    Earlier in the same week (Oct 2) this machine had a similar 409 episode, so this recurs.

    Expected: a reconnect from the same installation_id + server_id with a valid refreshed token should take over (evict) the stale session, or the server should expire it on missed heartbeats within a minute or two. At minimum, the client should surface the stuck state instead of retrying silently forever. Disabling and re-enabling remote control does not fix it.

    +1 to the request in this issue for a supported takeover/repair path.

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)remote

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions