* fix(cli): land a Web Shell update on the current latest release The daemon pins its update target when it prepares. Once the state is `ready` the check returns the cached status even for `?refresh=true`, so polling never re-downloads — and Web Shell prepares automatically on mount. For a long-lived daemon the prepared release is therefore whatever was latest shortly after it started. Clicking Update restarted into that stale target, and every release published in between needed another prepare-and-restart round, which reads as advancing one version per click. The CLI resolves and installs at each process start, so it always reached the current latest in one step. Re-resolve the release once at activation, after the restart has been acknowledged, and stage the newer one when it is higher and one-click covers the installation. Both the standalone and the managed npm paths share one resolver, so eligibility cannot drift between the check and the click. Readiness stays pinned and background polling still never re-downloads; only the explicit click re-resolves. A failed re-resolve or staging applies the prepared release instead, so a registry failure cannot strand an explicit update request on the running version. The activation guard still preserves an equal or newer active installation, and a failed activation of the newer release cleans both payloads. Staging runs after the acknowledgement and can take minutes, during which the daemon keeps answering. The browser now measures its give-up deadline from continuous unreachability rather than from the click: a reachable daemon is still working, and the old click-time budget would have reported a failure while the update was in fact succeeding. The protocol reference and both language versions of the update design doc state the pinned-readiness contract and the new deadline semantics. * fix(cli): keep an update restart observable and its payload committed An audit of the activation-time re-resolve found three defects in it. Discarding the superseded payload ran before the newer one was committed, so a failed discard fell back to a payload that might already be half removed and orphaned the one just staged, losing the update entirely. Commit to the newer payload first and discard the old one best-effort: a failed discard now leaks disk instead of losing the update. Dropping the browser's click-time deadline had also removed its only overall bound. A daemon whose process replacement does not land on the new build comes back reachable and settled on the same version, and the control then waited forever for a version change that would never arrive. Restarting is sticky until the process is replaced, so an acknowledged restart followed by any other state with an unchanged running version now terminates the wait and reports a failure. That path publishes the settled state itself, because the poll immediately after it can drop while the daemon is still coming back, which would otherwise leave a stale restarting status rendering no control at all. The rule is gated on the restart having been acknowledged: polling starts on the click, before the request resolves, and can still observe the pre-click state. Browser fixtures now model the daemon's contract — restarting from the acknowledged restart until the process is replaced — because a fixture that keeps reporting a prepared state after the click cannot exercise that distinction. Assertions are unchanged; only the stimulus became faithful. Also pin the edges the re-resolve depends on: a prerelease below the prepared release, a registry rollback, build metadata that does not change precedence and a malformed registry value must none of them restage, and an in-flight restart must survive the operator disabling automatic updates, concurrent polls and retries, and ordinary shutdown. Every mechanism added here is mutation-witnessed, including the two halves of the deadline change and the acknowledgement gate. * fix(cli): sample the restart acknowledgement when the poll starts The settled-state termination read the acknowledgement ref after awaiting the status request, which does not establish that the response was sampled after the acknowledgement. The status and restart requests are independent, so the first restart poll can be issued while the restart is still pending and deliver its pre-restart snapshot after the 202 arrives. The ref is then true, the stale snapshot looks like a daemon that settled without updating, and the control reports a false failure and clears the restart — after which a genuine version change is handled by the idle branch, so the page never reloads and the captured task URL is never restored. Sample the flag when the poll is initiated instead. A response delivered after the acknowledgement by a poll started before it is now ignored, and the next poll — initiated afterwards — is the first one allowed to conclude the replacement did not land. The regression test holds the status request open, acknowledges the restart, then delivers the old snapshot, and requires no error plus a successful reload once the new version is reported. Also restore an absolute backstop, which the move to a continuous- unreachability deadline had removed. A daemon that stays reachable but never leaves restarting — a hung activation or process replacement — would otherwise leave the control disabled forever with no error, and a page reload hides it entirely. The cap sits above the daemon's own staging ceilings (five-minute archive download, ten-minute managed npm child) so a slow but progressing update never reaches it, and a test pins both halves: the wait survives fourteen minutes of a reachable restarting daemon and ends at the cap. Both the protocol reference and the two language versions of the design doc state the sampling order and the backstop. * fix(cli): log why a click fell back, and keep the entry after giving up Three review findings on the update restart path. A registry outage at click time left no trace. The release check resolves an error result rather than throwing, so the fallback returned silently and "the registry said nothing newer" was indistinguishable from "the registry was unreachable" — both install the older prepared release. Log the classified failure before falling back. The catch below only ever sees detection or staging throw, so it now names restaging instead of sending the reader to the registry. The test that covered this used a rejected check promise, a shape the real function does not produce for an outage; it now resolves the real error result and asserts the log, and a second test pins that a staging failure is not reported as a registry failure. Giving up took the update entry with it. While the wait runs, each successful poll publishes the daemon's state, so a wedged daemon leaves it at restarting; the give-up branch cleared the restart without publishing anything, and the render gate does not display restarting, so the entry unmounted and could not return while the daemon kept reporting it. The user got one notification and then no affordance at all. Every give-up path — both deadlines and a daemon-reported error — now keeps a retryable entry on screen. The design sentence claiming the control is never hidden was untrue of these paths; it is now true of all of them. The wedged-daemon test asserted only that the spinner was gone, which a vanished control also satisfies, so it now asserts a visible enabled entry. The absolute cap was sized against a ceiling the code does not have. The standalone archive download tries OSS and then GitHub sequentially with a five-minute budget each, and the checksum and signature fetches add up to two more minutes, so one restage can spend about twelve minutes downloading and verifying before activation — not the five the docs and the constant's comment claimed. That left the fifteen-minute cap about three minutes of headroom while the text promised a slow update would never reach it. Raise the cap to twenty minutes and state the real chain in the protocol reference, both design docs and the comment.
12 KiB
Web Shell updates
Problem and scope
Web Shell shows the running version but cannot prepare or apply updates. Check and download in the background; only show a small Update button beside the lower-left version when the payload is ready. Clicking applies the update, restarts the connected daemon, and returns to the URL open when clicked.
This updates the connected daemon's installation, including remote daemons. It does not update an embedded host. Restart ends running work through the daemon's existing graceful shutdown path; it does not resume an interrupted model turn.
Design
- Authenticated, process-global
GET /daemon/updatechecks for a release without downloading or installing. Cache checks for 15 minutes and errors for one minute;?refresh=trueforces a check. Concurrent checks share one request. POST /daemon/update/preparedownloads a server-selected release and returns immediately. Reuse standalone archive download/checksum verification or stage a managed npm installation without changing the active installation. Never hold the installation lock while waiting for the user. At activation, recheck the installation under its lock and preserve an already equal or newer version.POST /daemon/update/restartacknowledges before applying the prepared payload. Only this explicit action activates the update. After acknowledging, re-resolve the current release once and, when it is newer than the prepared one and one-click covers it, stage the newer release and discard the prepared payload. A daemon that stays alive across several releases therefore lands on the current latest in one click, matching the CLI, which resolves and installs at every process start. Readiness itself stays pinned — polling never re-downloads — so the re-resolve belongs to the click, not to the check. A failed re-resolve or staging applies the prepared release: a registry failure must not strand an explicit update request on the running version. Commit to the newer payload before discarding the superseded one, and treat that discard as best-effort — a failed discard leaks disk, but must not orphan the payload just staged or fall back to one that may already be half removed. Staging runs after the acknowledgement because it can take minutes, and that window must be undisturbed: while the state is restarting, the status, prepare and restart routes return without consulting the operator setting and shutdown cleanup is a no-op, so neither disabling automatic updates nor concurrent clients nor an ordinary shutdown can pull the payload out from under an authorized restart. Then close daemon services and replace the process withprocess.execve. Preserve PID, startup directory, bound primary port, authentication token, and CLI arguments; discard managed version pins and browser-opening flags so the new launcher selects the update. Before closing services, require the standalone launcher's official shell header and run its--versionwith the restart environment, a 10-second timeout, and a valid semver result. A failed check leaves the current daemon serving. This checks the actual interpreter/runtime chain; it cannot guarantee recovery if files change after checking or if process replacement or serve startup fails.- Restrict preparation/restart to primary connections using the daemon token
or trusted tokenless loopback, actual CLI entry points with a restart callback, POSIX platforms with
process.execve, supported standalone installs and global npm/pnpm/yarn/bun installs (including root-owned npm prefixes), and operator settings permittinggeneral.enableAutoUpdate. Ignore workspace settings for this process-wide decision. Windows, embedded servers, development installs, and unsupported Node runtimes do not offer the button; other unsupported shapes report an available update with manual instructions instead. Eligibility and button visibility are widened by Web Shell update coverage. - Status is
available,up-to-date,installing(background preparation only),ready,restarting,unavailable, orerror, with current/latest versions,canInstall, optional instructions, and optional message. Concurrent mutations share one operation; a prepared payload survives browser reloads. Clean staged files on cancellation, activation, failure, and ordinary daemon shutdown. - Advertise
daemon_update; expose REST-only SDK check/prepare/restart methods. Keep existing authentication, CORS, and strict mutation authorization. Clients cannot supply installation commands, paths, or a target version. - Web Shell checks on mount and periodically, requests preparation automatically when supported, and polls preparation/restart every 2 seconds. The expanded sidebar places the version and button on a separate line above footer actions, including narrow widths. The collapsed sidebar shows an accessible icon when ready or when manual instructions are available. Use the shared button, semantic colors, and English/Chinese labels.
- Keep checks/downloads quiet. Explicit restart failures use the existing error notification. During restart, disable the button and tolerate connection loss; capture the full URL when clicked and, after the running version changes, restore that URL in the current history entry and reload the document, including when the URL has an unchanged fragment. Session requests may return 404 and navigate the shell home during restart; that must not change the return target. Measure the give-up deadline from continuous unreachability, not from the click: the daemon keeps answering while it re-resolves and stages, which can outlast any fixed click-time budget, and a reachable daemon is still working. If it has been unreachable for 60 seconds, show an error on the first completed poll. Keep a generous absolute cap as well — 20 minutes — so a daemon that stays reachable but never leaves restarting — a hung activation or process replacement — cannot disable the control forever with no error. Size it against the whole staging chain, not a single timeout constant: the standalone archive download tries OSS and then GitHub sequentially with a five-minute budget each, and the checksum and signature fetches add up to two more minutes, so one restage can spend about twelve minutes downloading and verifying before activation; the managed npm child is capped at ten. The cap must sit far enough above that chain that a slow but progressing update never reaches it. Unreachability is not the only terminal condition. Restarting is sticky until the process is replaced, so once the restart is acknowledged a reachable daemon reporting any other state with an unchanged running version means the replacement did not land on the new build: stop waiting, publish that settled state so the control offers whatever the daemon now reports, and report a failure. Sample the acknowledgement when the poll is initiated, not when its response arrives: the status and restart requests are independent, so a poll started before the acknowledgement can deliver a pre-restart snapshot after it, and treating that snapshot as a settled daemon reports a false failure and suppresses the reload a successful update still owes the user. Dropping the short click-time budget must not leave the control waiting forever for a version change that will never arrive. No failure path may hide the control either: a wedged daemon keeps reporting restarting, which the render gate does not display, so every give-up path — both deadlines and a daemon-reported error — keeps a retryable entry on screen rather than leaving the user one notification and then no affordance at all. Unmounting cancels browser polling, not daemon work.
- Hide the control for older daemons and the desktop shell. Custom footer items
can omit
update; this also disables the browser's preparation requests.
Ownership and affected areas
The routes, release check, installer, staged payload, and restart belong to the daemon process, not a selected workspace or session owner. Detection uses the startup directory and operator settings. Only primary connections with credentials that survive restart (or trusted tokenless loopback) offer updates. Restarting still invalidates other devices' ephemeral pairings; those devices must pair again. SDK and Web Shell are the consumers; neither can redirect the operation into a workspace runtime.
Changes cover CLI serve routes/lifecycle/entry points and installer staging helpers, SDK daemon types/client exports, Web Shell sidebar/translations, and protocol documentation. Existing interactive CLI update behavior remains intact.
Validation and acceptance
Establish the missing API/UI baseline with global qwen serve in an isolated
runtime. Never update the user's installation during tests. Exercise real local
archives/npm fixtures with fake activation boundaries; verify preparation does
not change the active install, cached payload activation works, and cleanup and
existing direct update behavior hold. Test authorization, primary-listener scope,
settings/platform/embedding restrictions, duplicate operations, failure recovery,
response-before-restart ordering, and preserved restart arguments/environment.
Exercise an unusable launcher in a disposable process with real execve, and
verify that the startup check rejects before closing a working listener. Pin
cleanup during a failed restart so a refreshed check can prepare another update.
Cover the activation-time re-resolve on both the standalone and managed npm
paths: a newer release replaces the prepared payload and the old one is cleaned;
polling alone never re-downloads; and an unreachable registry, a release with
nothing newer, a newer release without a one-click path, and a failed restage
each apply the prepared payload, with the failed restage leaving it intact until
after activation. Pin the comparison direction against a prerelease below the
prepared release, a registry rollback, build metadata that does not change
precedence, and a malformed registry value — none may restage. Pin that a failed
discard of the superseded payload still activates the newer one, and that an
in-flight restart survives the operator disabling automatic updates, concurrent
polls and retries, and ordinary shutdown.
Test hidden/up-to-date/downloading/ready/restarting/error states, explicit-only activation, disconnection/recovery, timeout, collapsed controls, narrow layouts, and bilingual labels. The timeout coverage must distinguish a reachable daemon that is still staging from an unreachable one: only continuous downtime trips the deadline, and a successful poll restarts it. A daemon that comes back settled on the same version must also terminate the wait, report a failure, and hand back an actionable control instead of hanging or disappearing. Cover both response orderings around the acknowledgement: a settled snapshot delivered before it must not terminate the wait, and one delivered after it by a poll that was initiated before it must not either — only a poll initiated afterwards may. Cover a daemon that stays reachable and wedged in restarting, so the absolute cap is shown to end the wait without firing during a slow but progressing update, and assert a visible retryable entry afterwards — "the spinner is gone" is equally true of a control that vanished, so it cannot witness this property. Browser fixtures must model the daemon's own contract — restarting from the acknowledged restart until the process is replaced — because a fixture that keeps reporting a prepared state after the click cannot exercise that distinction. Build, typecheck, bundle, run focused package tests, and verify the packaged daemon plus browser flow. Record limitations separately. The browser regression must drive a restart-time session 404, observe the shell's own navigation, then verify recovery restores the original task URL.
Acceptance: no update control appears until preparation completes; the version and update button sit together without overlapping footer actions; clicking once applies/restarts once into the current latest — not into the release that happened to be latest when the daemon prepared — and returns to the same task after recovery. Unsupported or disabled installations remain usable without a misleading restart button.
Open questions
None. Windows replacement and restarting arbitrary embedded hosts are outside this change.