Remote Control: session permanently wedged when worker dies unregistered (phantom worker_status:running); no recovery path exposed

Open 💬 0 comments Opened Jul 10, 2026 by xxparrotxx

Summary

If a Remote Control worker process dies without deregistering (crash, kill, power loss) while a turn is in flight, its session is left in a phantom state server-side: worker_status: "running" + connection_status: "disconnected", indefinitely (observed 3+ hours with no TTL). New prompts from the mobile app are accepted as events (last_event_at updates) but are never dispatched to a new worker. The session card churns forever. There is no documented recovery path, and every intuitive recovery attempt fails.

Related: #37321 (closed "not planned"), #60790, #52860.

Environment

  • CLI 2.1.206 (npm, Windows 11), Desktop 1.20186.x, iOS app
  • Remote Control via claude remote-control daemon on the desktop

Reproduction

  1. Start a Remote Control session from the mobile app; let it run a tool call.
  2. Kill the worker process (and/or its parent claude remote-control daemon) mid-turn, hard, so the graceful teardown path never runs. (In our case the session's own tool call killed the daemon: Stop-Process on the hub PID. The teardown_archive_timeout_ms: 1500 best-effort archive obviously never fires on a hard kill.)
  3. Session state via GET /v1/code/sessions/<id>: worker_status: "running", connection_status: "disconnected". It stays that way for hours.
  4. From the app: tapping stop, sending new prompts - events are accepted, nothing dispatches, card spins forever. Restarting the daemon (which re-adopts the environment and correctly revives other sessions whose worker deaths were registered) does not help this one.

Everything a user might try, and why it fails

| Attempt | Result |
|---|---|
| Stop button + new prompt in app | Event accepted, no dispatch, still churning |
| Restart the remote-control daemon | Revives cleanly-disconnected sessions only; phantom untouched |
| claude --print --sdk-url .../<id> --session-id <id> | RemoteIO: transport closed permanently (code 403) (no worker JWT) |
| claude remote-control --session-id <id> | Blocked by local bridge-pointer lock while a live daemon serves the env |
| POST <session>/interrupt, /reset, /worker/reset | 404 |
| PATCH <session> | 405 |

Root cause (from reading the client)

worker_status is entirely client-authored (PUT {sdkUrl}/worker). If the worker dies without writing a terminal state, the value freezes at "running" and nothing server-side ever reconciles it against the dead transport. Dispatch logic then treats the session as served.

Recovery that actually works (undocumented)

The epoch mechanism already supports takeover; it is just not exposed anywhere:

  1. POST /v1/code/sessions/<id>/bridge (OAuth bearer, anthropic-version: 2023-06-01, anthropic-beta: ccr-byoc-2025-07-29, body {}) returns {worker_jwt, worker_epoch, ...}. The fresh epoch supersedes the phantom.
  2. Spawn a worker with the JWT passed the way the daemon does it, via environment variables: CLAUDE_CODE_SESSION_ACCESS_TOKEN=<worker_jwt>, CLAUDE_CODE_WORKER_EPOCH=<epoch>, CLAUDE_CODE_SESSION_ID=<new-guid>, CLAUDE_CODE_ENVIRONMENT_KIND=bridge, then claude --print --sdk-url https://api.anthropic.com/v1/code/sessions/<id> --session-id <id>.

The worker attaches, resumes the interrupted turn (including re-requesting the in-flight tool permission), and the session goes connected / requires_action. Verified live on our wedged session: 3-hour phantom to fully recovered, conversation history intact.

Asks

  1. Server-side reconciliation: if connection_status is disconnected for more than N minutes, clear worker_status so normal dispatch resumes. This alone fixes the user-facing symptom (stop + reprompt would then just work).
  2. Expose an interrupt/reset: the epoch takeover exists; a claude remote-control --reconnect <session-id> that performs the bridge claim (bypassing or reconciling the local pointer lock) would make recovery a one-liner.
  3. Document it: the Remote Control docs currently contain no recovery guidance for stuck sessions. Even documenting POST <session>/archive (which works, 200/409, and force-closes a dead card) would give users an escape hatch.

Happy to provide session IDs / request IDs from our reproduction to whoever picks this up.

View original on GitHub ↗