Desktop auto-update process conflicts and stuck UI fixes
NousResearch/hermes-agent · cluster 37532 · apps/desktop
Data as of
Nothing open on GitHub. Fix merged in #38158. Check it covers your case.
The short answer
- Known?
- Yes. 8 related items grouped here, 3 issues already closed.
- Fix event
- A fix was merged to main in PR #38158. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
partially resolved, deciding PRs #38158, #52203. That is separate from the fix event. - Still open
- Nothing open on GitHub.
These issues and PRs concern bugs where the desktop app's update mechanism kills its own backend processes or gets stuck, along with related fixes and refactoring of the update flow and WebSocket layer.
Open issues0
Open PRs0
Fixes on record
-
#38158 fix(desktop): stop in-app update from killing its own backend (#37532)
files:
apps/desktop/electron/main.cjs,hermes_cli/main.py,tests/hermes_cli/test_update_stale_dashboard.pyShow the diff
-
apps/desktop/electron/main.cjs@@ -1412,6 +1412,18 @@ async function applyUpdatesPosixInApp(opts = {}) { PATH: [extraPath, process.env.PATH].filter(Boolean).join(path.delimiter) } + // `hermes update` reaps stale `hermes dashboard` backends (a code update + // leaves the running process serving old Python against the freshly-updated + // JS bundle). But OUR backend is one of those processes, and killing it + // mid-update produces the boot→kill→crash loop in #37532 — the desktop + // already restarts its own backend via the rebuild+relaunch below, so the + // reap must spare it. Hand the live backend's PID to the update process; + // _kill_stale_dashboard_processes reads HERMES_DESKTOP_CHILD_PID and excludes + // it while still reaping any genuinely-orphaned dashboards. (#37532) + if (hermesProcess && Number.isInteger(hermesProcess.pid)) { + env.HERMES_DESKTOP_CHILD_PID = String(hermesProcess.pid) + } + // Branch-pin so a non-main checkout doesn't get switched to main (and self-heal // to main when the pinned branch no longer exists on origin). let branchArgs = [] -
hermes_cli/main.py@@ -7363,7 +7363,10 @@ def cmd_gui(args: argparse.Namespace): sys.exit(launch_result.returncode) -def _find_stale_dashboard_pids() -> list[int]: +def _find_stale_dashboard_pids( + *, + exclude_pids: set[int] | None = None, +) -> list[int]: """Return PIDs of ``hermes dashboard`` processes other than ourselves. ``hermes dashboard`` is a long-lived server process commonly started and @@ -7378,6 +7381,15 @@ def _find_stale_dashboard_pids() -> list[int]: it. This helper is just the detection step; see ``_kill_stale_dashboard_processes`` for the kill. + *exclude_pids* is an optional set of PIDs that must never be returned. + This is used by the Hermes Desktop Electron app to protect its own + backend child process: when the desktop spawns ``hermes dashboard`` as + a backend and triggers an auto-update, the update must not kill the + dashboard that the desktop itself manages. The desktop sets the + environment variable ``HERMES_DESKTOP_CHILD_PID`` on the spawned + backend process; ``_kill_stale_dashboard_processes`` reads it and + passes it here. (#37532) + Returns an empty list on any scan error (missing ps/wmic, timeout, etc.). """ patterns = [ @@ -7453,6 +7465,8 @@ def _find_stale_dashboard_pids() -> list[int]: except (FileNotFoundError, subprocess.TimeoutExpired, OSError): return [] + if exclude_pids: + dashboard_pids = [p for p in dashboard_pids if p not in exclude_pids] return dashboard_pids @@ -7604,7 +7618,18 @@ def _kill_stale_dashboard_processes( launch args (--host, --port, --insecure, --tui, --no-open). The user restarts it manually; a hint is printed. """ - pids = _find_stale_dashboard_pids() + # When the Hermes Desktop Electron app spawns this dashboard as a + # backend child, it sets HERMES_DESKTOP_CHILD_PID so that the update + # path can skip killing the desktop-managed process. (#37532) + exclude: set[int] | None = None + raw_pid = os.environ.get("HERMES_DESKTOP_CHILD_PID") + if raw_pid: + try: + exclude = {int(raw_pid)} + except (ValueError, TypeError): + pass + + pids = _find_stale_dashboard_pids(exclude_pids=exclude) if not pids: return -
tests/hermes_cli/test_update_stale_dashboard.py@@ -185,6 +185,48 @@ def test_invalid_pid_lines_skipped(self): pids = _find_stale_dashboard_pids() assert pids == [12345] + def test_exclude_pids_filters_specified_pids(self): + """exclude_pids removes specific PIDs from the result — used by + the Desktop Electron app to protect its own backend child. (#37532) + """ + with patch("subprocess.run") as mock_run: + mock_run.return_value = MagicMock( + returncode=0, + stdout="\n".join([ + _ps_line(11111, "hermes dashboard --port 9119"), + _ps_line(22222, "hermes dashboard --port 9120"), + _ps_line(33333, "hermes dashboard --port 9121"), + ]) + "\n", + stderr="", + ) + # Exclude the desktop-managed backend PID + pids = _find_stale_dashboard_pids(exclude_pids={22222}) + assert 11111 in pids + assert 22222 not in pids + assert 33333 in pids + + def test_exclude_pids_none_is_noop(self): + """Passing exclude_pids=None (the default) changes nothing.""" + with patch("subprocess.run") as mock_run: + mock_run.return_value = MagicMock( + returncode=0, + stdout=_ps_line(12345, "hermes dashboard --port 9119") + "\n", + stderr="", + ) + pids = _find_stale_dashboard_pids(exclude_pids=None) + assert pids == [12345] + + def test_exclude_all_pids_returns_empty(self): + """If all matched PIDs are excluded, the result is empty.""" + with patch("subprocess.run") as mock_run: + mock_run.return_value = MagicMock( + returncode=0, + stdout=_ps_line(12345, "hermes dashboard --port 9119") + "\n", + stderr="", + ) + pids = _find_stale_dashboard_pids(exclude_pids={12345}) + assert pids == [] + @pytest.mark.skipif(sys.platform == "win32", reason="POSIX kill semantics") class TestKillStaleDashboardPosix:
-
-
#52203 fix(update): announce gateway drain waits so desktop updates don't look hung
files:
hermes_cli/gateway.py,hermes_cli/main.py,tests/hermes_cli/test_gateway_service.py
3 PRs closed without merging
From the close comments: merged via another change 1 · other 2
-
#37538 fix(cli): exclude desktop-managed backend from stale-dashboard kill · merged via another change →
5fca754,#38158 -
#37834 fix(desktop): treat 'done' as terminal update stage to unstick UI on Linux (#37775) · other →
#45205,#37834 -
#44540 fix(update): announce gateway drain waits so desktop updates don't look hung · other →
#44061
Closed issues · 3
- Closed#37532bug: desktop auto-update kills its own backend via _kill_stale_dashboard_processes
- Closed#37775Desktop app update gets stuck forever; terminal shows hermes:api ECONNREFUSED 127.0.0.1:9120 on Fedora Wayland
- Closed#44515[Bug]: Hermes Agent Desktop doesn't update unless hermes background processes are stopped
How this cluster was grouped
-
Link evidence (
partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text. - Members outside that structural core may belong to a different underlying problem.
-
Pairwise link density
0.20: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the Lab