Remote Control: session permanently wedged when worker dies unregistered (phantom worker_status:running); no recovery path exposed
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-controldaemon on the desktop
Reproduction
- Start a Remote Control session from the mobile app; let it run a tool call.
- Kill the worker process (and/or its parent
claude remote-controldaemon) mid-turn, hard, so the graceful teardown path never runs. (In our case the session's own tool call killed the daemon:Stop-Processon the hub PID. Theteardown_archive_timeout_ms: 1500best-effort archive obviously never fires on a hard kill.) - Session state via
GET /v1/code/sessions/<id>:worker_status: "running",connection_status: "disconnected". It stays that way for hours. - 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:
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.- 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, thenclaude --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
- Server-side reconciliation: if
connection_statusisdisconnectedfor more than N minutes, clearworker_statusso normal dispatch resumes. This alone fixes the user-facing symptom (stop + reprompt would then just work). - 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. - 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.