`uv pip install -e .` has to replace the console-script shims, so
_quarantine_running_hermes_exe must first rename the running hermes.exe out
of the way. That rename fails whenever any child process spawned from that
hermes.exe is still alive: on Windows a child inherits a handle on the parent
image. It is the inherited handle, not the trampoline, that pins the file --
killing the child makes the identical rename succeed, and the shim flavour
(uv trampoline vs distlib launcher) makes no difference.
The updater spawns such children itself (npx cache warm, memory-provider
refresh -- hindsight-api runs as a daemon with --idle-timeout 300 and outlives
the step that started it), so this presents as a race rather than a hard
failure: the same hand-off succeeds on one run and dies on the next. Step 2's
shim-unlock preflight cannot catch it, because the shim genuinely is unlocked
at that moment; the pinning child appears later, during the update.
When the rename loses that race, _schedule_replace_on_reboot is the last
resort -- and MOVEFILE_DELAY_UNTIL_REBOOT writes to HKLM, so it needs
elevation. A Desktop-driven update is not elevated, so it returns
ERROR_ACCESS_DENIED, `uv pip install -e .` exits 2, and the ZIP fallback
repeats the identical sequence. The desktop build stage is then never reached
while the pre-build clean has already removed apps/desktop/release, leaving an
install whose Start Menu shortcut points at a Hermes.exe that no longer exists.
Running the same code as `python.exe -m hermes_cli.main update` puts the
inherited handles on python.exe, which uv never has to replace.
posix.sh is deliberately untouched: unlinking a running executable is legal
there, so the equivalent call is harmless.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Round 4 of helix4u's review — the durable fallback is now real:
- Result protocol gains `manual`: an ok result the user still must act
on (reopen the app, reinstall the GUI package, fix the sandbox helper).
Both orchestrators set it on every DONE_NOTE/downgrade path; the Desktop
consumer surfaces manual results in a real dialog on next boot instead
of a log line — the browserless-Linux disappearance now ends at a
visible dialog, worst case one boot later. Older result files without
the field parse as manual:false (covered).
- notify ladder verifies EXECUTION, not existence: zenity/kdialog must
survive their first second (an instant death means no display and falls
through); the no-surface case is an explicit best-effort contract whose
guaranteed channel is the result dialog.
- mac DONE_NOTE + failed relaunch of the kept/rolled-back bundle is no
longer swallowed (`|| true` dropped): the durable message carries both
facts.
- launch/gate matrices assert `manual` in the result JSON; consumer
round-trip tested in handoff-result.test.ts.
gille's round-2 review: the terminal lifecycle claimed outcomes the
launch hadn't delivered yet.
- posix finish() reorders: outcome -> durable result+marker -> LAUNCH
WITH ACCEPTANCE -> terminal event. mac acceptance is open's exit code
(launchd rejects broken bundles loudly); linux verifies the setsid
child is still alive 1.5s after spawn, so an instant exec failure
downgrades to a held 'manual' state + truthful result instead of a
vanished 'done'. Gated skew/manual outcomes publish a real 'manual'
event (new third shim state -- still zero logic in the page).
- Renderer-free linux recovery: when no chromium-family browser exists,
manual/error outcomes fire notify-send/zenity/kdialog best-effort so a
gated non-relaunch is never a silent disappearance.
- windows.ps1 mirrors the contract: Start-DesktopRelaunch returns
verified acceptance (WMI pid alive / fallback process alive; dying
before the window appears counts as failure), and the finally block
downgrades to Show-ManualFinale + rewritten result when the launch
didn't land. Error path still relaunches after showing itself.
- repro.sh launch / npm run update:repro:launch: real-orchestrator
matrix for instant-exit relaunch downgrade and skew-message surfacing.
- posix.sh cds into the install root before hermes update (found by the
sandboxed behind-repro: parts of the update resolve the mutated tree
from cwd, which is the Desktop's cwd -- it updated the DEVELOPER'S
checkout while reporting success against the sandbox).
scripts/desktop-update.ps1 moves to scripts/desktop-update/windows.ps1 (a
compat forwarder stays at the old path for one asar/checkout skew cycle)
and gains the shim: scripts/desktop-update/ui.html rendered in a
chromeless Edge app window, fed done|error over a loopback /progress
endpoint. The page is #75895's hand-off screen ported verbatim (Fourier
Flow loader, one title, one line, OS light/dark, charcoal dark seeds);
failure is the terse card pointing at hermes debug share. The WinForms
card stays as the no-Edge fallback, same shape.
Salvaged from the web-shell spike: TcpListener runspace server, Edge
--app spawn with throwaway profile, degradation ladder, -SelfTestUi.
Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>