Skip to content

fix: make macOS desktop self-update swap+relaunch fail-fast and recoverable - #134

Merged
OmarB97 merged 1 commit into
mainfrom
fix/desktop-mac-swap-failfast-fork
Jun 10, 2026
Merged

fix: make macOS desktop self-update swap+relaunch fail-fast and recoverable#134
OmarB97 merged 1 commit into
mainfrom
fix/desktop-mac-swap-failfast-fork

Conversation

@OmarB97

@OmarB97 OmarB97 commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Why

Revives the work from NousResearch#38410, which was closed after an old force-push left its branch without the changes its description promised (the branch's only remaining commit was a subset of the NousResearch#38446 codesign re-seal). The original commit was recovered from the fork's pre-force-push branch head via the repo activity log.

The macOS in-app updater can quit Hermes.app and never reinstall/reopen: the detached swap script gated only ditto on success and fell through silently when the bundle swap failed, leaving the user on the old build — or with no app at all — after a dead quit.

What changed

  • apps/desktop/electron/main.cjs: the applyUpdatesPosixInApp swap script now runs set -euo pipefail, detects ditto and destination-replace failures immediately, cleans up the .hermes-update-old staging copy via an EXIT trap, and on any swap failure falls back to opening the freshly rebuilt bundle directly instead of leaving the user with a dead quit.

Deliberately not revived from the original PR:

  • hermes_state.py PRAGMA busy_timeout = 5000 — superseded by the BEGIN IMMEDIATE + jitter-retry redesign (fix: SQLite WAL write-lock contention causing 15-20s TUI freeze NousResearch/hermes-agent#3385), which intentionally keeps the SQLite busy handler short (timeout=1.0) and handles contention with application-level retries. A 5s busy_timeout would override that design.
  • The second recovered commit (9cf5343ae, "harden relaunch and wait loop") — it regressed the fail-fast invariant (dropped set -e and the failure guards, so a failed ditto would have deleted the app with no replacement) and stripped load-bearing comments. It reads as an abandoned experiment, not part of the reviewed fix.

How to review

Review the swapScript template in applyUpdatesPosixInApp (apps/desktop/electron/main.cjs, ~line 1815). The invariant: on ANY swap failure the user ends up with a working app launched (the rebuilt bundle via the open "$SRC" fallback) and no orphaned .hermes-update-* staging copies.

Evidence

Functional simulation of the rendered swap script with /usr/bin/open and /usr/bin/xattr stubbed:

  • Happy path: destination replaced with the new bundle, relaunch targets the destination, no staging leftovers, rc=0.
  • ditto-failure path (source missing): destination untouched (old bundle intact), [updates] ditto failed logged to stderr, fallback open targets the rebuilt source bundle, no leftovers, rc=0.

Verification

  • node --check apps/desktop/electron/main.cjs
  • bash -n on the rendered swap script
  • The two functional simulations above

Risks & gaps

  • If moving the old bundle aside fails AND rm -rf of it also fails, set -e aborts with the original app intact but no relaunch (the user reopens manually) — strictly better than the prior silent fall-through.
  • The open "$SRC" fallback launches the rebuilt bundle from the build tree rather than /Applications; this avoids a no-app state but may not match the user's preferred installed location (same trade-off as reviewed in the original PR).

…erable

The macOS in-app updater's detached swap script ran with plain 'set -u'
and a success-gated ditto: when ditto or the destination move failed,
the script fell through silently -- the app had already quit, the old
bundle stayed (or was left moved aside), and 'open "$DST"' either
relaunched the stale build or nothing at all. This matches the reported
"Update now does not actually update" behavior.

Make the swap fail-fast and recoverable:
- set -euo pipefail so unexpected failures stop the script instead of
  compounding.
- Detect ditto and destination-replace failures explicitly; on either,
  fall back to opening the freshly rebuilt bundle directly so the user
  is never left with a dead quit.
- Clean up the .hermes-update-old copy via an EXIT trap so it is
  removed on every exit path.

Recovered from the pre-force-push head of desktop-mac-swap-fix
(NousResearch#38410, which lost this work to a force-push
and was closed). The sqlite busy_timeout half of that PR is
intentionally not revived: the BEGIN IMMEDIATE + jitter-retry redesign
(NousResearch#3385) deliberately keeps the connection busy handler short, and a 5s
busy_timeout would override that design.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🔎 Lint report: fix/desktop-mac-swap-failfast-fork vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 10553 on HEAD, 10553 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 5543 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@OmarB97
OmarB97 merged commit 0945665 into main Jun 10, 2026
14 of 20 checks passed
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