Repository navigation
remote-control 409 stale enrollment needs supported takeover or repair path #28862
Description
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
- Remote control can get stuck after idle or network changes with 409 Conflict until re-enrollment #24024 - closed, but contains the same 409 shape and local recovery details.
- Android Codex shows stale local project threads despite connected desktop until remote-control enrollment reset #24749 - connected mobile host with stale/diverged thread list until enrollment reset.
- codex remote-control reuses persisted enrollment with stale server_name, causing silent WebSocket disconnect #25241 - persisted enrollment reuse with stale host identity fields.
- Codex Desktop remote control remains connected but unusable after remote device revoked permission, and cannot be deleted or re-authorized #23865 - revoked/reauthorization stale-state recovery gaps.
- Codex Desktop remote-control keeps using revoked client when connecting Mac to Windows, no approval request appears #25532 - stale/revoked client reuse.
- Remote Control goes offline while desktop WebSocket to chatgpt.com remains active #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.
Activity
fibo-blip commentedon Jul 1, 2026
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.
MaxFabian25 commentedon Aug 19, 2026
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.
nemf-agent commentedon Oct 6, 2026
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
codex app-server --remote-control --listen unix:// --managed-daemonat 0.160.1; CLI 0.160.0/modelsrequests succeed with the auth header attached, andremote/control/server/refreshtoken refreshes succeed during the outage.Sequence (local time, from
~/.codex/logs_2.sqlite)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_idthat 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-raya46657bcca53552b-SJC)5ab2b791-e480-49f2-a230-6385d3a0a826(cf-raya46657243c64303f-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_idwith 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.