Remote Control spawns session host processes without --resume, silently wiping conversation context (working local fix included)

Status Open
Reported on v2.1.220
Maintainer reply None cached
Activity 6 comments · opened Aug 6, 2026

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.

View original on GitHub ↗

3 Comments

mawikpartners-jpg · 23 days ago

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 no LD_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 --resume whose 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 via POST /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.

thevinchi · 22 days ago

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 new Created 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:

  1. The wrapper-shim workaround fails because the spawner is built with pinToCurrentBinary: true and execs its own resolved binary path. PATH interception never fires.
  2. A --resume= child path already exists in the self-hosted runner spawner. Transcripts already carry a bridgeSessionId: cse_… marker (#68606), so the bridge has everything it needs to pass --resume <uuid> --fork-session on adoption. The capability exists, it just isn't wired into the standalone bridge.
  3. Every start burns a self-inflicted 400: bridge-pointer.json stores the id as session_…, reconnect tries that first and gets 400 Session not found, then succeeds with the cse_… form.

Also worth noting: --continue is 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.

thevinchi · 22 days ago

@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:

  • (a) the daemon re-spawns on its next poll (and if so, why with --resume this time?)
  • (b) your watchdog spawns the child itself from the bridge POST response
  • (c) something like 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.

Showing cached comments. Read the full discussion on GitHub ↗