fix(update): drain uv/pip stderr - undrained pipe deadlocked windows desktop updates

Two consecutive Windows E2E runs hung inside dependency install until
the job timeout: 31449642122 65 minutes in uv pip install .[all],
31453853006 43 minutes in the SQLite-repair uv sync (RUST_LOG=uv=debug
made THAT run hang earlier and with more stderr - the tell).

Root cause: scripts/desktop-update.ps1 redirects both child pipes but
only pumps stdout while the child runs; stderr is ReadToEnd()'d after
exit. uv and pip write progress to stderr. Once that pipe hits the
~64KB buffer, uv blocks on write, hermes update blocks on uv, the
hand-off blocks on hermes update: deadlock. Slower stderr producers
survive by finishing before the buffer fills, which is why the linux
sandbox never sees this.

Fix both sides of the class:
- managed_uv.py candidate sync + main.py _run_install_with_heartbeat:
  merge stderr into stdout (the pipe that IS drained). This arm heals
  EXISTING installs, whose old hand-off script drives the NEW python
  after the git reset.
- desktop-update.ps1: drain stderr concurrently via ReadToEndAsync so
  future bases never block regardless of what a child writes there.

_run_logged_subprocess and _run_npm_install_deterministic already
merge or capture both pipes; the two fixed sites were the only update-
path spawns that redirect stderr without draining it live.
This commit is contained in:
ethernet
2026-08-11 00:03:47 -04:00
parent bdb1b53c08
commit e3d22b5b29
3 changed files with 26 additions and 3 deletions
+9
View File
@@ -780,6 +780,14 @@ def _stage_candidate_venv(
# UV_NO_CONFIG drops it and uv 0.12+ refuses --locked.
sync_env = dict(env)
sync_env.pop("UV_NO_CONFIG", None)
# stderr=STDOUT: uv writes progress to stderr. When the desktop
# hand-off (scripts/desktop-update.ps1) runs `hermes update`, it only
# drains the child's stdout while the child runs; a full stderr pipe
# (~64KB) blocks uv forever. Run 31453853006 hung 43 minutes in this
# exact call. Merging into stdout keeps the output streaming through
# the pipe that IS drained. Old installed bases run their old copy of
# the hand-off script, so the fix must live here, on the Python side
# the update refreshes before dependencies are installed.
synced = subprocess.run(
[
uv_bin,
@@ -792,6 +800,7 @@ def _stage_candidate_venv(
],
cwd=project_root,
env=sync_env,
stderr=subprocess.STDOUT,
check=False,
)
if synced.returncode != 0: