# NousResearch/hermes-agent · Desktop app self-update reliability across platforms

## Open now

- 3 issues, 1 PR open on GitHub.
- #39248: \[Bug\]: Desktop app update process is broken
- #46076: Hermes Desktop in-app update does not reliably apply or relaunch on macOS
- #64577: \[Bug\]: macOS desktop self-update fails four ways on long-lived installs (stale staged updater, silent backup, canonical-app desync, retry-loop errors)
- PR #57763: fix: macOS in-app update silent failure handling (#57645)

These issues and pull requests address bugs and improvements in the desktop app's in-app self-update flow on both macOS and Windows, covering problems such as silent failures, update loops, missing progress feedback, and unreliable updater handoff.

## TL;DR

- 13 member(s): 3 open issue(s), 6 closed issue(s), 1 open PR(s), 2 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #50448. Verify it addresses your case.
- **Recommended:** read PR #50448 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `partially_resolved` · deciding PR(s): `pr-50448,pr-86675`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.12`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Open related issues

- #39248: \[Bug\]: Desktop app update process is broken
- #46076: Hermes Desktop in-app update does not reliably apply or relaunch on macOS
- #64577: \[Bug\]: macOS desktop self-update fails four ways on long-lived installs (stale staged updater, silent backup, canonical-app desync, retry-loop errors)

## Closed / duplicate issues

- #37557: \[Bug\]: Windows desktop: /update from chat silently hangs — no progress/logs, updater handoff not observable
- #39339: \[Bug\]: Desktop app update installs but version remains unchanged — update loop on macOS
- #43531: \[Bug\]: hermes desktop errors out when updating
- #50419: feat(desktop/windows): show update progress before the desktop exits so it doesn't look like a crash
- #57645: \[Bug\]: desktop in-app update on macOS closes without updating
- #66753: \[Bug\]: Desktop exits without confirming detached updater spawn or early process survival

## Prior & competing PRs

