Skip to content

fix(desktop): warn on Rosetta + arch-stamp runner re-clone#936

Merged
lefarcen merged 3 commits intomainfrom
fix/rosetta-warn-and-arch-stamp
Apr 8, 2026
Merged

fix(desktop): warn on Rosetta + arch-stamp runner re-clone#936
lefarcen merged 3 commits intomainfrom
fix/rosetta-warn-and-arch-stamp

Conversation

@lefarcen
Copy link
Copy Markdown
Collaborator

@lefarcen lefarcen commented Apr 8, 2026

What

Two related fixes for users who install the wrong macOS architecture (reproduced manually on 2026-04-08, hit by two real users in one day):

  1. Warn on Apple-Silicon-running-x64 at app startup with a blocking dialog and a one-click link to the latest arm64 dmg in the user's current update feed.
  2. Include process.arch in the runtime extraction stamp so the cached nexu-runner.app and controller-sidecar are re-cloned when the user reinstalls a different arch of the same Nexu version.

Why

Both fixes were prompted by today's debugging session where two separate Apple Silicon users installed the x64 dmg by mistake:

  • macOS launches the wrong-arch build silently under Rosetta 2 with no warning.
  • The visible symptoms are 3-5x slower JS, 100% renderer CPU, and "Agent 启动中" never resolving — none of which look anything like an arch mismatch.
  • Recovery is also broken: even after reinstalling the correct arm64 dmg, ~/.nexu/runtime/.nexu-runner-version still matches the old (x64) install, so the cached nexu-runner.app + controller-sidecar are reused, the running arm64 Electron tries to load the cached x64 native modules (e.g. @snazzah/davey), and openclaw fails on the very first require() with Cannot find native binding. The gateway never binds 18789, the controller can never connect, and the UI is stuck on "Agent 启动中" forever.

Both users wasted hours debugging before we worked out it was an arch mismatch.

How

