Remote Control: sessions served by a crashed server are never re-claimed — restart doesn't restore them and messages to disconnected sessions queue forever
Environment
- Claude Code v2.1.251, native installer (
~/.local/bin/claude.exe) - Windows 11 Home 10.0.26200
- claude.ai subscription login, server mode (
claude remote-control)
Summary
When a claude remote-control server dies uncleanly (crash, or force-kill), the sessions it was serving lose their backend assignment. After that:
- Restarting
claude remote-controlin the same directory does NOT bring those sessions back, contradicting the docs ("brings back every session the server was serving", Resume sessions after stopping the server). Only the single session recorded in the project'sbridge-pointer.jsonis re-adopted at startup. - Messages sent from claude.ai to such a disconnected session queue forever ("Connecting…" / "not connected to \<machine\>"). No later registration or work-poll ever re-queues the session, so it is never served again. The docs' crash advice ("send it a message from a connected device. Claude Code serves it again") only works while the session's assignment is intact — not after a crash, which is when it matters most.
- The only CLI recovery is
claude remote-control --session-id <id>, one session at a time — exclusive by design (per-environment lock + single-session mode), so with several stale sessions you must serially swap dedicated hosts, and an unclean swap breaks the previously reattached session again.
Steps to reproduce
claude remote-controlin a project directory; attach 2–3 sessions from claude.ai; let them go idle.- Kill the server process forcefully (simulates the organic crash we hit: the TUI printed
Session failed: Process exited with error cse_…, later the process died;Capacity: 0/32). - Run
claude remote-controlagain in the same directory. - In claude.ai, all sessions except the bridge-pointer one stay "not connected to \<machine\>". Messages sent to them are held indefinitely; the new server's debug log shows only empty
GET .../work/pollresponses for them.
Expected
After a restart in the same directory — graceful stop or crash — previously served sessions are re-claimed (or at minimum, a queued message triggers re-serving, as documented for crashed sessions).
Actual
Only the pointer session is adopted. Restart debug log (--debug-file):
[bridge:init] Found prior environment env_01JX83s1q4BtT415SpMxEm5z in pointer (ageMs=67284); requesting reuse on registration
[bridge:api] POST /v1/environments/env_01JX83s1q4BtT415SpMxEm5z/bridge/reconnect session_id=[REDACTED]
[bridge:api] POST .../bridge/reconnect -> 200
[bridge:init] Adopted session cse_01MRnoE7fRhYtAAk8tW3U4UW re-queued via bridge/reconnect
[bridge:init] Registered, server environmentId=env_01JX83s1q4BtT415SpMxEm5z
For every other previously served session: no reconnect is attempted at registration, and nothing ever serves them again.
Workaround (proves the backend supports this)
Manually calling the same endpoint the CLI uses for the pointer session — but for each disconnected session — immediately re-queues it onto the already-running server; a worker spawns within ~10 s and the session shows connected again on claude.ai:
POST /v1/environments/<env_id>/bridge/reconnect
{"session_id": "cse_…"}
anthropic-beta: oauth-2025-04-20,environments-2025-11-01
Suggestion: on registration, re-queue every session still recorded for that environment/directory (server- or client-side), or persist the serving set locally and replay bridge/reconnect for each entry on startup. A --reconnect-all (or --reconnect <id,…>) flag on claude remote-control would also cover it.
Related observations from the same incident
- One auto-created session's worker crash-loops on resume: the child exits after ~1 s with
CCRClient: Epoch mismatch (409, reason=session_not_active), shutting down, surfacing in the server TUI asSession failed: Process exited with error cse_01KC7tyHzRyCGYyF7964dHqB. This failure preceded the original server crash. - One pre-created session ended up with no
environment_idserver-side.--session-id/--continuefor it fail withError: Session … has no environment_id. It may never have been attached to a bridge., andbridge/reconnectanswersSession not found. It was later auto-archived — such a session is permanently unrecoverable, taking any queued messages with it.
🤖 Filed with Claude Code after live-debugging the incident.
_The real me: claude (Fable) debugging claude... pretty pretty good ! No way I would have found the workaround and couldn't lose all these sessions_
3 Comments
Data point on v2.1.270 (Windows 11 Enterprise, server mode
--spawn=same-dir), after a machine reboot — partially different from the report aboveSetup:
claude remote-control --spawn=same-dirin one directory, ~6 sessions attached from claude.ai over several days. The machine rebooted (unclean stop of rc). Before restarting, a wrapper script touchesbridge-pointer.json's mtime so the >4h stale check doesn't clear it.Same as this issue
environmentId(no "Could not reuse" warning).sessionIdwas re-adopted at startup — and it failed ~7 s later withSession failed: Process exited with error cse_…(its local transcript had been deleted, so that part is expected).Capacity: 0/32afterwards.activeSessionIdsonly gets written to the pointer on the graceful[bridge:shutdown]path, so a reboot/crash never persists the serving set (same as #90777).Different: opening the session on claude.ai re-queued it immediately
Capacity0 → 3, transcripts resumed writing) — no manualbridge/reconnectneeded.But the Claude Desktop app does not trigger it
Readyon that environment. They never reconnected from the Desktop view.Contrast, for anyone landing here: sessions started locally (Desktop Code tab / interactive CLI with Remote Control on) are different by design — the bridge lives inside that process, so after a reboot nothing on the host can pick them up and opening them on claude.ai doesn't help. Sending one message to the session in Desktop respawns the process, and it re-binds to the same remote session id (verified:
bridgeSessionIdin the new~/.claude/sessions/<pid>.jsonmatches the oldcse_…recorded in the transcript).Suggested fixes
activeSessionIdsduring normal operation (e.g. on attach/detach), not only on graceful shutdown.bridge/reconnectevery session recorded for it.---
🤖 Investigated and written by Claude Code running Claude Opus 5. My input was one screenshot of the "Can't reach" banner plus "is this a bug?", and noticing that clicking sessions on the web brought them back. Opus 5 did the rest in a single session: read the rc log and
bridge-pointer.jsonto confirm the environment was reused but only one session re-adopted; cross-referenced local transcripts with~/.claude/sessions/*.jsonto prove local sessions re-bind to the same remote id; grepped the 2.1.270 binary for the pointer-write / shutdown / re-queue logic (activeSessionIds,parked idle; the server re-queues it on the next message); searched existing issues to land on this one and #90777; and drafted the fixes above. It also corrected its own first hypothesis (that the orphaned sessions needed--teleport) once the web-click observation came in.Same issue on MacOS 26.6.2, Claude 2.1.284. Reproduced by starting
claude rcand cutting the network connection.Starting
claude rcagain in the same folder brings up only the bridge session. All other sessions are markedunreachablein claude.ai and on the web.claude rc --session-idresults inError: This folder is already served by a terminalclaude remote-controlon this device. Stop it first.Workaround as proposed by @yan-hic: Claude.ai does not reconnect the sessions and the API workaround fails with "Session does not belong to this environment." The solution is to find the old env_id and update the
bridge-pointer.jsonby hand. Runningclaude rcafterwards picks some of the sessions up, or they can be reattached from claude.ai/web; except when it does not. I was unable to establish why some of the launches with the same environment id come up healthy, while others never pick old sessions up.@graven the "does not belong to this environment" part matches what the bundled JS does after that give-up. I read it in 2.1.285 and again in 2.1.286 (your run was on 2.1.284); the path is the same in both. I have not reproduced the network cut myself, so this comes from the code only, following the pointer analysis I posted on #86045 (2026-09-08).
When the poll loop logs
Server unreachable for N minutes, giving up.it sets a flag and breaks out. I call it the fatal flag; in the bundle it has a minified name. The shutdown path then skips everything that is guarded bypreserveOnShutdown && !fatal:activeSessionIdsis not written tobridge-pointer.json(that write only happens for anupgrade,reloadoryieldshutdown, and never after a fatal error);markEnvironmentOfflineis skipped.Instead, after the
Shutting down 10 active session(s)…line from your log, it falls through to the teardown used for a normal non-preserving exit:archiveSessionfor every active session,deregisterEnvironment, and finallyclearBridgePointer(when this process owns the pointer). The pointer delete is local, so it always succeeds. The two API calls run right after the give-up, usually while the network is still down, and their failures are only logged at verbose level.That explains the sequence you saw. The next
claude rcfinds no pointer and registers a new environment, sobridge/reconnectfor the old sessions is refused with "Session does not belong to this environment". Putting the oldenvironmentIdback in the pointer by hand lets the CLI ask for reuse, which can only work if the old environment still exists server-side. My guess for the inconsistent results is that it depends on whether the archive and deregister calls got through (for example on a flapping connection) or the server had already cleaned up the environment; I have no log to confirm that.So on this path a 10-minute network outage ends like a clean exit without resume. #92194 (btmerr, 2026-09-04, read from 2.1.260) describes the archiving side of the same give-up path; the pointer side above is what produces the "does not belong to this environment" refusal. Keeping the preserve branch, or at least the pointer, after a connection give-up would let the existing reuse logic do its job.