Remote Control spawns session host processes without --resume, silently wiping conversation context (working local fix included)
Environment
- Claude Code 2.1.220, Debian 13 (headless server),
claude remote-control --name <host>running as a systemd user service (always-on) - Sessions used daily from the mobile app and claude.ai/code against this one environment
Bug
When the remote-control daemon (re)starts, it loses track of the host processes of existing sessions. Opening any pre-existing conversation from the app afterwards makes the daemon spawn a fresh host like this:
versions/2.1.220 --print --sdk-url https://api.anthropic.com/v1/code/sessions/<cse> \
--session-id <cse> --input-format stream-json --output-format stream-json --replay-user-messages
Note: no --resume - even though the full local transcript for that conversation exists in ~/.claude/projects/... and the full history is also retrievable via GET /v1/code/sessions/<cse>/events. The app renders the full history (it comes from the API), but the model starts blank and answers along the lines of "this chat is empty" - from the user's perspective the assistant has amnesia mid-conversation.
Any daemon restart (crash, watchdog, update, reboot) also kills every session host in its cgroup, so after a restart every conversation opened from the app comes up context-free. Today that hit 6 long-running conversations at once.
Expected
The daemon should resume the existing transcript when spawning a host for a session it does not currently own, e.g. by passing --resume <local-session-uuid> --fork-session. This works flawlessly when done manually - which is how we fixed it locally.
Local fix that works (happy to share details)
We replaced the versioned binary with a tiny wrapper: when invoked with --sdk-url/--session-id cse_* and without --resume, it looks up the best local transcript for that cse (a deterministic per-session file plus an index) and execs the real binary with --resume <uuid> --fork-session appended. Combined with a cron that re-bridges disconnected sessions via POST /v1/code/sessions/<cse>/bridge, conversations now survive daemon restarts and reboots with full context. It would be trivial for the daemon to do this natively.
Related
#35583 (closed not planned), #60790 (closed as duplicate), #34531, #36401 - same underlying gap: session context does not survive the death of the host process.
Showing cached comments. Read the full discussion on GitHub ↗
3 Comments
Follow-up finding that makes a native fix more important: the wrapper-shim workaround described above turns out to be insufficient for daemon-initiated spawns.
The remote-control daemon spawns session hosts via its own executable path (child processes' argv[0] is the resolved binary, not the
versions/<v>path string), so a wrapper placed at the versioned path never runs for those spawns. Since the binary is statically compiled, there is noLD_PRELOAD-style interception either. We confirmed this with--debug-file: the daemon logs[bridge:session] Spawning sessionId=... sdkUrl=...with no resume attempt, and the child comes up context-free.We now run a second workaround layer: a watchdog loop (5 s) that detects any freshly spawned host lacking
--resumewhose session has a substantial local transcript, kills it immediately (even mid-generation - a half-written amnesiac reply is better than a delivered one), re-bridges viaPOST /v1/code/sessions/<id>/bridge, and respawns with--resume <uuid> --fork-session. Detection-to-replacement is ~15 s in practice.This works, but it is a race against the user's first message. The daemon has all the information needed to do this natively at spawn time.
Reproduced on 2.1.226. Related: #70096 (same symptom), #85028 (env-reclaim variant). Some mechanism detail from a debug trace:
The restart adopts the same remote session (
[bridge:init] Adopted session cse_01F1Y8… re-queued via bridge/reconnect, no newCreated initial session), so the amnesia is purely local: the child spawned to serve it gets--print --sdk-url … --session-id <cse>with no--resume, and writes a fresh transcript UUID every time.Three things not yet on any of these issues:
pinToCurrentBinary: trueand execs its own resolved binary path. PATH interception never fires.--resume=child path already exists in the self-hosted runner spawner. Transcripts already carry abridgeSessionId: cse_…marker (#68606), so the bridge has everything it needs to pass--resume <uuid> --fork-sessionon adoption. The capability exists, it just isn't wired into the standalone bridge.session_…, reconnect tries that first and gets400 Session not found, then succeeds with thecse_…form.Also worth noting:
--continueis no escape hatch for server operators. It forces single-session mode and skips environment reclaim, so there's currently no supported way to run a multi-session server whose sessions survive its own restart. The docs' Limitations section doesn't mention this failure mode, which presents as success: the app renders the full thread while the model answers from empty context.Happy to provide full debug logs.
@mawikpartners-jpg taking you up on "happy to share details" - specifically the respawn half of the watchdog. After the kill and the
POST /v1/code/sessions/<cse>/bridge, who spawns the replacement host, and with what credentials?The sdk-url + per-session access token normally arrive in the daemon's work-poll ack, which an external watchdog doesn't hold. Is it:
--resumethis time?)claude --session-id <cse> --resume <uuid> --fork-session?Also curious how you resolve cse -> local transcript uuid - marker in the jsonl, or your own index? Building the same thing into a supervisor and would rather match a known-working shape than rediscover the failure modes.