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

Open 💬 2 comments Opened Jun 18, 2026 by 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.

View original on GitHub ↗

2 Comments

fibo-blip · 1 month ago

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.

MaxFabian25 · 8 days ago

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.