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.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