feat(cli): handle outdated hub sessions with drain and replace flow (#13727)
* feat(cli): handle outdated hub sessions with drain and replace flow Add logic to detect when the CLI is newer than the running Hub and provide users with options to either keep the older Hub running (to avoid interrupting active sessions from other clients) or force-replace it. Implement `describeOutdatedHubSessions` helper to show quantified session activity in the dialog, and add `HubOutdatedContent` UI component with detailed messaging for the `build_mismatch` case. The `unsupported_protocol` case remains a modal requiring update, while the softer mismatch now uses a toast with enter-to-replace or escape-to-keep choices. Includes tests for draining and replacing an older busy hub when forced. * fix(hub): gate desktop hub_upgrade behind trusted connection and make drain-first a hard guarantee Address review: an originless local WebSocket client could invoke the forceful hub_upgrade command, and a failed drain request still allowed a forced retirement, so work started during the wait window could be killed. - hub_upgrade now requires the same canApproveTools per-connection gate as the tool-approval commands. - upgradeManagedHub skips the idle-wait window when the drain was not established (an undrained hub keeps admitting work, so waiting only widens the blast radius) and refuses to replace a busy hub that did not accept the drain, force or not. An idle hub is still replaced so pre-drain-endpoint hubs (404) remain upgradable. * fix(hub): treat failed activity readings as unknown, not idle, during hub upgrade A transient session.list failure inside the drain wait window previously read as an idle hub, which could end the grace window early and authorize retirement while turns were still finishing. - Failed readings never end the wait window early, never overwrite the last real observation, and never authorize a non-forced retirement. - Without force, a hub whose activity was never confirmed is handed back un-drained (still_busy) instead of retired; an undrained hub is now replaced only when positively observed idle. - With force and an accepted drain, an unanswerable hub is still replaced: the user already consented to interrupting its sessions. * fix(hub): never retire an undrained hub on an idle snapshot An older hub that rejects the drain has no admission barrier, so a single idle reading cannot authorize retirement: a session admitted right after the snapshot would die in a retire the consent prompt never covered. upgradeManagedHub now retires a hub only under an accepted drain. The undrained-idle case is delegated to the locked ensure path, which re-checks activity immediately before its own retire ladder and attaches (deferring the swap) when new work arrived in the meantime; the upgrade then reports still_busy instead of replaced, and the desktop/TUI surfaces tell the user to retry. * fix(hub): require an accepted drain unconditionally before any upgrade retirement Review follow-up: the undrained-idle delegation still reached retireDiscoveredHub, whose own drain attempt is best-effort, so a session admitted after the idle re-check could die in the shutdown. upgradeManagedHub now fails fast when the hub does not accept the drain - no wait window, no idle exception, no delegation. The drain is the admission barrier that keeps every subsequent reading true through the retire; a hub too old or wedged to accept it is left to the automatic ensure path, which replaces it once idle at the next client startup, and the error says so. * fix(hub): establish the drain barrier before the automatic idle check Review follow-up: the automatic incompatible-hub path read session activity first and drained only inside the retire ladder, so a session admitted between the idle snapshot and the shutdown could be terminated. retireIncompatibleHub now requests the drain before the busy check: with the drain accepted, the idle reading stays true through the retire. A deferred (busy) hub, and one whose retirement fails or is skipped by the circuit breaker, gets the drain lifted so it never sits alive-but-refusing work. Hubs that do not accept the drain (pre-/drain builds answer 404) keep the historical best-effort snapshot rather than being stranded forever. * polish(hub): tighten the outdated-hub dialog copy Two short sentences instead of four long ones, spell out what Quit Cline does (closes the app, leaves the Hub running), and rename the action to Update Now in both the desktop dialog and the TUI variant. * fix(cli): show the keep-Hub reminder toast when the outdated-hub dialog is dismissed (#13754) dialog.choice() resolves undefined on Esc rather than rejecting, so the reminder toast in .catch() never ran. Move it to the falsy branch of .then(), matching the unsupported_protocol handler. --------- Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
B
Bee committed
4d28d82efaf907a2e591c708812347054d08da80
Parent: b427fae
Committed by GitHub <noreply@github.com>
on 9/2/2026, 3:40:40 AM