From 14fee4f11265728c427b5afb1942179ba6aba7f7 Mon Sep 17 00:00:00 2001 From: brooklyn! Date: Fri, 5 Jun 2026 08:37:16 -0500 Subject: [PATCH] fix(update/windows): retry handoff `hermes update` once on first-run crash (#39831) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The in-app updater (Hermes-Setup --update) runs `hermes update`, which lazily imports the freshly-pulled modules — but the dependency-install step runs the already-in-memory PRE-pull code for one invocation. When a release changes an updater-path contract across that boundary, the FIRST update on the parked population crashes even though the fix is already on disk. Concretely this is #39780's `_UvResult`: its `__iter__` yields (path, bool), so Windows `subprocess.list2cmdline([uv_bin, "pip", ...])` injects the bool and dies with `TypeError: sequence item 1: expected str instance, bool found` (fixed in #39820). A parked Windows user clicking Update pulls #39820 to disk, then still crashes on the in-memory pre-merge module; only the SECOND click runs clean. Field repro: ryanc's bootstrap.log (2026-06-05 12:41:41). Fix: when the first `hermes update` exits non-zero (and it isn't the concurrent-instance guard, exit 2, which a retry can't fix), retry once automatically. The retry loads the now-current module from the start and succeeds — so the parked user gets a working one-click update instead of a scary crash + manual second attempt. Verified: cargo check clean. --- .../src-tauri/src/update.rs | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/apps/bootstrap-installer/src-tauri/src/update.rs b/apps/bootstrap-installer/src-tauri/src/update.rs index bd0595af2f8..658bff6c540 100644 --- a/apps/bootstrap-installer/src-tauri/src/update.rs +++ b/apps/bootstrap-installer/src-tauri/src/update.rs @@ -183,7 +183,7 @@ async fn run_update(app: AppHandle) -> Result<()> { emit_stage(&app, "update", StageState::Running, None, None); let started = Instant::now(); - let update = run_streamed( + let mut update = run_streamed( &app, &hermes, &update_args, @@ -192,6 +192,38 @@ async fn run_update(app: AppHandle) -> Result<()> { Some("update"), ) .await?; + + // Retry-once for the update-boundary crash. `hermes update` lazily imports + // the FRESHLY PULLED modules, but the dependency-install step still runs the + // already-in-memory pre-pull code for one invocation. A release that changed + // an updater-path contract across that boundary (e.g. #39780's `_UvResult`, + // whose `__iter__` injected a bool into the argv and crashed Windows + // `list2cmdline` with `TypeError: sequence item 1: expected str instance, + // bool found`, fixed in #39820) therefore kills the FIRST update on the + // parked population — even though the fix is already on disk by then. A + // second `hermes update` runs clean because the now-current module is loaded + // from the start. Rather than make the parked user click Update twice (and + // stare at a scary crash first), retry once automatically. Skip the retry + // for the concurrent-instance guard (exit 2) — that's a "close Hermes" state + // a retry can't fix. + if !matches!(update.exit_code, Some(0) | Some(UPDATE_EXIT_CONCURRENT)) { + emit_log( + &app, + Some("update"), + LogStream::Stdout, + "[update] first update attempt failed; retrying once (the fix it just \ + pulled loads on the second run)…", + ); + update = run_streamed( + &app, + &hermes, + &update_args, + &install_root, + &child_env, + Some("update"), + ) + .await?; + } let update_ms = started.elapsed().as_millis() as u64; match update.exit_code {