🤖 feat: self-update a supervised mux server from the About dialog - #4083
🤖 feat: self-update a supervised mux server from the About dialog#4083ibetitsmike wants to merge 7 commits into
Conversation
…alog ServerUpdater fills the UpdateService impl slot in CLI mode. It recognizes the coder/mux launcher layout (external bin symlink into a bun/npm/pnpm install) and a declared restart supervisor, checks the npm dist-tag for the effective channel, stages the exact version into a sibling xum-staging-<version> dir, and on install atomically re-points the launcher symlink and runs the SIGTERM cleanup path so the supervisor relaunches the new version. The restart is refused while volatile server-owned work exists (active streams, pending turn work, queued messages, pending auto-retry, open or starting terminals, running background processes or direct commands), and shutdown now latches new session, terminal, and command admission before the first await. Unrecognized layouts report unsupported and every action no-ops. The About dialog works in browser mode (unsupported reason, install-blocked blockers, indeterminate download), and browser clients reload when GET /version differs from the bundled build after reconnecting. --- _Generated with `xum` • Model: `anthropic:claude-fable-5-1` • Thinking: `xhigh` • Cost: `$60.84`_ <!-- mux-attribution: model=anthropic:claude-fable-5-1 thinking=xhigh costs=60.84 -->
… close the install race Stage verification no longer depends on a .bin shim (pnpm shims and the hardcoded mux name go away); the smoke run uses process.execPath. The installing latch is taken before the blocker snapshot so two concurrent install calls cannot both activate. Existing sessions stay reachable during shutdown; only new session creation is refused. Registry override resolves through the XUM/MUX compatibility resolver.
…ed process UAT found that a server running with a generated auth token relaunches with a new token, so every browser session lands on the auth page after an update. Self-update now reports unsupported unless the token is stable (MUX_SERVER_AUTH_TOKEN, --auth-token, or --no-auth). It also refuses when the process was started from the entry file directly even if MUX_BINARY names a launcher, because the supervisor would relaunch the old path.
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Automations to automatically generate PRs for you. |
|
@codex review |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
💡 Codex Security Review
Here are some automated security review suggestions for this pull request.
Reviewed commit: 67efeacddc
ℹ️ About Codex security reviews in GitHub
This is an experimental Codex feature. Security reviews are triggered when:
- You comment "@codex security review"
- A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review
Once complete, Codex will leave suggestions, or a comment if no findings are found.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 67efeacddc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…try trust - Refuse plaintext registries except on loopback: the staged package is executed by the smoke run, so a network registry must be TLS-protected. - Count workspaces whose background init is still running as a blocker. - Refresh lazily tracked background process statuses before the synchronous blocker snapshot so a naturally exited command cannot block restarts indefinitely. - Give /version the API CORS treatment so the reconnect version probe works when the frontend is served from another origin.
|
@codex review |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c4d2e364bd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…utdown - Count workspaces from the start of provisioning (init abort controllers), not only from init settlement registration. - Require HTTPS registries outright; loopback plaintext can still traverse an inherited HTTP proxy. - Build the proxy dispatcher on first use so a malformed proxy variable is a check error instead of a startup crash. - Refuse to prune stages when the launcher no longer points at the running entry. - Abort a pending staged install during shutdown and wait for it to settle so a detached package manager cannot outlive the server.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2e9ebe94df
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
This comment has been minimized.
This comment has been minimized.
…tch shutdown - Resolve the published mux forwarding shim (bin/mux.js) to the @coder/xum entry so the registry module layout is recognized under every package manager; activation and pruning use the same resolution. - Block restarts while workspaces are being removed, archived, forked, or staged, and refuse new removals and archives once shutdown has begun. - Latch the updater on shutdown so no check, download, or install can start a detached installer after cleanup began. - Reload the browser when git_describe differs too, so a stable build on the same commit as a nightly is picked up. - Probe /version only when the bundle is served from the backend origin; split-origin dev bundles and extension webviews cannot be refreshed by a reload. Generate src/version.ts before the VS Code extension build. - Document that registries must be HTTPS without credentials.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Security Review
Here are some automated security review suggestions for this pull request.
Reviewed commit: 1d4d584a1f
ℹ️ About Codex security reviews in GitHub
This is an experimental Codex feature. Security reviews are triggered when:
- You comment "@codex security review"
- A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review
Once complete, Codex will leave suggestions, or a comment if no findings are found.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1d4d584a1f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… gate - Verify a staged package with node --check instead of executing it; nothing from the registry runs until the operator activates it. - Always re-point the launcher the process was started through (argv[1]); a declared XUM_BINARY/MUX_BINARY may only confirm that path. - Count renames and context mutations as lifecycle blockers and refuse new renames once shutdown has begun. - Pass --no-global to npm so inherited global config cannot redirect the stage. - Roll the runtime channel back when persisting the preference fails.
|
@codex review |
|
Security review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 479d7b80e9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const counts: Array<[RestartBlocker["kind"], number]> = [ | ||
| ["active-streams", this.streamManager.getActiveStreams().length], | ||
| ["terminals", this.terminalService.getOpenSessionCount()], | ||
| ["background-processes", this.backgroundProcessManager.getRunningProcessCount()], | ||
| ]; |
There was a problem hiding this comment.
Gate restarts on project lifecycle operations
When another authenticated client is cloning, creating, or removing a project, none of those operations contributes to this snapshot. Install & restart can therefore exit during ProjectService.cloneWithProgress() or createUnlocked()—including while a detached Git clone or git init is mutating the destination—leaving a partial clone/directory that can block the next attempt. Add project lifecycle reservations to the blocker snapshot and latch new project mutations out once shutdown begins.
AGENTS.md reference: AGENTS.md:L11-L11
Useful? React with 👍 / 👎.
| const counts: Array<[RestartBlocker["kind"], number]> = [ | ||
| ["active-streams", this.streamManager.getActiveStreams().length], | ||
| ["terminals", this.terminalService.getOpenSessionCount()], | ||
| ["background-processes", this.backgroundProcessManager.getRunningProcessCount()], |
There was a problem hiding this comment.
Block restart while PortableDesktop sessions are live
When a live or starting PortableDesktop session exists, this list can still be empty, so installation proceeds into disposeOnce(), whose desktopSessionManager.closeAll() invokes PortableDesktopSession.close() and runs down. That terminates the user's live GUI/browser state even though comparable terminals are explicitly protected; include both registered sessions and startup promises in the restart gate.
AGENTS.md reference: AGENTS.md:L10-L11
Useful? React with 👍 / 👎.
| const pendingTurns = new Set(this.preflightSendCounts.keys()); | ||
| let queuedMessages = 0; | ||
| let autoRetries = 0; | ||
| for (const [workspaceId, session] of sessions) { | ||
| if (session.hasActiveOrPendingTurnWork()) pendingTurns.add(workspaceId); | ||
| if (session.hasQueuedMessages()) queuedMessages++; | ||
| if (session.hasPendingAutoRetry()) autoRetries++; |
There was a problem hiding this comment.
Count in-process workflows before restarting
When a background workflow is between agent turns, hasInProcessWorkflowWork(workspaceId) remains true but the session may have no active stream, pending turn, queue entry, or retry, so this method reports no blocker. The update shutdown then aborts the workflow runner and leaves the run interrupted, requiring manual recovery despite the restart gate; incorporate the existing in-process workflow admission registry into this snapshot.
AGENTS.md reference: AGENTS.md:L10-L11
Useful? React with 👍 / 👎.
| const previous = this.currentChannel; | ||
| if (this.impl) { | ||
| this.impl.setChannel(channel); | ||
| } | ||
| await this.config.setUpdateChannel(channel); | ||
| try { | ||
| await this.config.setUpdateChannel(channel); | ||
| } catch (error) { | ||
| this.impl?.setChannel(previous); |
There was a problem hiding this comment.
Roll back to the implementation's inferred channel
When no channel is persisted and the installed version is nightly, ServerUpdater correctly infers nightly, but currentChannel was initialized by Config.getUpdateChannel() to its fallback stable. If switching to stable then fails to persist, this catch calls setChannel("stable") again rather than restoring nightly, leaving the dialog on nightly while subsequent checks use stable. Fresh evidence beyond the earlier rollback fix is the mismatch between the implementation's inferred channel and this cached config fallback; capture this.impl.getChannel() before changing it.
AGENTS.md reference: AGENTS.md:L150-L150
Useful? React with 👍 / 👎.
| try { | ||
| const response = await fetch(`${backendBaseUrl}/version`, { | ||
| cache: "no-store", | ||
| signal: AbortSignal.timeout(SERVER_VERSION_CHECK_TIMEOUT_MS), | ||
| }); |
There was a problem hiding this comment.
Publish the reconnected client before probing version
When the same-origin /version route is slow or stalls behind a proxy while the WebSocket ping succeeds, this awaited fetch keeps the provider in reconnecting with api: null for the full five-second timeout on every reconnect. Version discovery is documented as non-blocking, so publish the working client first and run the reload probe independently, while retaining the connection-id guard before reloading.
AGENTS.md reference: AGENTS.md:L10-L10
Useful? React with 👍 / 👎.
| bun: ["add", "--ignore-scripts", "--exact"], | ||
| npm: ["install", "--no-global", "--no-audit", "--no-fund", "--omit=dev", "--ignore-scripts"], | ||
| pnpm: ["add", "--ignore-scripts"], |
There was a problem hiding this comment.
Force pnpm installs to remain local
When pnpm is selected and inherited configuration sets global=true, this branch lacks the local-mode override added to npm, so Download can modify the pnpm global installation instead of the staging directory before verification fails. Fresh evidence beyond the earlier npm fix: checked pnpm 10.28.1, where pnpm add --help lists -g, --global, npm_config_global=true pnpm config get global returns true, and --no-global overrides it; add that override or sanitize the inherited setting for pnpm too.
AGENTS.md reference: AGENTS.md:L11-L11
Useful? React with 👍 / 👎.
Summary
mux servercan now update itself from the About dialog when it runs under a restart supervisor with the launcher layout thecoder/muxregistry module creates. The server checks the npm dist-tag for its channel, stages the exact version next to the current installation, and on install re-points the launcher symlink and exits through the normal graceful shutdown so the supervisor relaunches the new version. Restarts are refused while any volatile server-owned work exists, and unrecognized layouts fail closed with a visible reason.Background
The dogfood server is installed by the registry module (
bun add --ignore-scripts mux@nextunder/tmp/mux/npm, launcher symlink/tmp/mux/mux -> npm/node_modules/.bin/mux, nohup restart loop withRESTART_ON_KILL_VALUE=true). Until now the only way to pick up a newer build was a workspace rebuild:UpdateServicehad a null implementation in CLI mode and the About dialog said updates were desktop-only. The module's restart loop already acts as a crash supervisor, so an update only needs to stage a new version and exit cleanly.Implementation
src/node/services/serverUpdate/:installLayout.tsrecognizes the layout (launcher symlink resolving to the runningdist/cli/index.js, package manager from the lockfile abovenode_modules, declared supervisor viaRESTART_ON_KILL_VALUE=trueorXUM_SERVER_SUPERVISED, registry override),registry.tsfetches dist-tags (latestfor stable,nextfor nightly; a-next.version implies nightly when no channel is saved),staging.tsinstalls the exact version into a siblingxum-staging-<version>directory with the detected package manager (--ignore-scripts), verifies the package and smoke-runs--version, and prunes older stages;activation.tsswaps the launcher symlink atomically (temp link + rename);serverUpdater.tsis the state machine behind the existingupdate.*oRPC.ServiceContainer.collectRestartBlockers()composes existing signals (active streams, sessions with pending turn work, queued messages, or pending auto-retry, pre-stream and direct-command reservations, open or starting terminals, running background processes). Any blocker yields the newinstall-blockedstatus and nothing happens.WorkspaceServiceandTerminalServicelatchshuttingDownsynchronously so no new session, terminal, or direct command can start between the idle snapshot and teardown; the CLIcleanup()is reused as the restart callback.unsupported(and every action no-ops) without a supervisor, when the process was not started through a launcher symlink, when the package manager is ambiguous, or when the auth token is generated per process (a relaunch would lock every browser session out;MUX_SERVER_AUTH_TOKEN,--auth-token, or--no-authare required).window.api; it renders the unsupported reason, the blocker list, and an indeterminate download. Browser clients compareGET /versionwith the bundled build after reconnecting and reload when it changed.UpdateStatusSchemagainsunsupportedandinstall-blocked;downloading.percentis nullable.docs/config/server-access.mdxgets an "Updating the server" section, including the caveat that the module'smax_restart_attemptscounts self-updates.Validation
MUX_BINARY+ direct-entry bypass; both are now refused as unsupported and were re-verified.tests/ipc/update.test.tsconfirms the harness reportsunsupportedand the no-op path leaves the server running.Risks
Generated with
xum• Model:anthropic:claude-fable-5-1• Thinking:xhigh• Cost:$74.80