Skip to content

🤖 feat: self-update a supervised mux server from the About dialog - #4083

Draft
ibetitsmike wants to merge 7 commits into
mainfrom
mike/server-self-update
Draft

🤖 feat: self-update a supervised mux server from the About dialog#4083
ibetitsmike wants to merge 7 commits into
mainfrom
mike/server-self-update

Conversation

@ibetitsmike

Copy link
Copy Markdown
Contributor

Summary

mux server can now update itself from the About dialog when it runs under a restart supervisor with the launcher layout the coder/mux registry 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@next under /tmp/mux/npm, launcher symlink /tmp/mux/mux -> npm/node_modules/.bin/mux, nohup restart loop with RESTART_ON_KILL_VALUE=true). Until now the only way to pick up a newer build was a workspace rebuild: UpdateService had 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.ts recognizes the layout (launcher symlink resolving to the running dist/cli/index.js, package manager from the lockfile above node_modules, declared supervisor via RESTART_ON_KILL_VALUE=true or XUM_SERVER_SUPERVISED, registry override), registry.ts fetches dist-tags (latest for stable, next for nightly; a -next. version implies nightly when no channel is saved), staging.ts installs the exact version into a sibling xum-staging-<version> directory with the detected package manager (--ignore-scripts), verifies the package and smoke-runs --version, and prunes older stages; activation.ts swaps the launcher symlink atomically (temp link + rename); serverUpdater.ts is the state machine behind the existing update.* oRPC.
  • Restart gate: 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 new install-blocked status and nothing happens.
  • Shutdown admission: WorkspaceService and TerminalService latch shuttingDown synchronously so no new session, terminal, or direct command can start between the idle snapshot and teardown; the CLI cleanup() is reused as the restart callback.
  • Fail closed: the feature reports 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-auth are required).
  • UI: the About dialog is no longer gated on window.api; it renders the unsupported reason, the blocker list, and an indeterminate download. Browser clients compare GET /version with the bundled build after reconnecting and reload when it changed.
  • Schema: UpdateStatusSchema gains unsupported and install-blocked; downloading.percent is nullable.
  • Docs: docs/config/server-access.mdx gets an "Updating the server" section, including the caveat that the module's max_restart_attempts counts self-updates.

Validation

  • Remote dogfood UAT through Coder Agents against the pushed head: staged, blocked (streams, turns, queued messages, terminals, background processes), unsupported, registry-failure, and idle install paths, with the supervisor relaunching the published nightly and the browser reloading onto it. Round 1 found the generated-token lockout and the MUX_BINARY + direct-entry bypass; both are now refused as unsupported and were re-verified.
  • 18 red-green mutation guards over the layout rules, staging verification, activation atomicity, blocker composition, shutdown latches, restart-once, and the browser reload path.
  • tests/ipc/update.test.ts confirms the harness reports unsupported and the no-op path leaves the server running.

Risks

  • The restart relies on the supervisor relaunching the launcher path. The module's 10-restart cap counts self-updates and is invisible to the server (documented; a module change is the follow-up).
  • Anything between the blocker snapshot and the shutdown latch is covered by the existing partial-stream crash recovery, not by the gate.
  • Browser reconnect has a ~45 s budget; a slower relaunch shows the existing Retry affordance.

Generated with xum • Model: anthropic:claude-fable-5-1 • Thinking: xhigh • Cost: $74.80

…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.
@mintlify

mintlify Bot commented Sep 4, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
Mux 🟢 Ready View Preview Sep 4, 2026, 2:08 PM

💡 Tip: Enable Automations to automatically generate PRs for you.

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

This comment has been minimized.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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.

Comment thread src/node/services/serverUpdate/installLayout.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/node/services/backgroundProcessManager.ts
Comment thread src/browser/contexts/API.tsx Outdated
Comment thread src/node/services/workspaceService.ts
…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.

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

This comment has been minimized.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/node/services/workspaceService.ts Outdated
Comment thread src/node/services/serverUpdate/installLayout.ts Outdated
Comment thread src/node/services/serverUpdate/registry.ts Outdated
Comment thread src/node/services/serverUpdate/staging.ts
Comment thread src/node/services/serverUpdate/staging.ts
…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.

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/node/services/serverUpdate/serverUpdater.ts
Comment thread src/node/services/serverUpdate/installLayout.ts
Comment thread src/browser/contexts/API.tsx Outdated
Comment thread src/node/services/serverUpdate/registry.ts
Comment thread src/node/services/workspaceService.ts
@chatgpt-codex-connector

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.

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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.

Comment thread src/node/services/serverUpdate/staging.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/node/services/workspaceService.ts
Comment thread src/node/services/serverUpdate/installLayout.ts Outdated
Comment thread src/node/services/serverUpdate/staging.ts Outdated
Comment thread src/node/services/serverUpdate/serverUpdater.ts
… 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.

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Security review completed. No security issues were found in this pull request.

Reviewed commit: 479d7b80e9

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ 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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +553 to +557
const counts: Array<[RestartBlocker["kind"], number]> = [
["active-streams", this.streamManager.getActiveStreams().length],
["terminals", this.terminalService.getOpenSessionCount()],
["background-processes", this.backgroundProcessManager.getRunningProcessCount()],
];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +553 to +556
const counts: Array<[RestartBlocker["kind"], number]> = [
["active-streams", this.streamManager.getActiveStreams().length],
["terminals", this.terminalService.getOpenSessionCount()],
["background-processes", this.backgroundProcessManager.getRunningProcessCount()],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +3957 to +3963
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++;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +136 to +143
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +269 to +273
try {
const response = await fetch(`${backendBaseUrl}/version`, {
cache: "no-store",
signal: AbortSignal.timeout(SERVER_VERSION_CHECK_TIMEOUT_MS),
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +23 to +25
bun: ["add", "--ignore-scripts", "--exact"],
npm: ["install", "--no-global", "--no-audit", "--no-fund", "--omit=dev", "--ignore-scripts"],
pnpm: ["add", "--ignore-scripts"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant