mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-16 04:22:36 +00:00
fix(update): add heartbeat during dependency install
This commit is contained in:
parent
04193cf71c
commit
54c0b10d14
2 changed files with 74 additions and 19 deletions
|
|
@ -230,6 +230,7 @@ except Exception:
|
||||||
pass # best-effort — don't crash if config isn't available yet
|
pass # best-effort — don't crash if config isn't available yet
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import threading
|
||||||
import time as _time
|
import time as _time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
@ -6445,6 +6446,45 @@ def _load_installable_optional_extras() -> list[str]:
|
||||||
return referenced
|
return referenced
|
||||||
|
|
||||||
|
|
||||||
|
def _run_install_with_heartbeat(
|
||||||
|
cmd: list[str],
|
||||||
|
*,
|
||||||
|
env: dict[str, str] | None = None,
|
||||||
|
heartbeat_interval_seconds: int = 30,
|
||||||
|
) -> None:
|
||||||
|
"""Run dependency install command with periodic heartbeat output.
|
||||||
|
|
||||||
|
Some resolvers/build backends (especially when compiling Rust/C extensions)
|
||||||
|
can stay quiet for minutes. Emit a simple elapsed-time heartbeat so users
|
||||||
|
know ``hermes update`` is still progressing even if pip/uv itself is silent.
|
||||||
|
"""
|
||||||
|
done = threading.Event()
|
||||||
|
start = _time.time()
|
||||||
|
|
||||||
|
def _heartbeat() -> None:
|
||||||
|
# Wait first, then print, so short installs don't emit noise.
|
||||||
|
while not done.wait(heartbeat_interval_seconds):
|
||||||
|
elapsed = int(_time.time() - start)
|
||||||
|
print(
|
||||||
|
f" … still installing dependencies ({elapsed}s elapsed)"
|
||||||
|
" — compiling Rust/C extensions can take several minutes",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
t = threading.Thread(target=_heartbeat, daemon=True)
|
||||||
|
t.start()
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
cmd,
|
||||||
|
cwd=PROJECT_ROOT,
|
||||||
|
check=True,
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
done.set()
|
||||||
|
t.join(timeout=0.2)
|
||||||
|
|
||||||
|
|
||||||
def _install_python_dependencies_with_optional_fallback(
|
def _install_python_dependencies_with_optional_fallback(
|
||||||
install_cmd_prefix: list[str],
|
install_cmd_prefix: list[str],
|
||||||
*,
|
*,
|
||||||
|
|
@ -6461,12 +6501,13 @@ def _install_python_dependencies_with_optional_fallback(
|
||||||
Collecting/Building/Installing step), so keeping it visible costs
|
Collecting/Building/Installing step), so keeping it visible costs
|
||||||
nothing on fast hardware and prevents the "hermes update hangs" reports
|
nothing on fast hardware and prevents the "hermes update hangs" reports
|
||||||
on slow hardware.
|
on slow hardware.
|
||||||
|
|
||||||
|
We also add periodic heartbeat lines in case the resolver/build backend is
|
||||||
|
itself silent for long stretches.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
subprocess.run(
|
_run_install_with_heartbeat(
|
||||||
install_cmd_prefix + ["install", "-e", ".[all]"],
|
install_cmd_prefix + ["install", "-e", ".[all]"],
|
||||||
cwd=PROJECT_ROOT,
|
|
||||||
check=True,
|
|
||||||
env=env,
|
env=env,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
@ -6475,10 +6516,8 @@ def _install_python_dependencies_with_optional_fallback(
|
||||||
" ⚠ Optional extras failed, reinstalling base dependencies and retrying extras individually..."
|
" ⚠ Optional extras failed, reinstalling base dependencies and retrying extras individually..."
|
||||||
)
|
)
|
||||||
|
|
||||||
subprocess.run(
|
_run_install_with_heartbeat(
|
||||||
install_cmd_prefix + ["install", "-e", "."],
|
install_cmd_prefix + ["install", "-e", "."],
|
||||||
cwd=PROJECT_ROOT,
|
|
||||||
check=True,
|
|
||||||
env=env,
|
env=env,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -6486,10 +6525,8 @@ def _install_python_dependencies_with_optional_fallback(
|
||||||
installed_extras: list[str] = []
|
installed_extras: list[str] = []
|
||||||
for extra in _load_installable_optional_extras():
|
for extra in _load_installable_optional_extras():
|
||||||
try:
|
try:
|
||||||
subprocess.run(
|
_run_install_with_heartbeat(
|
||||||
install_cmd_prefix + ["install", "-e", f".[{extra}]"],
|
install_cmd_prefix + ["install", "-e", f".[{extra}]"],
|
||||||
cwd=PROJECT_ROOT,
|
|
||||||
check=True,
|
|
||||||
env=env,
|
env=env,
|
||||||
)
|
)
|
||||||
installed_extras.append(extra)
|
installed_extras.append(extra)
|
||||||
|
|
|
||||||
|
|
@ -323,15 +323,15 @@ def test_cmd_update_retries_optional_extras_individually_when_all_fails(monkeypa
|
||||||
return SimpleNamespace(stdout="main\n", stderr="", returncode=0)
|
return SimpleNamespace(stdout="main\n", stderr="", returncode=0)
|
||||||
if cmd == ["git", "rev-list", "HEAD..origin/main", "--count"]:
|
if cmd == ["git", "rev-list", "HEAD..origin/main", "--count"]:
|
||||||
return SimpleNamespace(stdout="1\n", stderr="", returncode=0)
|
return SimpleNamespace(stdout="1\n", stderr="", returncode=0)
|
||||||
if cmd == ["git", "pull", "origin", "main"]:
|
if cmd == ["git", "pull", "--ff-only", "origin", "main"]:
|
||||||
return SimpleNamespace(stdout="Updating\n", stderr="", returncode=0)
|
return SimpleNamespace(stdout="Updating\n", stderr="", returncode=0)
|
||||||
if cmd == ["/usr/bin/uv", "pip", "install", "-e", ".[all]", "--quiet"]:
|
if cmd == ["/usr/bin/uv", "pip", "install", "-e", ".[all]"]:
|
||||||
raise CalledProcessError(returncode=1, cmd=cmd)
|
raise CalledProcessError(returncode=1, cmd=cmd)
|
||||||
if cmd == ["/usr/bin/uv", "pip", "install", "-e", ".", "--quiet"]:
|
if cmd == ["/usr/bin/uv", "pip", "install", "-e", "."]:
|
||||||
return SimpleNamespace(returncode=0)
|
return SimpleNamespace(returncode=0)
|
||||||
if cmd == ["/usr/bin/uv", "pip", "install", "-e", ".[matrix]", "--quiet"]:
|
if cmd == ["/usr/bin/uv", "pip", "install", "-e", ".[matrix]"]:
|
||||||
raise CalledProcessError(returncode=1, cmd=cmd)
|
raise CalledProcessError(returncode=1, cmd=cmd)
|
||||||
if cmd == ["/usr/bin/uv", "pip", "install", "-e", ".[mcp]", "--quiet"]:
|
if cmd == ["/usr/bin/uv", "pip", "install", "-e", ".[mcp]"]:
|
||||||
return SimpleNamespace(returncode=0)
|
return SimpleNamespace(returncode=0)
|
||||||
# Catch-all must include stdout/stderr so consumers that parse
|
# Catch-all must include stdout/stderr so consumers that parse
|
||||||
# output (e.g. the dashboard-restart `ps -A` scan added in the
|
# output (e.g. the dashboard-restart `ps -A` scan added in the
|
||||||
|
|
@ -344,10 +344,10 @@ def test_cmd_update_retries_optional_extras_individually_when_all_fails(monkeypa
|
||||||
|
|
||||||
install_cmds = [c for c in recorded if "pip" in c and "install" in c]
|
install_cmds = [c for c in recorded if "pip" in c and "install" in c]
|
||||||
assert install_cmds == [
|
assert install_cmds == [
|
||||||
["/usr/bin/uv", "pip", "install", "-e", ".[all]", "--quiet"],
|
["/usr/bin/uv", "pip", "install", "-e", ".[all]"],
|
||||||
["/usr/bin/uv", "pip", "install", "-e", ".", "--quiet"],
|
["/usr/bin/uv", "pip", "install", "-e", "."],
|
||||||
["/usr/bin/uv", "pip", "install", "-e", ".[matrix]", "--quiet"],
|
["/usr/bin/uv", "pip", "install", "-e", ".[matrix]"],
|
||||||
["/usr/bin/uv", "pip", "install", "-e", ".[mcp]", "--quiet"],
|
["/usr/bin/uv", "pip", "install", "-e", ".[mcp]"],
|
||||||
]
|
]
|
||||||
|
|
||||||
out = capsys.readouterr().out
|
out = capsys.readouterr().out
|
||||||
|
|
@ -371,7 +371,7 @@ def test_cmd_update_succeeds_with_extras(monkeypatch, tmp_path):
|
||||||
return SimpleNamespace(stdout="main\n", stderr="", returncode=0)
|
return SimpleNamespace(stdout="main\n", stderr="", returncode=0)
|
||||||
if cmd == ["git", "rev-list", "HEAD..origin/main", "--count"]:
|
if cmd == ["git", "rev-list", "HEAD..origin/main", "--count"]:
|
||||||
return SimpleNamespace(stdout="1\n", stderr="", returncode=0)
|
return SimpleNamespace(stdout="1\n", stderr="", returncode=0)
|
||||||
if cmd == ["git", "pull", "origin", "main"]:
|
if cmd == ["git", "pull", "--ff-only", "origin", "main"]:
|
||||||
return SimpleNamespace(stdout="Updating\n", stderr="", returncode=0)
|
return SimpleNamespace(stdout="Updating\n", stderr="", returncode=0)
|
||||||
return SimpleNamespace(returncode=0, stdout="", stderr="")
|
return SimpleNamespace(returncode=0, stdout="", stderr="")
|
||||||
|
|
||||||
|
|
@ -384,6 +384,24 @@ def test_cmd_update_succeeds_with_extras(monkeypatch, tmp_path):
|
||||||
assert ".[all]" in install_cmds[0]
|
assert ".[all]" in install_cmds[0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_install_heartbeat_prints_when_dependency_install_is_silent(monkeypatch, capsys):
|
||||||
|
"""Long quiet installs should emit periodic heartbeat lines."""
|
||||||
|
|
||||||
|
def fake_run(cmd, **kwargs):
|
||||||
|
hermes_main._time.sleep(1.2)
|
||||||
|
return SimpleNamespace(returncode=0)
|
||||||
|
|
||||||
|
monkeypatch.setattr(hermes_main.subprocess, "run", fake_run)
|
||||||
|
|
||||||
|
hermes_main._run_install_with_heartbeat(
|
||||||
|
["uv", "pip", "install", "-e", "."],
|
||||||
|
heartbeat_interval_seconds=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "still installing dependencies" in out
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# ff-only fallback to reset --hard on diverged history
|
# ff-only fallback to reset --hard on diverged history
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue