fix(banner): show correct update status on nix-built hermes (#17550)

check_for_updates() looked at __file__.parent.parent for a .git dir to
  diff against origin/main. A nix-built hermes lives in /nix/store with
  no .git there, so the check fell through to whatever editable-install
  dev checkout last populated ~/.hermes/.update_check, producing stale
  "X commits behind" warnings right after a fresh `nix run --refresh`.

  Embed the locked flake rev into the wrapper as HERMES_REVISION (only
on
  clean builds — dirty refs don't represent any upstream commit). When
  set, banner.py compares it to upstream main via `git ls-remote`
instead
  of inspecting a local checkout, and the cache key includes the rev so
  nix updates invalidate immediately. Without local history we can't
  count commits, so the message is a plain "update available" with no
  suggested command — nix users may install via `nix run`, profile,
  system flake, or home-manager, and we don't know which.

  Also bump web/package-lock.json npmDepsHash via `nix run
.#fix-lockfiles`.
This commit is contained in:
ethernet
2026-04-29 21:33:00 -04:00
committed by GitHub
parent fc7f55f490
commit 9fc9c15b4a
6 changed files with 97 additions and 62 deletions

View File

@@ -5,6 +5,7 @@ Pure display functions with no HermesCLI state dependency.
import json
import logging
import os
import shutil
import subprocess
import threading
@@ -122,35 +123,36 @@ def get_available_skills() -> Dict[str, List[str]]:
# Cache update check results for 6 hours to avoid repeated git fetches
_UPDATE_CHECK_CACHE_SECONDS = 6 * 3600
# Sentinel returned when we know an update exists but can't count commits
# (e.g. nix-built hermes — no local git history to count against).
UPDATE_AVAILABLE_NO_COUNT = -1
def check_for_updates() -> Optional[int]:
"""Check how many commits behind origin/main the local repo is.
_UPSTREAM_REPO_URL = "https://github.com/NousResearch/hermes-agent.git"
Does a ``git fetch`` at most once every 6 hours (cached to
``~/.hermes/.update_check``). Returns the number of commits behind,
or ``None`` if the check fails or isn't applicable.
def _check_via_rev(local_rev: str) -> Optional[int]:
"""Compare an embedded git revision to upstream main via ls-remote.
Returns 0 if up-to-date, ``UPDATE_AVAILABLE_NO_COUNT`` if behind,
or ``None`` on failure.
"""
hermes_home = get_hermes_home()
repo_dir = hermes_home / "hermes-agent"
cache_file = hermes_home / ".update_check"
# Must be a git repo — fall back to project root for dev installs
if not (repo_dir / ".git").exists():
repo_dir = Path(__file__).parent.parent.resolve()
if not (repo_dir / ".git").exists():
return None
# Read cache
now = time.time()
try:
if cache_file.exists():
cached = json.loads(cache_file.read_text())
if now - cached.get("ts", 0) < _UPDATE_CHECK_CACHE_SECONDS:
return cached.get("behind")
result = subprocess.run(
["git", "ls-remote", _UPSTREAM_REPO_URL, "refs/heads/main"],
capture_output=True, text=True, timeout=10,
)
except Exception:
pass
return None
if result.returncode != 0 or not result.stdout:
return None
upstream_rev = result.stdout.split()[0]
if not upstream_rev:
return None
return 0 if upstream_rev == local_rev else UPDATE_AVAILABLE_NO_COUNT
# Fetch latest refs (fast — only downloads ref metadata, no files)
def _check_via_local_git(repo_dir: Path) -> Optional[int]:
"""Count commits behind origin/main in a local checkout."""
try:
subprocess.run(
["git", "fetch", "origin", "--quiet"],
@@ -160,7 +162,6 @@ def check_for_updates() -> Optional[int]:
except Exception:
pass # Offline or timeout — use stale refs, that's fine
# Count commits behind
try:
result = subprocess.run(
["git", "rev-list", "--count", "HEAD..origin/main"],
@@ -168,15 +169,52 @@ def check_for_updates() -> Optional[int]:
cwd=str(repo_dir),
)
if result.returncode == 0:
behind = int(result.stdout.strip())
else:
behind = None
return int(result.stdout.strip())
except Exception:
behind = None
pass
return None
# Write cache
def check_for_updates() -> Optional[int]:
"""Check whether a Hermes update is available.
Two paths: if ``HERMES_REVISION`` is set (nix builds embed it), compare
it to upstream main via ``git ls-remote``. Otherwise look for a local
git checkout and count commits behind ``origin/main``.
Returns the number of commits behind, ``UPDATE_AVAILABLE_NO_COUNT`` (-1)
if behind but the count is unknown, ``0`` if up-to-date, or ``None`` if
the check failed or doesn't apply. Cached for 6 hours.
"""
hermes_home = get_hermes_home()
cache_file = hermes_home / ".update_check"
embedded_rev = os.environ.get("HERMES_REVISION") or None
# Read cache — invalidate if the embedded rev has changed since last check
now = time.time()
try:
cache_file.write_text(json.dumps({"ts": now, "behind": behind}))
if cache_file.exists():
cached = json.loads(cache_file.read_text())
if (
now - cached.get("ts", 0) < _UPDATE_CHECK_CACHE_SECONDS
and cached.get("rev") == embedded_rev
):
return cached.get("behind")
except Exception:
pass
if embedded_rev:
behind = _check_via_rev(embedded_rev)
else:
repo_dir = hermes_home / "hermes-agent"
if not (repo_dir / ".git").exists():
repo_dir = Path(__file__).parent.parent.resolve()
if not (repo_dir / ".git").exists():
return None
behind = _check_via_local_git(repo_dir)
try:
cache_file.write_text(json.dumps({"ts": now, "behind": behind, "rev": embedded_rev}))
except Exception:
pass
@@ -549,13 +587,23 @@ def build_welcome_banner(console: Console, model: str, cwd: str,
# Update check — use prefetched result if available
try:
behind = get_update_result(timeout=0.5)
if behind and behind > 0:
from hermes_cli.config import recommended_update_command
commits_word = "commit" if behind == 1 else "commits"
right_lines.append(
f"[bold yellow]⚠ {behind} {commits_word} behind[/]"
f"[dim yellow] — run [bold]{recommended_update_command()}[/bold] to update[/]"
)
if behind is not None and behind != 0:
from hermes_cli.config import get_managed_update_command, recommended_update_command
if behind > 0:
commits_word = "commit" if behind == 1 else "commits"
right_lines.append(
f"[bold yellow]{behind} {commits_word} behind[/]"
f"[dim yellow] — run [bold]{recommended_update_command()}[/bold] to update[/]"
)
else:
# UPDATE_AVAILABLE_NO_COUNT: nix-built hermes; we know an update
# exists but not by how much, and we don't know how the user
# installed it (nix run, profile, system flake, home-manager).
managed_cmd = get_managed_update_command()
line = "[bold yellow]⚠ update available[/]"
if managed_cmd:
line += f"[dim yellow] — run [bold]{managed_cmd}[/bold][/]"
right_lines.append(line)
except Exception:
pass # Never break the banner over an update check