1. Rosetta detection + warning dialog (apps/desktop/main/index.ts)

  • isRunningUnderRosetta() reads sysctl.proc_translated (Apple's official Rosetta detection) with a 1s timeout. Returns false on any non-darwin platform, on darwin arm64, or on any failure.
  • warnIfRunningUnderRosetta() is called as the first thing in app.whenReady(), before any heavy startup work, so users with the wrong dmg are not stuck waiting on a slow cold start. Skipped in dev and skippable via NEXU_SKIP_ARCH_WARNING=1.
  • The dialog is localised (zh-CN / en) based on app.getLocale() and uses Electron's native dialog.showMessageBox() (no extra dependencies). Two buttons:
    • Download arm64 build: opens the resolved dmg URL in the system browser and exits the app.
    • Continue anyway: dismisses and proceeds with startup.
  • resolveLatestArm64DownloadUrl() constructs the download link from the same update feed the auto-updater uses, so the link always points at the exact build auto-update would otherwise install:
    • If NEXU_UPDATE_FEED_URL is set (the canonical feed for this build), swap any trailing /x64/arm64.
    • Otherwise build from https://desktop-releases.nexu.io/{channel}/arm64, where channel comes from NEXU_DESKTOP_UPDATE_CHANNEL (defaults to stable).
    • Fetch latest-mac.yml from that base, parse the first url: …dmg entry (matching the dmg, not the auto-update zip), and return the absolute URL.
    • On any fetch failure (offline, parse error, 404), fall back to the latest-mac.yml URL itself — modern browsers display it as text and it still reliably points the user at the right channel.

2. Arch in runtime extraction stamp (apps/desktop/main/platforms/mac/launchd-paths.ts, apps/desktop/main/services/launchd-bootstrap.ts)

  • buildRuntimeExtractionStamp() previously serialised only { appVersion, bundleVersion }. Both x64 and arm64 builds of the same nightly share those fields, so the cached runner stamp matched and the runner + controller-sidecar were never re-cloned across arch swaps.
  • Adding arch: process.arch to the JSON forces a re-clone whenever the running Electron's arch differs from the cached one.
  • Two duplicate copies of buildRuntimeExtractionStamp exist (one in platforms/mac/launchd-paths.ts, the live path used by index.ts; one in services/launchd-bootstrap.ts, dead code from a refactor). Both are updated to keep them consistent in case the dead copy gets reactivated.

Affected areas

  • Desktop app (Electron shell)

Checklist

  • pnpm typecheck — not run locally (worktree without node_modules); changes are type-stable: only adds a new helper function and a field to a JSON-stringified object.
  • pnpm lint — not run locally for the same reason.
  • pnpm test — N/A, no tests cover these specific paths today.
  • pnpm generate-types — N/A.
  • No credentials or tokens in code or logs.
  • No any types introduced.

Notes for reviewers

  • Manual verification: I will demo the dialog locally before merging. The cleanest way to verify on a real install:

    1. Build the x64 dmg of any nightly with this PR included.
    2. Install it on an Apple Silicon Mac.
    3. First launch should immediately show the warning dialog.
    4. Clicking Download arm64 should open the matching arm64 dmg URL from the same nightly channel and exit the app.
  • Why not also include arch in the openclaw sidecar stamp? The openclaw sidecar uses ${size}:${mtimeMs} of the source archive as its stamp (in manifests.ts). Each arch ships its own archive with different size/mtime, so the stamp comparison naturally catches arch swaps without needing an explicit arch: field. The bug was specifically in the runner/controller-sidecar paths, which use appVersion + bundleVersion.

  • Why use dialog.showMessageBox() over a React modal? The warning fires before any window or React tree exists, so a renderer-side modal would not be visible until the renderer process is spawned — by which point we have already done expensive setup work the dialog is supposed to short-circuit. The native dialog shows immediately, blocks main process startup, and requires zero extra dependencies.

  • Why not "don't show again"? Intentionally omitted. Users who install the wrong arch will keep seeing this every launch until they fix it, which is the desired pressure. If we add a "do not show again" toggle, users will reflexively click it and silently keep paying the Rosetta tax.

Closes the action item from today's launch-prep debugging session.

Two related fixes for users who install the wrong macOS architecture
(reproduced manually on 2026-04-08, hit by two real users).

1. Detect x64-on-Apple-Silicon and show a blocking warning dialog at
   app startup.

   The Intel build runs under Rosetta 2 on Apple Silicon Macs with no
   visible warning from macOS. The resulting symptoms — 3-5x slower JS,
   100% renderer CPU, 'Agent 启动中' never resolving — give no hint
   that the root cause is an arch mismatch. Catches it via
   sysctl.proc_translated and shows a localised dialog (zh-CN / en)
   with two buttons:

   - Download arm64 build: resolves the latest arm64 dmg URL from the
     same update feed (channel + arm64) the auto-updater uses, opens it
     in the system browser, and exits the app immediately so the user
     does not waste time on a slow Rosetta cold start.
   - Continue anyway: dismisses the dialog and proceeds with startup.

   Skipped in dev (the test runner is often x64) and skippable via
   NEXU_SKIP_ARCH_WARNING=1 for CI.

   The download URL is fetched dynamically from latest-mac.yml in the
   user's current channel (stable / beta / nightly), so the link always
   points at the exact build auto-update would otherwise install — no
   cross-channel surprises. On any fetch failure, falls back to the
   latest-mac.yml URL itself.

2. Include process.arch in the runtime extraction stamp.

   buildRuntimeExtractionStamp() previously only encoded
   { appVersion, bundleVersion } in the .nexu-runner-version stamp file.
   When a user reinstalls the same Nexu version with a different arch
   (e.g. x64 → arm64 of the same nightly), the stamp matched and the
   cached nexu-runner.app + controller-sidecar were reused even though
   their native binaries no longer match the running Electron. The
   resulting silent native-binding load failures (notably @snazzah/davey)
   leave openclaw stuck in early startup and the controller unable to
   connect to the gateway.

   Adding process.arch to the stamp forces a re-clone on arch swaps.
   The duplicate buildRuntimeExtractionStamp in services/launchd-bootstrap.ts
   is updated in the same way to keep both copies consistent.
Copy link
Copy Markdown

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

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: b3dfb1cee3

ℹ️ 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 apps/desktop/main/index.ts Outdated
- Read updateChannel and feed URL from runtimeConfig (which loads from
  build-config.json + env), not process.env directly. Packaged builds
  bake these into build-config.json, so the previous code would fall
  back to 'stable' for nightly/beta users (codex P1).
- Reorder dialog buttons so 'Download arm64' is the rightmost / default
  highlighted button per macOS HIG, guiding users toward the fix.
- Update launchd-bootstrap-edge tests to mirror the new stamp shape that
  includes process.arch.
- biome format fix for execFileSync call.
- Trim verbose comments.
@sentry
Copy link
Copy Markdown

sentry Bot commented Apr 8, 2026

Codecov Report

❌ Patch coverage is 1.17647% with 84 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
apps/desktop/main/index.ts 0.00% 83 Missing ⚠️
apps/desktop/main/platforms/mac/launchd-paths.ts 0.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

… + (recommended) label

Workaround for electron/electron#40466: on macOS Sonoma+, showMessageBox
does not visually highlight the default button when buttons use
non-standard labels and the dialog is unfocused. The download button
ends up looking identical to the cancel button, defeating the whole
purpose of the warning dialog.

Two layered fixes:
- Trailing space on the default-button label forces macOS to render the
  blue highlight (the documented community workaround for #40466).
- Append '(推荐)' / '(recommended)' to the same label as a textual
  fallback so the recommended action stays obvious even if the visual
  highlight ever stops working again.
@lefarcen lefarcen merged commit f816c17 into main Apr 8, 2026
17 of 19 checks passed
@mrcfps mrcfps deleted the fix/rosetta-warn-and-arch-stamp branch April 8, 2026 10:31
@lefarcen lefarcen mentioned this pull request Apr 8, 2026
@Celina-create Celina-create added priority:p2 P2-nice-to-have: Low impact, backlog type:feature Issue type is a feature request area/backend Backend (Node.js/OpenClaw runtime) labels Apr 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/backend Backend (Node.js/OpenClaw runtime) priority:p2 P2-nice-to-have: Low impact, backlog type:feature Issue type is a feature request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants