5bfb7ee42f
`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>