fix(desktop): warn on Rosetta + arch-stamp runner re-clone#936
Merged
fix(desktop): warn on Rosetta + arch-stamp runner re-clone#936
Conversation
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.
There was a problem hiding this comment.
💡 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".
- 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.
Codecov Report❌ Patch coverage is
📢 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.
nettee
approved these changes
Apr 8, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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):
process.archin the runtime extraction stamp so the cachednexu-runner.appandcontroller-sidecarare 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:
~/.nexu/runtime/.nexu-runner-versionstill matches the old (x64) install, so the cachednexu-runner.app+controller-sidecarare reused, the running arm64 Electron tries to load the cached x64 native modules (e.g.@snazzah/davey), and openclaw fails on the very firstrequire()withCannot 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()readssysctl.proc_translated(Apple's official Rosetta detection) with a 1s timeout. Returnsfalseon any non-darwin platform, on darwin arm64, or on any failure.warnIfRunningUnderRosetta()is called as the first thing inapp.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 viaNEXU_SKIP_ARCH_WARNING=1.app.getLocale()and uses Electron's nativedialog.showMessageBox()(no extra dependencies). Two buttons: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:NEXU_UPDATE_FEED_URLis set (the canonical feed for this build), swap any trailing/x64→/arm64.https://desktop-releases.nexu.io/{channel}/arm64, where channel comes fromNEXU_DESKTOP_UPDATE_CHANNEL(defaults tostable).latest-mac.ymlfrom that base, parse the firsturl: …dmgentry (matching the dmg, not the auto-update zip), and return the absolute URL.latest-mac.ymlURL 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.arch: process.archto the JSON forces a re-clone whenever the running Electron's arch differs from the cached one.buildRuntimeExtractionStampexist (one inplatforms/mac/launchd-paths.ts, the live path used byindex.ts; one inservices/launchd-bootstrap.ts, dead code from a refactor). Both are updated to keep them consistent in case the dead copy gets reactivated.Affected areas
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.anytypes introduced.Notes for reviewers
Manual verification: I will demo the dialog locally before merging. The cleanest way to verify on a real install:
Why not also include arch in the openclaw sidecar stamp? The openclaw sidecar uses
${size}:${mtimeMs}of the source archive as its stamp (inmanifests.ts). Each arch ships its own archive with different size/mtime, so the stamp comparison naturally catches arch swaps without needing an explicitarch:field. The bug was specifically in the runner/controller-sidecar paths, which useappVersion + 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.