- **#42121: fix(desktop): stop macOS Update & Restart loop by always updating in-app**. closed without merge · fix tier `suggested`
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#50448: feat(desktop/windows): show update-in-progress feedback before the desktop exits**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `apps/desktop/electron/main.cjs`, `apps/desktop/src/i18n/en.ts`, `apps/desktop/src/i18n/ja.ts`, `apps/desktop/src/i18n/zh-hant.ts`, `apps/desktop/src/i18n/zh.ts`
  - fix hunks:
  - `apps/desktop/electron/main.cjs`

    ```diff
    @@ -1129,6 +1129,14 @@ function directoryExists(filePath) {
     // marker's own age ceiling; covers a stuck-but-alive updater).
     const UPDATE_WAIT_TIMEOUT_MS = 20 * 60 * 1000
     const UPDATE_WAIT_POLL_MS = 1000
    +// How long the desktop lingers on the "updating, don't reopen" overlay after
    +// spawning the detached updater, before it quits to release the venv shim. The
    +// old 600ms was long enough to register the child process but far too short for
    +// the user to READ the overlay — the window just vanished, looked like a crash,
    +// and the user relaunched mid-update (the #50238 restart-loop trigger). A
    +// couple of seconds lets the message land and bridges the gap until the
    +// updater's own progress window appears. (#50419)
    +const UPDATE_HANDOFF_DWELL_MS = 2500
     
     // Block until no live update is in progress (or we hit the wait timeout).
     // Emits a boot-progress phase so the renderer shows "Update in progress…"
    ```

    ```diff
    @@ -1867,7 +1875,11 @@ async function applyUpdates(opts = {}) {
           return { ok: true, manual: true, command, hermesRoot: updateRoot }
         }
     
    -    emitUpdateProgress({ stage: 'restart', message: 'Handing off to the Hermes updater…', percent: 100 })
    +    emitUpdateProgress({
    +      stage: 'restart',
    +      message: 'Updating Hermes — this window will close and the updater will open. Don’t reopen Hermes yourself; it restarts automatically when the update finishes.',
    +      percent: 100
    +    })
         repairMacUpdaterHelper(updater)
     
         const updateRoot = resolveUpdateRoot()
    ```

    ```diff
    @@ -1903,11 +1915,14 @@ async function applyUpdates(opts = {}) {
     
         rememberLog(`[updates] launched updater: ${updater} ${updaterArgs.join(' ')}; exiting desktop to release venv shim`)
     
    -    // Give the OS a beat to register the new process, then quit. The updater
    -    // rebuilds and relaunches us when it's done.
    +    // Linger on the "updating — don't reopen" overlay long enough for the user
    +    // to actually read it (and to bridge the gap until the updater's own window
    +    // appears), THEN quit to release the venv shim. The updater rebuilds and
    +    // relaunches us when it's done. (#50419 — a 600ms quit looked like a crash
    +    // and lured users into the #50238 relaunch loop.)
         setTimeout(() => {
           app.quit()
    -    }, 600)
    +    }, UPDATE_HANDOFF_DWELL_MS)
     
         return { ok: true, handedOff: true, updater }
       } finally {
    ```

    ```diff
    @@ -1946,9 +1961,12 @@ async function handOffWindowsBootstrapRecovery(reason) {
       child.unref()
     
       rememberLog(`[bootstrap] handed off ${reason} recovery to updater: ${updater} ${updaterArgs.join(' ')}; exiting desktop to release app.asar`)
    +  // Same dwell as the in-app update hand-off (#50419): give the updater's
    +  // window time to appear before we vanish, so the recovery doesn't look like
    +  // a crash and provoke a mid-recovery relaunch.
       setTimeout(() => {
         app.quit()
    -  }, 600)
    +  }, UPDATE_HANDOFF_DWELL_MS)
     
       return true
     }
    ```
  - `apps/desktop/src/i18n/en.ts`

    ```diff
    @@ -1382,10 +1382,11 @@ export const en: Translations = {
         copy: 'Copy',
         copied: 'Copied',
         done: 'Done',
    -    applyingBody: 'The Hermes updater will take over in its own window and reopen Hermes when it’s done.',
    +    applyingBody:
    +      'The Hermes updater takes over in its own window and reopens Hermes automatically when it’s done. Please don’t reopen Hermes yourself while it’s updating.',
         applyingBodyBackend:
           'The remote backend is applying the update and will restart. Hermes reconnects automatically when it’s back.',
    -    applyingClose: 'Hermes will close to apply the update.',
    +    applyingClose: 'This window will close while the update runs, then Hermes reopens on its own.',
         errorTitle: 'Update didn’t finish',
         errorBody: 'No worries — nothing was lost. You can try again now.',
         notNow: 'Not now',
    ```
  - `apps/desktop/src/i18n/ja.ts`

    ```diff
    @@ -1512,9 +1512,9 @@ export const ja = defineLocale({
         copy: 'コピー',
         copied: 'コピーしました',
         done: '完了',
    -    applyingBody: 'Hermes アップデーターが独自のウィンドウで引き継ぎ、完了後に Hermes を再度開きます。',
    +    applyingBody: 'Hermes アップデーターが独自のウィンドウで引き継ぎ、完了後に自動的に Hermes を再度開きます。更新中はご自分で Hermes を開き直さないでください。',
         applyingBodyBackend: 'リモートバックエンドが更新を適用して再起動します。復帰すると Hermes が自動的に再接続します。',
    -    applyingClose: 'Hermes は更新を適用するために閉じます。',
    +    applyingClose: 'このウィンドウは更新中に閉じ、その後 Hermes が自動的に再度開きます。',
         errorTitle: '更新が完了しませんでした',
         errorBody: 'ご安心ください。何も失われていません。今すぐ再試行できます。',
         notNow: '今は後で',
    ```
  - `apps/desktop/src/i18n/zh-hant.ts`

    ```diff
    @@ -1463,9 +1463,9 @@ export const zhHant = defineLocale({
         copy: '複製',
         copied: '已複製',
         done: '完成',
    -    applyingBody: 'Hermes 更新程式會在自己的視窗中接管，並在完成後重新開啟 Hermes。',
    +    applyingBody: 'Hermes 更新程式會在自己的視窗中接管，並在完成後自動重新開啟 Hermes。更新期間請勿自行重新開啟 Hermes。',
         applyingBodyBackend: '遠端後端正在套用更新並將重新啟動。恢復後 Hermes 會自動重新連線。',
    -    applyingClose: 'Hermes 將關閉以套用更新。',
    +    applyingClose: '此視窗會在更新期間關閉，隨後 Hermes 會自動重新開啟。',
         errorTitle: '更新未完成',
         errorBody: '沒有資料遺失。您可以現在重試。',
         notNow: '暫不',
    ```
  - `apps/desktop/src/i18n/zh.ts`

    ```diff
    @@ -1568,9 +1568,9 @@ export const zh: Translations = {
         copy: '复制',
         copied: '已复制',
         done: '完成',
    -    applyingBody: 'Hermes 更新器会在自己的窗口中接管，并在完成后重新打开 Hermes。',
    +    applyingBody: 'Hermes 更新器会在自己的窗口中接管，并在完成后自动重新打开 Hermes。更新期间请不要自行重新打开 Hermes。',
         applyingBodyBackend: '远程后端正在应用更新并将重启。恢复后 Hermes 会自动重新连接。',
    -    applyingClose: 'Hermes 将关闭以应用更新。',
    +    applyingClose: '此窗口会在更新期间关闭，随后 Hermes 会自动重新打开。',
         errorTitle: '更新未完成',
         errorBody: '没有数据丢失。你可以现在重试。',
         notNow: '暂不',
    ```
- **#86675: fix(desktop): confirm detached updater spawn before quitting for hand-off (#66753)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `apps/desktop/electron/main.ts`, `apps/desktop/electron/updater-process.test.ts`, `apps/desktop/electron/updater-process.ts`
- **#57763: fix: macOS in-app update silent failure handling (#57645)**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - files: `apps/desktop/electron/main.ts`, `apps/desktop/electron/preload.ts`, `apps/desktop/electron/update-handoff-marker.test.ts`, `apps/desktop/electron/update-handoff-marker.ts`, `apps/desktop/src/global.d.ts`, `apps/desktop/src/i18n/en.ts`, `apps/desktop/src/i18n/ja.ts`, `apps/desktop/src/i18n/types.ts`, `apps/desktop/src/i18n/zh-hant.ts`, `apps/desktop/src/i18n/zh.ts`, `apps/desktop/src/store/updates.test.ts`, `apps/desktop/src/store/updates.ts`

## Fix status

- Merged to main in PR #50448 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-08-15`.
- Subsystem: `apps/desktop`
