fix(coding-agent): reconnect attached windows when the daemon restarts - #2458
sethkarten wants to merge 5 commits into
Conversation
Prime Agent performance — completedPR Overall: 0 regressed · 0 improved · 41 no clear change.
Python runtime
Session transport
UI interactions
Sandbox cost: ~$0.0983 — no inference calls. Methodology and samplesMain resolved at 2026-09-20T05:01:35.876043+00:00. Harness
|
Every attached TUI window died permanently when the daemon restarted: the supervisor broadcast daemon_closing "shutdown" even when an update restart was prepared, and a shutdown-reason close was treated as a permanent session loss, so windows showed the saved-transcript error and then failed every further send. - Supervisor: a shutdown command during a prepared update restart now closes clients with reason "update" so every attached window takes the existing update-recovery path (parity with daemon-mode.ts). The reason is captured at dispatch so a later phase change cannot rewrite it, and Agents View skips its daemon relaunch for that reason while the update coordinator owns the restart. - DaemonAgentConnection: a shutdown-reason close now runs a bounded recovery loop (reconnectTimeoutMs, default 60s) that polls the same socket path, re-attaches the same session (matched by session file or session id, active id may differ after a restart), and resyncs the transcript; it never relaunches the daemon. Session discovery waits no longer than the bound, so the terminal close lands within it; on timeout it keeps the current saved-transcript hard-fail message. - Reconnect banners are version-honest: the recovered window reports the daemon version, and when the daemon is newer than this window's binary it says so instead of pretending the window is updated.
86127a9 to
5442ea4
Compare
…sync - formatDaemonReconnectBanner now treats a numeric-equal daemon release as newer than the client own prerelease (per semver 1.2.3 > 1.2.3-beta.1), so prerelease windows see the restart-to-update warning. - A generic shutdown recovery now yields permanently once an update recovery restored the session (its sessionRevision bump), not merely while updateRestartPending is set, so it can no longer emit a duplicate session_resynced/connected.
…tdown - Route a shutdown session_closed into the bounded shutdown-recovery reconnect only when the daemon announced itself closing first (daemon_closing): an orderly daemon shutdown now recovers attached windows like a reasoned socket close does, while a bare session stop (worker /stop, no notice) still terminal-closes immediately. - The notice clears on every attach, so a later bare stop after a recovered shutdown is not misrouted into a reconnect hang.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 09c8308. Configure here.
…d close relay - An orderly supervisor shutdown archive-stops its workers, so attached windows read the relayed session_closed with reason "killed" after the daemon_closing notice; route that close into the shutdown-recovery reconnect like a direct worker link's "shutdown" close. The notice, not the close reason, stays the discriminator: a bare session stop (worker /stop, no notice) still terminal-closes immediately.

Motivation
Running
/updatein one window restarts the daemon, but every OTHER attached TUI window died permanently: it showedand every further send failed with
Cannot send daemon command "prompt" because the Prime Agent daemon is not connected.The window never reconnected.Two causes, both verified against the code and a scripted unix-socket probe (daemon close + new daemon on the same socket path):
DaemonSupervisorhardcodeddaemon_closingreason"shutdown"for theshutdowncommand, even when an update-restart coordinator had just prepared the daemon.daemon-mode.tsderives the reason from the update phase; the supervisor did not. Attached windows therefore saw a plain shutdown instead of an update restart and skipped the existing update-recovery path.DaemonAgentConnection. Any daemon restart that is not an update (manual stop + start, stale-daemon replacement) killed every attached window even though the daemon came back on the same socket path and the session transcript stayed on disk.Implementation
daemon-supervisor.ts: theshutdowncommand now closes clients with reason"update"when the update restart is prepared (captured at dispatch so a later phase change cannot rewrite it); a plain stop still reports"shutdown".daemon-agent-connection.ts: a shutdown-reason close now runs a bounded recovery loop (reconnectTimeoutMs, default 60s, 100ms poll) instead of an immediate terminal close. The loop reconnects to the same socket path, lists sessions, matches the SAME session by session file or session id (the active id changes across a restart), re-attaches, and emitssession_resyncedso the view re-renders. Session discovery waits no longer than the bound, so the terminal close lands within it. It never relaunches the daemon (an explicit stop stays stopped); on timeout it falls back to the existing saved-transcript hard-fail message. An update-restart recovery still outranks it (including mid-iteration, so the two recoveries cannot emit duplicate resyncs), and a generic reconnect in flight yields to it.restoreConnectionAfterDaemonRestart) and reused by both the update and shutdown flows.agents-view-mode.ts: an update-reason close now reconnects withoutrecoverDaemon, so the Agents View never spawns a competing daemon while the update coordinator owns the restart (a plain outage keeps the relaunch behavior).connection_statusnow carries an optionaldaemonVersion(the restarted daemon'sdaemon_hello.appVersion) when recovery re-attached.Daemon restarted (vX) - reconnected; when the restarted daemon is NEWER than this window's binary it warnsDaemon restarted (vX), this window still runs vY - restart the window to pick up the update.(an older or unorderable daemon version is reported without the advice). No auto-relaunch: the repo has no safe relaunch mechanism for a passive reconnect path (/updateowns its own relaunch).Verification
agent-connection-daemon.test.ts: a shutdown close with the daemon coming back recovers (reconnect, re-attach the same session under its new active id, resync, connected banner carries the daemon version, the shutdown-specific reconnecting reason is reported, no terminal close); a shutdown recovery that times out (fake timers) emits the existing saved-transcript close.daemon-supervisor-admission.test.ts: theshutdowncommand closes with reason"update"while an update restart is prepared (with and without force), and"shutdown"otherwise.agents-view-state.test.ts: an update-reason close polls without callingrecoverDaemon; a plain outage still relaunches.interactive-update-relaunch.test.ts:formatDaemonReconnectBannermaps missing/matching/newer/older daemon versions to message + tone.daemon_closing "shutdown", exited, and a new daemon bound the same socket path; the connection recovered withreconnecting -> session_resynced -> connected (daemon version)and exactly one new socket on the restarted daemon. On main, the same scenario emitted the terminal saved-transcript close (the reported bug).agent-connection-daemon(87),daemon-agent-connection-reconnect-park,daemon-supervisor-admission/monitor,daemon-client,daemon-routed-client,daemon-peer-transport,agents-view-*,interactive-update-relaunch,rpc-*,acp-*,package-self-update-daemonall pass;npm run check(biome, test-policy, tsgo, installer, push-guard, browser-smoke) is green.daemon-supervisor-process.test.tsfails 12 tests locally, verified identical on pristineorigin/main(environment artifact: the suite inherits daemon-worker env vars that flip spawned supervisors into peer mode; it is excluded fromtest:ci).Note
Medium Risk
Changes core daemon disconnect/reconnect and session-restore paths with subtle precedence rules between update, shutdown, and generic reconnect; regressions could leave windows stuck reconnecting or close sessions that should recover.
Overview
Attached TUI windows no longer die permanently when the Prime Agent daemon restarts. Shutdown recovery polls the same socket path (default 60s), re-attaches the prior session by session file or ID, emits
session_resynced, and only falls back to the saved-transcript close if the daemon never returns. Recovery runs only when the daemon announceddaemon_closing; a bare session stop without that notice still closes the window.Update-restart fixes:
DaemonSupervisornow emits an update close reason when stopping a prepared update-restart daemon (so windows other than the one running/updateuse update recovery). Update and shutdown recoveries sharerestoreConnectionAfterDaemonRestart, with update taking precedence so resyncs and reconnect races are not duplicated. Generic reconnect and Agents View defer to shutdown/update recovery; Agents View skipsrecoverDaemonon update closes so it does not spawn a competing daemon.UI:
connection_statusconnected events can includedaemonVersion; the TUI shows a one-line reconnect banner and warns to restart the window when the recovered daemon is newer than the client binary.Reviewed by Cursor Bugbot for commit f6dcbab. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add bounded shutdown recovery to
DaemonAgentConnectionfor daemon restartsreconnectAfterShutdownand sharedrestoreConnectionAfterDaemonRestarttoDaemonAgentConnection, which polls for the daemon without relaunching, matches the prior session by file or ID, reattaches, and emitssession_resyncedDaemonSupervisorshutdown handler now passesupdateas the close reason during a prepared update-restart andshutdownotherwise;handleDaemonMessagetriggers shutdown recovery only when adaemon_closingnotice precedes the session closeconnection_statusevents now carry the restarted daemon version;InteractiveModeshows a reconnect banner that warns the user to restart the window when the daemon is newer than the client binarydaemon_closingnotice) remain terminal; ifrestoreConnectionAfterDaemonRestartfails to find the prior session within the deadline, it emits the existing shutdown close instead of retrying indefinitelyMacroscope summarized f6dcbab.