Remote Control archives live sessions after a transient network give-up, bypassing the preserve-on-shutdown path
Preflight Checklist
- [x] I have searched existing issues. Closely related reports exist (#71873, #90387, #91627) — see Related Issues. This report supplies the mechanism #71873 was closed for lacking.
- [x] This is a single bug report
- [x] I am using the latest version of Claude Code
What's Wrong?
claude remote-control treats a temporary loss of connectivity as a fatal error.
On that exit path it archives every active session and deregisters the
environment. The sessions were healthy and the user never archived them, but
they show up archived in the mobile app and on claude.ai/code.
The shutdown path already has a preserve branch that skips archiving. A
connection give-up sets the same fatal flag as a genuinely unrecoverable error,
so it bypasses that branch.
From the strings in the shipped bundle (2.1.260):
// Archiving is skipped only when preserveOnShutdown is set AND the fatal flag is clear
if (e.preserveOnShutdown && !ft) {
// "Environment preserved. Restart `claude remote-control` to reconnect…"
return;
}
if ($t.size > 0)
await Promise.allSettled([...$t].map((v) => d.archiveSession(...)));
await d.deregisterEnvironment(r);
// …and the plain connection give-up sets that flag, alongside real fatal errors
if (j >= w.connGiveUpMs) {
o.logError(`Server unreachable for ${...} minutes, giving up.`);
...
ft = !0;
break;
}
The budget is connGiveUpMs: 600000 against a retry backoff capped atconnCapMs: 120000. Ten minutes of budget with a two-minute cap allows only
about six probes, so a window of recovery can be missed. There is no environment
variable or flag to raise the threshold.
What Should Happen?
A lost network connection is not an unrecoverable environment. The give-up path
should take the existing preserve branch and leave sessions alone, so they are
still there when connectivity returns. Archiving should stay reserved for
teardowns that genuinely end the environment.
Error Messages/Logs
From ~/.claude/logs/ on my machine, ANSI stripped. The bridge runs under a
service wrapper that restarts it.
An active session killed and archived directly by the give-up:
[19:10:42] Connection error, retrying in 149.9s (225s elapsed): timeout of 10000ms exceeded
[19:13:32] Connection error, retrying in 137.6s (395s elapsed): timeout of 10000ms exceeded
[19:16:10] Connection error, retrying in 113.1s (552s elapsed): timeout of 10000ms exceeded
[19:18:23] Error: Server unreachable for 11 minutes, giving up.
[19:18:23] Shutting down 1 active session(s)…
[19:18:23] Session failed: Process exited with error cse_01UzhdL4Gt8U9hGgyTqFjVWh
[19:18:28] Failed to archive session session_01UzhdL4Gt8U9hGgyTqFjVWh: not found
[19:18:28] Failed to archive session cse_01UzhdL4Gt8U9hGgyTqFjVWh: not found
[19:18:30] Environment deregistered.
An earlier occurrence where the give-up was demonstrably premature. The
bridge declared the server unreachable, the wrapper restarted it, and the new
process connected on its first attempt:
[15:59:56] Error: Server unreachable for 11 minutes, giving up.
[15:59:56] Failed to deregister environment: Remote Control is only available with claude.ai subscriptions. Please use `/login` to sign in with your claude.ai account.
[15:59:56] Environment offline.
Remote Control v2.1.220
Spawn mode: worktree
...
·✔︎· Connected · agent-os
Note the deregister failed with an auth message rather than a network one, which
suggests these "connection errors" may in part be failed token refreshes
surfacing as timeouts. I have not confirmed that, but it matches the trigger
suspected in #71873.
In both captured cases the archive call returned not found, because the server
had already reaped the environment. So what I can prove directly is that the
bridge attempts to archive healthy sessions after a network blip. When the
server still holds the session — the expected case for a shorter outage — that
call succeeds, which is the user-visible symptom reported in #71873 and #90387.
Steps to Reproduce
- Start a server:
claude remote-control --spawn worktree --capacity 4 - Open the printed
claude.ai/code?environment=…URL and send a message, so a
session is active.
- Cut the machine's network connection — or block the API host — and keep the
machine awake. Do not let it sleep; the poll loop detects a sleep gap and
resets its error budget, which masks the bug.
- Wait past the ten-minute
connGiveUpMsbudget. Observe
Server unreachable for 11 minutes, giving up. followed by
Shutting down N active session(s)…, archive calls, and
Environment deregistered.
- Restore the network. The session is gone from the default list.
Related Issues
Filed separately because the trigger and code path differ from each of these.
- #71873 (closed, not planned) — "Remote Control session auto-archives while
still active," which that reporter attributed to a connection / token-refresh
blip without being able to point at a mechanism. This report is that
mechanism. Worth reconsidering alongside it.
- #90387 (open) — sessions archived at teardown and never unarchived,
invisible on mobile. Same destination, different trigger (desktop app
auto-update). That report explicitly excludes server mode: "Sessions on a
separate machine running claude remote-control in server mode are unaffected."
This is a server-mode path that reaches the same outcome. The unarchive-never-
propagates half of that report applies here too and makes the loss permanent.
- #91627 (open) — the archive call is issued against a constructed
session_ id that the server has no record of. I see the same thing, and can
add one data point: in --spawn worktree mode the bridge issues two
archive calls for one session, once as session_<ULID> and once as
cse_<ULID> (visible in the log above, same ULID, five seconds apart). The
shutdown set is a Set of id strings, so it dedupes by exact match, while the
id-comparison helper used on the line above it already handles both forms.
That belongs to #91627; I mention it only because it appears in my logs.
Claude Model
Opus
Is this a regression?
I don't know
Claude Code Version
2.1.260 (Claude Code), native install. Also observed on 2.1.220.
Platform
claude.ai subscription via /login, not an API key
Operating System
macOS 26.5.2, arm64
Additional Information
- Spawn mode
worktree, capacity 4, bridge run under a service wrapper that
restarts it on exit.
- Operational note for anyone hitting this: system sleep is safer than a flaky
connection. The poll loop detects a sleep gap and resets its error budget, so
closing the lid is fine. A connection that keeps timing out while the machine
stays awake burns the budget through to the archive.
3 Comments
Confirming on 2.1.272 – 2.1.283, macOS 26 and 27 (native install,
claude remote-controlunder launchd withKeepAlive). Same mechanism: afterPersistent errors for 10 minutes, giving up.the process exits 0, the environment is deregistered, and every session in the app shows "Can't reach <host>" for good. The supervisor's restart registers a new environment, so nothing reattaches.Three more paths that end the same way (sessions left on an environment no process serves), in case they help whoever fixes this:
bridge-pointer.jsonif its mtime is older than 4 h ([bridge:pointer] stale mtime, clearing) and registers a new environment. rc refreshes the file hourly, but only while it runs, so any restart that takes effect more than 4 h after the previous process stopped (a start that stalled, a reboot waiting at the FileVault login screen) orphans every session.launchctl kickstart -k. The replacement can start while the old pid is still alive, take the "pointer writer pid still running" branch, and register a fresh environment.This folder is already served by a terminal claude remote-control on this device. Stop it first.to any other registration for that folder, including a brand-new environment. rc exits about a minute after that error, so a supervisor sees a restart loop until the window passes.Workaround until this is fixed:
SIGSTOPthe rc process while the network is down andSIGCONTit afterwards. rc treats the gap as system sleep, resets its error budget, and never reaches the give-up. I packaged that with mitigations for the paths above (pointer freshening, restoring the environment, reattaching stranded sessions) as https://github.com/VTAkbay/claude-rc-keepalive (MIT, macOS). It depends on exactly these internals, so a fix here would make it unnecessary.Confirming on 2.1.284, Linux (Ubuntu 26.04, kernel 7.0). Several
claude remote-controlservers run on a headless host, each in tmux under a systemd user unit with awhile true; do claude remote-control …; sleep 30; donerestart loop.What happened (2026-09-29, ISP outage of about 87 minutes)
Server unreachable for 10 minutes, giving up.and exited. Each one archived its live sessions and deregistered its environment.getaddrinfo EAI_AGAIN api.anthropic.comuntil the network came back. Each server then registered a fresh environment with one empty session. The earlier sessions were listed under "Other" on claude.ai/code and their processes were gone.claude --remote-controlsession (not server mode) rode out the whole outage and reconnected by itself. So the difference is in the server-mode give-up path, not in the network or the host.Workaround tried:
claude remote-control --continue. It does reattach to the last recorded session for the directory. But it starts in single-session mode ("Single session · exits when complete"), so the server loses--capacityand same-directory spawning. That makes it unusable as a replacement for a multi-session server, and we reverted it.What would fix it for us (either one):
claude remote-control --continuereattach as a server: same environment, spawn mode and capacity kept, and the previously live sessions brought back.Same mechanism on 2.1.288, Linux: three
claude remote-control --spawn=same-dirservers under systemd user units (Restart=always).connGiveUpMsand gave up, so their supervisor restarts registered new environments. The third had started its error clock later and survived.getaddrinfo EAI_AGAIN api.anthropic.com, about 540 restarts each. On recovery each one registered a fresh environment and pre-created an empty session. The give-up path had also deletedbridge-pointer.json(clearBridgePointerafter the failed deregister), so there was nothing to reuse.The backend itself is tolerant. We ran four probes (throwaway servers, 9 Oct):
| Test | Result |
|---|---|
|
SIGKILL(no archive or deregister), restart with the pointer after 15 min | same environment, same pre-created session || same, after 1 h 54 min | same environment, same pre-created session |
|
SIGSTOP20 min, thenSIGCONT| same environment,Reconnected after 2s, no give-up ||
SIGSTOP1 h 50 min, thenSIGCONT| same environment,Reconnected after 2s, no give-up |So the environment survives long silences. What loses it is the client's own give-up: it tries to archive and deregister, and then clears the pointer.
Note also that
EAI_AGAIN(andENOTFOUND) are not in the connection-error code set, so a DNS outage counts againstgeneralGiveUpMs("Persistent errors"), notconnGiveUpMs. Both are 10 min.Our workaround on Linux, in case it helps others:
SIGSTOP) each server's main process after about 60 s with api.anthropic.com unreachable, and resumes it (SIGCONT) when the API answers. The sleep-gap check (> connCapMs*2) resets the budget, so the server never gives up.bridge-pointer.jsonand restores it before the next start if it has gone missing.Either fix suggested above would make both unnecessary: take the preserve path on a give-up, or keep retrying as the interactive mode does.