Update when agents finish: deferred self-update for web/VPS and desktop#526
Update when agents finish: deferred self-update for web/VPS and desktop#526SawyerHood wants to merge 9 commits into
Conversation
Web/VPS (npx bb-app): the update toast gains "Update when agents finish". The server stages an npm install of the latest bb-app under the data dir while agents keep running, watches for zero busy threads sustained over a 45s quiet period, then exits with a reserved code; the launcher swaps both children to the staged version (seconds of downtime) and falls back to the current version if the staged one fails to start. Capability is gated by a launcher-set env var, so dev servers, standalone bb-server, old launchers, and desktop-owned runtimes never see the protocol. Desktop: the "Desktop update ready" toast gains "Relaunch when agents finish" when the shell owns the local runtime. The main process polls the new /system/agents/activity endpoint and defers electron-updater's quitAndInstall until agents have been idle for the same quiet period. Fixes #477 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
df9fb69 to
c2a1bc5
Compare
The update toast now reads live agent activity: "Update now" when nothing is running, "Update when agents finish" while agents work. Both go through the same schedule endpoint; the server skips the 45s quiet period only when it has seen no busy agents since arming AND nothing is queued for the auto-send sweep to start (isServerAtRest), so a stale label can never kill an agent. Desktop mirrors this: plain Relaunch at rest, the deferred option only while agents are busy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The schedule endpoint gains an explicit mode: "when-idle" defers behind the idle watcher (with the at-rest fast path), "now" applies as soon as staging completes, interrupting running agents at the user's request. While agents work the web toast offers both "Update when agents finish" and "Update now"; re-posting "now" escalates an already-waiting schedule. At rest the single "Update now" button keeps mode when-idle since the fast path is already immediate but still backs off if an agent starts during staging. Crash resume stays conservative (when-idle). Scheduling from the toast no longer records a version dismissal, so cancelling brings the offer back. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The busy/idle action label now reads the same push-updated sidebar-navigation cache that renders the sidebar's running spinners, instead of polling /system/agents/activity every 15s: the label flips instantly and always agrees with what the sidebar shows. The bootstrap excludes side-chats and archived threads, so an idle-looking label maps to mode "when-idle", whose server-side at-rest gate counts every thread before skipping the quiet period. The activity endpoint remains for the desktop main-process deferred-relaunch poller, which has no SPA cache. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reverts the sidebar-cache derivation in favor of polling /system/agents/activity every second while the update choice is on screen. The endpoint is one indexed count on a small table (~1ms over loopback) and the poll is gated to when an update is available, capable, and not yet scheduled or dismissed — so the cost is negligible. Unlike the sidebar bootstrap (which excludes side-chats and archived threads) the endpoint counts every thread, so the busy/idle label is global truth rather than a filtered view, while still flipping within a second. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
From an agent review of the PR for simplification opportunities: - Flatten the launcher supervision loop: the inlined ~85-line swap branch becomes applySelfUpdateSwap + restartFromStack helpers, and the loop's duplicated per-process restart blocks collapse into one tail. - Drop requestedAt from the client-facing contracts (nothing read it); it survives only in the on-disk sentinel as debugging provenance. The desktop deferredInstall object collapses to a boolean. - Remove the illusory protocol "version" constant: the server parses the env var as a boolean, so the value never carried version information. - Share the 45s quiet period as one constant in @bb/config so the server watcher and desktop controller cannot drift. - Delete the dead parseSelfUpdateSentinel export, a redundant regex in resolveNpmInvocation, and the preload's duplicate invoke helper. - Extract useAgentsBusy so the unknown-reads-as-busy default cannot drift between the web and desktop toasts. No behavior changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The persistent update toasts were verbose and, once both button slots held real actions, had no visible way to dismiss. AppToastContent gains an opt-in corner close button (routes through the toast's normal dismissal, so onDismiss persistence still applies). The update toasts drop their redundant description lines — the version moves into the title — and the deferred action reads "Update when idle" / "Relaunch when idle". X on a scheduled toast only hides it; the update itself continues. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The tab that watched the update was left running the pre-update frontend bundle. On seeing the post-swap version, reload the page and stash the version in session storage so the fresh bundle announces "bb-app updated" after the reload. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Quiet period: queued follow-ups are now checked directly on every watcher tick (server via countQueuedIdleThreads, desktop via a queuedThreadCount field on /system/agents/activity) instead of insured against with time, so the shared quiet period drops from 45s to 10s and polls from 15s to 5s. After the last agent finishes, a deferred update now applies in ~10-15s. Queued work also reads as busy for the toast label. Blank page: reloading into the updated app could race the restart — a failed module fetch never retries, stranding a blank page (reproduced in a real browser). The reload now waits until the server serves the app shell, and index.html gains a one-shot boot watchdog that reloads if the app has not mounted after 10s. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Handoff notes (state as of
|
Fixes #477
bb running unattended (VPS via
npx bb-app, or the desktop app with an owned local runtime) can now apply updates without interrupting running agents: schedule the update from the toast, and it applies at the first moment no agents have been running for a 45s quiet period.Web / VPS (
npx bb-app)now: applies straight after staging, interrupting agents at the user's explicit request; also escalates an already-waiting schedule) (when the server runs under a capable launcher; otherwise the current "restart bb-app" copy is unchanged). While scheduled, a persistent toast shows the target version with Cancel update; after the swap, a "bb is now running vX" toast confirms.POST/DELETE /api/v1/system/update/schedule+selfUpdatestate on/system/version. The intent lives server-side, so closing the browser tab doesn't lose it.npm install bb-app@<latest>under<dataDir>/self-update/while agents keep running (a staging failure surfaces as an error toast before anything restarts), writes a sentinel file, and — oncethreads.status IN ('starting','active','stopping')stays empty for 45s — exits with a reserved code (75).BB_SELF_UPDATE_PROTOCOL=1only in the supervised full-stack path — never for dev servers, standalonebb-server, old launchers, or desktop-owned runtimes (electron-updater owns those). A pending schedule survives server crashes (resumed from the sentinel on boot); stale staged installs are pruned on boot.Desktop
GET /api/v1/system/agents/activityendpoint (same busy-thread definition as the server-side watcher, shared viaagent-activity.ts) and defersquitAndInstall()until the quiet period passes. Unreachable server counts as busy; the deferral auto-cancels if the downloaded update goes away; quitting the app still installs on quit as before.BbDesktopInfofields and API methods are optional, so a new SPA against an old shell degrades to today's behavior.Verified
bb-app@0.0.27→ 45s quiet → launcher swap → back up as 0.0.27 in ~5s, sentinel consumed. The e2e also exercised the failed-swap rollback and boot-time staging cleanup, and caught two real bugs now fixed with regression tests:npm_execpathpoisoned by pnpm, and symlinked staged paths (/tmp→/private/tmp) silently killing the daemon's argv-gated entrypoint.quitAndInstall, which the existing Relaunch button already exercises.🤖 Generated with Claude Code