[BUG] Desktop app: re-arming Remote Control never retires the dead bridge handle — mobile list accumulates permanent duplicate sessions
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
This is not the arming bug. #82140, #78730 and #78564 all cover Remote Control failing to re-arm after the hosting process ends. This issue is about what happens after a successful manual re-arm: a bridge handle orphaned by unclean termination is never retired, so the mobile Code tab gains a permanent duplicate row. Fixing the arming bug does not fix this — and, as noted below, would make it worse.
Environment: Claude Desktop 1.24012.9 (x64 MSIX), Windows 11 Pro 26200, bundled Claude Code 2.1.219, auto-connect resolving as source=explicit_pref.
Setup (the part covered by #82140). A Desktop session with Remote Control active appears as a Connected row in the mobile app's Code tab, holding bridge handle session_01<A>. The hosting Claude Code process then ends without the app running its shutdown path. In my case this was an OS reboot; a crash or force-kill is equivalent. The bridge dies with it, and the mobile app shows that session as Disconnected. Relaunching does not re-arm Remote Control for the restored sessions, so the user runs /remote-control manually. The command succeeds and mints a second bridge handle, session_01<B>. A clean quit does not produce this — see the correction comment below. Handles that are live when the app shuts down cleanly are retired, and their rows disappear from the mobile Code tab rather than lingering as Disconnected.
The defect starts here. The new handle is appended to the session's bridgeSessionIds array, and the dead handle session_01<A> is never retired server-side:
// claude-code-sessions/<account>/<org>/local_<L>.json
"bridgeSessionIds":["session_01<A>","session_01<B>"]
The mobile Code tab now shows that single Desktop session as two rows: one Connected and current (session_01<B>), one Disconnected and frozen at the content from before the process ended (session_01<A>). The stale row is server-held state, not mobile-client cache — it survives a force-close and relaunch of the mobile app.
Neither handle can be retired individually. bridgeSessionIds is append-only:
e.bridgeSessionIds ?? (e.bridgeSessionIds = []),
e.bridgeSessionIds.includes(l) || (e.bridgeSessionIds.push(l), this.saveSession(e))
and the retirement path I could locate in the bundle runs when the local session is archived or deleted, where it acts on every handle the session has ever held:
const l = i ? [...new Set([a.bridgeSessionId, ...a.bridgeSessionIds ?? []]
.filter(g => typeof g == "string" && g !== ""))] : [];
// → deleteRemoteCodeSession / archiveRemoteCodeSession per id
So archiving or deleting the Desktop session removes the stale row and the working row together, taking the live bridge down along with the dead one. A clean app shutdown also retires live handles — observed behaviour, see the correction comment — but it does not touch handles that were already orphaned. Neither path clears a single stale row on its own.
Why this is not a duplicate of #82140 / #78730 / #78564. Those three end where this one begins. They are fixed by evaluating the auto-enable predicate on resume; this one is fixed by not orphaning the previous handle when a new one is minted. Ship the arming fix alone and every restored session silently gains a duplicate row instead of a stale one — the symptom changes, the row count still grows.
Impact. Each unclean-termination-then-re-arm cycle leaves one more permanently orphaned row. Users who keep long-lived named Desktop sessions accumulate near-identical Disconnected rows in the mobile Code tab that cannot be cleared individually, making the correct row progressively harder to identify. No error is emitted at any point in this sequence.
What Should Happen?
When Remote Control is re-armed for a Desktop session whose previous bridge handle is dead, that dead handle should be retired server-side, so the mobile Code tab shows one row per Desktop session.
Error Messages/Logs
Unclean termination. The app's own log distinguishes this case from a quit — note outside app quit — and the exit code is 1073807364 (0x40010004, DBG_TERMINATE_PROCESS):
[error] Session local_<L> query error: Claude Code process exited with code 1073807364
[warn] [pty-host] worker exited (1073807364) with 5 live PTY(s); refork on demand
[error] Network Service process gone (reason=killed, exitCode=1073807364) outside app quit — main-process networking is broken until relaunch
The later re-arm logs as a clean success, with no mention of the handle it orphaned:
[info] Enabling remote control for session local_<L>
[info] [remote-control] bridge_state: ready
[info] [remote-control] bridge_state: connected
[info] Remote control enabled: https://claude.ai/code/session_01<B>
Steps to Reproduce
- Enable Remote Control by default in Desktop settings, then start a new Desktop session. The session bridges and appears as a Connected row in the mobile app's Code tab.
- Terminate the app without a clean shutdown — reboot the machine, or end the process from Task Manager. Do not use the tray icon's Quit: that retires the handle cleanly and will not reproduce this. The mobile app shows that session as Disconnected.
- Open the restored session in the Desktop app and send it a message. The session runs normally but unbridged, and the mobile app still shows it as Disconnected with content frozen at step 1.
- Run
/remote-controlin that session. It reconnects, and the mobile Code tab shows a Connected row with current content. - Observe the mobile Code tab: the one Desktop session is now listed as two rows, one Connected and one Disconnected.
- Force-close and reopen the mobile app. The Disconnected row persists.
- Inspect
claude-code-sessions/<account>/<org>/local_<L>.json—bridgeSessionIdscontains both bridge handles.
Claude Model
Opus
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.219 (bundled in Claude Desktop 1.24012.9, x64 MSIX)
Platform
Anthropic API
Operating System
Windows
Terminal/Shell
Other
Additional Information
This is the Claude Desktop app's Code tab, not a terminal session, so the Terminal/Shell dropdown has no accurate option.
Suggested fix
Stated against observable state rather than as a patch, since the implementation is not public.
Rebind the existing remote session rather than creating a new one.
When a session's bridge dies, the remote code session itself is still valid — only the transport is gone. The current re-arm path treats that as "create a new remote session", which is what produces the duplicate row. A rebinding operation would avoid the problem at the source:
resumeRemoteCodeSession({ apiHost, token, remoteId, ... })
reattaching a new local bridge connection to the existing remoteId and returning the same session_url. bridgeSessionIds never grows, no handle is orphaned, and the existing mobile row flips Disconnected → Connected in place with its history intact.
If the handle must rotate — for example because it doubles as a bridge capability token — then the equivalent is a transfer rather than a create:
updateRemoteCodeSession({ apiHost, token, previousRemoteId, newRemoteId, ... })
carrying the row's identity and history from the old handle to the new one, so the mobile list still shows one row per Desktop session.
Expected behavior after the fix. One row per Desktop session, whose state tracks the live bridge. A session that loses its bridge and later re-arms returns to Connected on its existing row with continuous history, rather than appearing as a second row.
Ordering constraint. Whichever shape is chosen, it should land with, or before, the resume auto-re-arm fix in #82140. Automatically re-arming restored sessions while re-arm still mints a fresh handle would turn every currently-stale row into a duplicate row rather than repairing it.
Existing orphans, and the only current workaround. Rebinding prevents new orphaned rows but does not clear the ones already on users' accounts. Today the sole way to remove an already-orphaned row is to archive or delete the entire Desktop session, which runs the existing cleanup over every handle in bridgeSessionIds and removes the live row along with the stale one — so it is not a usable workaround for anyone still working in that session. /remote-control retires nothing; it only mints and appends. A one-off cleanup path, or a way to retire a single handle, is needed regardless of which fix shape is chosen. A clean app quit retires only handles that are live at shutdown; it leaves existing orphans untouched.
Related issues
Recording the cluster map for cross-referencing, grouped by what would fix each group.
Arming — bridge never (re-)established (shared root cause: the auto-enable predicate is evaluated only when a session is created)
- #82140 —
maybeAutoEnableRemoteControlgated onisFirstTurn - #78730 — same finding via app relaunch; per-session Remote Control state is runtime-only (macOS)
- #78564 — resuming a previously-bridged session does not re-mint a bridge (Windows)
- #82462 — registration lost after
/clear— same predicate, different trigger
Retiring — dead handles never cleaned up
- This issue — re-arming appends to
bridgeSessionIdswith no per-handle removal - #83193 — duplicate Connected/Disconnected rows in the mobile Code tab (the user-visible symptom)
Disconnect path — missing null guard
- #77915 —
/remote-controldisconnect throws onsession_url - #78336 — same TypeError, leaves the toggle ON
Reconnection after transport loss (distinct — the bridge dies without the hosting process dying)
- #34255 — no automatic reconnection, drops silently (macOS/iOS)
- #79388 — no auto-reconnect after a network interruption
Feature requests that would obviate much of this
- #77985 — machine-level Remote Control surviving session death
- #48949 — persistent always-on Remote Control for the Desktop app
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