diff --git a/hermes_cli/main.py b/hermes_cli/main.py index b1047c44c3..59b19684e1 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -8060,11 +8060,20 @@ def _run_install_with_heartbeat( t = threading.Thread(target=_heartbeat, daemon=True) t.start() try: + # stderr=STDOUT: uv/pip write progress to stderr. The desktop + # hand-off (scripts/desktop-update.ps1) only drains the child's + # stdout while the child runs, so a full stderr pipe (~64KB) + # blocks the installer forever — run 31449642122 hung 65 minutes + # inside this call. Merged into stdout, the output rides the pipe + # that IS drained. The fix lives here (not only in the hand-off + # script) because old installed bases run their OLD copy of the + # hand-off, but import THIS file fresh after the git reset. subprocess.run( cmd, cwd=PROJECT_ROOT, check=True, env=env, + stderr=subprocess.STDOUT, ) finally: done.set() diff --git a/hermes_cli/managed_uv.py b/hermes_cli/managed_uv.py index 8c42cdcb41..db9563ab39 100644 --- a/hermes_cli/managed_uv.py +++ b/hermes_cli/managed_uv.py @@ -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: diff --git a/scripts/desktop-update.ps1 b/scripts/desktop-update.ps1 index d45484501d..fdcb3a426d 100644 --- a/scripts/desktop-update.ps1 +++ b/scripts/desktop-update.ps1 @@ -268,8 +268,13 @@ function Invoke-StreamedHermes([string]$Exe, [string[]]$HermesArgs, [string]$Tag $proc = [System.Diagnostics.Process]::Start($psi) $outWriter = [System.IO.File]::CreateText($outFile) $errWriter = [System.IO.File]::CreateText($errFile) - # Pump synchronously in small reads so the UI stays alive; stderr is - # drained at the end (hermes update is stdout-dominant). + # Drain stderr from the START, asynchronously. uv writes its progress + # to stderr; a redirected pipe nobody reads fills at ~64KB and blocks + # the child mid-update (E2E run 31453853006 deadlocked 43 minutes in + # `uv sync` exactly this way). ReadToEndAsync keeps the pipe empty + # while the loop below pumps stdout. + $errTask = $proc.StandardError.ReadToEndAsync() + # Pump stdout synchronously in small reads so the UI stays alive. while (-not $proc.HasExited) { while (-not $proc.StandardOutput.EndOfStream) { $ln = $proc.StandardOutput.ReadLine() @@ -289,7 +294,7 @@ function Invoke-StreamedHermes([string]$Exe, [string[]]$HermesArgs, [string]$Tag if ($ln.Trim()) { Write-HandoffLog ("{0}| {1}" -f $Tag, $ln) } } } - $errText = $proc.StandardError.ReadToEnd() + $errText = $errTask.GetAwaiter().GetResult() if ($errText) { $errWriter.Write($errText) foreach ($ln in ($errText -split "`r?`n")) {