remote-control 409 stale enrollment needs supported takeover or repair path
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 already0.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:
- Run Codex remote control on a long-lived Linux VM.
- Accumulate a stale/ghost remote-control state where the backend believes an old app-server is still online.
- Attempt to restart/reconnect remote control.
- Observe
409 Conflictwith{"detail":"Remote app server already online"}. - Stop remote control and kill lingering
codex remote-control/codex app-serverprocesses. - Clear
remote_control_enrollmentsfrom~/.codex/state_5.sqlite. - Attempt to start remote control again.
- In this case, the backend still returned 409 until the local
installation_idwas 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:
- Stop
codex remote-control. - Kill lingering
codex remote-control,codex app-server, and related remote-control processes. - Back up
~/.codex/state_5.sqliteand~/.codex/logs_2.sqlite. - Clear only Codex remote-control enrollment state from
remote_control_enrollments. - 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:
- Each app-server process generates a fresh
lease_idor monotonicconnection_epochwhen remote control starts. - WebSocket connect or server refresh includes
installation_id,server_id, and the current lease. - 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=truepath. - The backend marks the older lease superseded and closes or rejects traffic from it.
- Mobile/remote clients route only to the newest active lease for the host/environment.
- The CLI treats
409 Remote app server already onlineas 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 onlinespecifically. - 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 repairorcodex remote-control resetcommand 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 stopreports success while a long-lived app-server process still exists.- Local enrollment row is deleted but the same
installation_idstill 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
- https://github.com/openai/codex/issues/24024 - closed, but contains the same 409 shape and local recovery details.
- https://github.com/openai/codex/issues/24749 - connected mobile host with stale/diverged thread list until enrollment reset.
- https://github.com/openai/codex/issues/25241 - persisted enrollment reuse with stale host identity fields.
- https://github.com/openai/codex/issues/23865 - revoked/reauthorization stale-state recovery gaps.
- https://github.com/openai/codex/issues/25532 - stale/revoked client reuse.
- https://github.com/openai/codex/issues/24179 - remote-control offline state while desktop websocket remains active.
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.
2 Comments
Additional Windows Desktop + WSL evidence for the same stale/ghost app-server shape.
Environment shape:
OpenAI.Codex_26.623.11225.0_x64__2p2nqsd0c76g0CODEX_HOME=/home/<wsl-user>/.codex-appUser-visible symptom:
Unable to send message/Loading....Local process evidence:
codex app-server --analytics-default-enabledprocesses were alive at the same time.CODEX_HOME=/home/<wsl-user>/.codex-app.Relevant local log pattern from the older process:
Related local Electron/UI log pattern seen earlier in the same family of failures:
Manual cleanup that reduced the broken state:
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.
macOS Desktop confirmation: I hit the same 409, and rotating the local installation identity fixed it.
Environment
26.814.41407(6720)26.6.2on Apple siliconcodex-cli 0.148.0-alpha.15The UI showed:
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-controldaemon. The desktop logs repeatedly showed:Workaround verified on this Mac
~/.codex/installation_id,.codex-global-state.json,state_5.sqlitewith its WAL/SHM files, and the daemon settings.electron-local-remote-control-installation-idandelectron-local-remote-control-environment-idfrom.codex-global-state.json.remote_control_enrollmentsinstate_5.sqlite.remoteControlEnabledtofalsein the inactive daemon settings, if that file exists.~/.codex/installation_idinto the backup instead of permanently deleting it.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 onlineerrors.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.