mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 05:17:15 +00:00
fix(update): read update.log and receipts from the root home
The writers moved to the root home; the readers did not. `hermes logs update` / `handoff`, `hermes logs list`, the debug bundle, the dashboard's durable update-completion marker, the post-swap hand-off file and pm's sync receipts (same update_receipts dir) now use get_default_hermes_root(), so a sticky profile no longer hides the update log.
This commit is contained in:
1 parent
c035f2a614
commit
ba03113d96
8 files changed
+85
-18
No files matched your search
+2
-3
@@ -219,9 +219,8 @@ class LogSnapshot:
|
||||
|
||||
def _primary_log_path(log_name: str) -> Optional[Path]:
|
||||
"""Where *log_name* would live if present. Doesn't check existence."""
|
||||
from hermes_cli.logs import LOG_FILES
|
||||
filename = LOG_FILES.get(log_name)
|
||||
return (get_hermes_home() / "logs" / filename) if filename else None
|
||||
from hermes_cli.logs import log_file_path
|
||||
return log_file_path(log_name) # update/handoff: the ROOT home's, whatever profile is active
|
||||
|
||||
|
||||
# Logs written by a client process, invisible to a remote/docker/SSH backend running `debug
|
||||
|
||||
+24
-6
@@ -28,7 +28,7 @@ from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Optional, Sequence
|
||||
|
||||
from hermes_constants import get_hermes_home, display_hermes_home
|
||||
from hermes_constants import display_hermes_home, get_default_hermes_root, get_hermes_home
|
||||
|
||||
# Known log files (name → filename)
|
||||
LOG_FILES = {
|
||||
@@ -53,6 +53,20 @@ LOG_FILES = {
|
||||
"mcp": "mcp-stderr.log",
|
||||
}
|
||||
|
||||
# Written to the ROOT home whatever profile is active: ``hermes update`` mutates the checkout every
|
||||
# profile shares, and its mirror (main_dashboard) + the Desktop hand-off scripts write <root>/logs.
|
||||
ROOT_HOME_LOGS = frozenset({"update", "handoff"})
|
||||
|
||||
|
||||
def log_file_path(log_name: str) -> Optional[Path]:
|
||||
"""Where *log_name* lives (doesn't check existence); None for an unknown name."""
|
||||
filename = LOG_FILES.get(log_name)
|
||||
if filename is None:
|
||||
return None
|
||||
home = get_default_hermes_root() if log_name in ROOT_HOME_LOGS else get_hermes_home()
|
||||
return home / "logs" / filename
|
||||
|
||||
|
||||
# "2026-04-05 22:35:00[,123]" at the start of a line; update.log /
|
||||
# desktop-update-handoff.log stamp with the shell's ISO-8601 "T" shape
|
||||
# ("2026-09-29T21:36:18+08:00", "=== hermes update started 2026-09-29T21:36:18 ===").
|
||||
@@ -174,7 +188,7 @@ def tail_log(
|
||||
print(f"Unknown log: {log_name!r}. Available: {', '.join(sorted(LOG_FILES))}")
|
||||
sys.exit(1)
|
||||
|
||||
log_path = get_hermes_home() / "logs" / filename
|
||||
log_path = log_file_path(log_name)
|
||||
if not log_path.exists():
|
||||
print(f"Log file not found: {log_path}")
|
||||
print("(Logs are created when Hermes runs — try 'hermes chat' first)")
|
||||
@@ -313,19 +327,23 @@ def _age_label(mtime: datetime) -> str:
|
||||
|
||||
|
||||
def list_logs() -> None:
|
||||
"""Print available log files with sizes."""
|
||||
"""Print available log files with sizes (plus the root home's update logs under a profile)."""
|
||||
log_dir = get_hermes_home() / "logs"
|
||||
if not log_dir.exists():
|
||||
root_logs = [path for name in sorted(ROOT_HOME_LOGS)
|
||||
if (path := log_file_path(name)).parent != log_dir and path.is_file()]
|
||||
if not log_dir.exists() and not root_logs:
|
||||
print(f"No logs directory at {display_hermes_home()}/logs/")
|
||||
return
|
||||
|
||||
print(f"Log files in {display_hermes_home()}/logs/:\n")
|
||||
found = False
|
||||
for entry in sorted(log_dir.iterdir()):
|
||||
entries = sorted(log_dir.iterdir()) if log_dir.exists() else []
|
||||
for entry in [*entries, *root_logs]:
|
||||
if entry.is_file() and entry.suffix == ".log":
|
||||
st = entry.stat()
|
||||
age_str = _age_label(datetime.fromtimestamp(st.st_mtime))
|
||||
print(f" {entry.name:<25} {_size_label(st.st_size):>8} {age_str}")
|
||||
label = entry.name if entry.parent == log_dir else f"{entry.name} (root)"
|
||||
print(f" {label:<25} {_size_label(st.st_size):>8} {age_str}")
|
||||
found = True
|
||||
|
||||
if not found:
|
||||
|
||||
@@ -30,10 +30,10 @@ def is_post_swap_child() -> bool:
|
||||
|
||||
|
||||
def write_handoff(payload: dict[str, Any]) -> Path:
|
||||
"""Persist the post-swap payload under HERMES_HOME; returns its path."""
|
||||
from hermes_constants import get_hermes_home
|
||||
"""Persist the post-swap payload beside the receipts (the ROOT home); returns its path."""
|
||||
from hermes_constants import get_default_hermes_root
|
||||
|
||||
directory = get_hermes_home() / "logs" / "update_receipts"
|
||||
directory = get_default_hermes_root() / "logs" / "update_receipts"
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
path = directory / f"post_swap_{os.getpid()}.json"
|
||||
path.write_text(json.dumps(payload, indent=2, default=str), encoding="utf-8")
|
||||
|
||||
@@ -357,7 +357,12 @@ async def get_action_status(name: str, lines: int = 200):
|
||||
durable_update_action_id = None
|
||||
update_receipt_summary = None
|
||||
if name == "hermes-update":
|
||||
durable_update_action_id = _durable_completed_update_action_id(_tail_lines(log_dir / "update.log", 2000))
|
||||
# ``hermes update`` mirrors to the ROOT home's update.log (main_dashboard), never this
|
||||
# dashboard's profile home: read it where it is written.
|
||||
from hermes_constants import get_default_hermes_root
|
||||
|
||||
durable_update_action_id = _durable_completed_update_action_id(
|
||||
_tail_lines(get_default_hermes_root() / "logs" / "update.log", 2000))
|
||||
if durable_update_action_id:
|
||||
marker = f"=== hermes-update completed {durable_update_action_id} ==="
|
||||
if marker not in tail:
|
||||
|
||||
+4
-3
@@ -123,10 +123,11 @@ def _utc_now_iso() -> str:
|
||||
|
||||
def _receipt_dir() -> Path:
|
||||
"""The receipts dir — a pure path computation, NO mkdir side
|
||||
effect: readers (latest()) must not create state."""
|
||||
from hermes_constants import get_hermes_home
|
||||
effect: readers (latest()) must not create state. The ROOT home, like the
|
||||
updater's receipts (hermes_cli.update_receipt): one dir whatever profile runs."""
|
||||
from hermes_constants import get_default_hermes_root
|
||||
|
||||
return get_hermes_home() / "logs" / "update_receipts"
|
||||
return get_default_hermes_root() / "logs" / "update_receipts"
|
||||
|
||||
|
||||
def begin(kind: str) -> contextvars.Token:
|
||||
|
||||
@@ -67,6 +67,23 @@ class TestCaptureDefaultLogSnapshots:
|
||||
assert "Desktop GUI build failed" in snaps["update"].tail_text
|
||||
assert "ELIFECYCLE" in snaps["handoff"].tail_text
|
||||
|
||||
def test_snapshots_read_the_root_home_update_logs_under_a_profile(self, tmp_path, monkeypatch):
|
||||
"""``hermes update`` and the hand-off scripts write the ROOT home's logs, whatever profile."""
|
||||
from hermes_cli.debug import _capture_default_log_snapshots
|
||||
|
||||
root = tmp_path / "root"
|
||||
profile = root / "profiles" / "coder"
|
||||
(root / "logs").mkdir(parents=True)
|
||||
(profile / "logs").mkdir(parents=True)
|
||||
(root / "logs" / "update.log").write_text(UPDATE_LOG_BODY)
|
||||
(root / "logs" / "desktop-update-handoff.log").write_text(HANDOFF_LOG_BODY)
|
||||
monkeypatch.setenv("HERMES_HOME", str(profile))
|
||||
|
||||
snaps = _capture_default_log_snapshots(100)
|
||||
|
||||
assert "Desktop GUI build failed" in snaps["update"].tail_text
|
||||
assert "ELIFECYCLE" in snaps["handoff"].tail_text
|
||||
|
||||
def test_missing_files_report_absence_without_raising(
|
||||
self, home_without_update_logs
|
||||
):
|
||||
|
||||
@@ -228,3 +228,26 @@ def test_every_log_file_writes_a_stamp_hermes_logs_since_can_read():
|
||||
# gateway.error.log (launchd stderr, not in LOG_FILES) uses the shared stamper.
|
||||
from hermes_cli.stderr_timestamp import stamp_line
|
||||
assert _parse_line_timestamp(stamp_line("raw gateway stderr")) is not None
|
||||
|
||||
|
||||
def test_update_logs_are_read_from_the_root_home_under_a_profile(tmp_path, monkeypatch, capsys):
|
||||
# ``hermes update`` mirrors to <root>/logs/update.log and the hand-off scripts write the root
|
||||
# too; `hermes logs update` under a sticky profile read the profile's (absent) copy.
|
||||
from hermes_cli.logs import list_logs, log_file_path, tail_log
|
||||
|
||||
root = tmp_path / "root"
|
||||
profile = root / "profiles" / "coder"
|
||||
(root / "logs").mkdir(parents=True)
|
||||
(profile / "logs").mkdir(parents=True)
|
||||
(root / "logs" / "update.log").write_text("=== hermes update started 2026-10-03T10:00:00 ===\n✓ Update complete!\n")
|
||||
(profile / "logs" / "agent.log").write_text("2026-10-03 10:00:00,000 INFO run_agent: hi\n")
|
||||
monkeypatch.setenv("HERMES_HOME", str(profile))
|
||||
|
||||
assert log_file_path("update") == root / "logs" / "update.log"
|
||||
assert log_file_path("handoff") == root / "logs" / "desktop-update-handoff.log"
|
||||
assert log_file_path("agent") == profile / "logs" / "agent.log"
|
||||
tail_log("update", num_lines=5)
|
||||
assert "✓ Update complete!" in capsys.readouterr().out
|
||||
list_logs()
|
||||
listing = capsys.readouterr().out
|
||||
assert "agent.log" in listing and "update.log (root)" in listing
|
||||
@@ -1455,14 +1455,18 @@ CONFIG_SCHEMA = ProviderConfigSchema(
|
||||
assert check_data["update_command"] == data["update_command"]
|
||||
|
||||
def test_update_status_recovers_completed_result_after_dashboard_restart(self, monkeypatch, tmp_path):
|
||||
|
||||
# The dashboard runs under a profile home; ``hermes update`` mirrors to the ROOT home's
|
||||
# update.log (main_dashboard), so the durable completion marker is read there.
|
||||
root = tmp_path / "root"
|
||||
(root / "logs").mkdir(parents=True)
|
||||
monkeypatch.setenv("HERMES_HOME", str(root / "profiles" / "coder"))
|
||||
action_id = "c" * 32
|
||||
(tmp_path / "hermes-update.log").write_text(
|
||||
"=== hermes-update started 2026-08-17 11:19:34 ===\n"
|
||||
"pulling updates...\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(tmp_path / "update.log").write_text(
|
||||
(root / "logs" / "update.log").write_text(
|
||||
"=== hermes update started 2026-08-17T11:19:35 ===\n"
|
||||
"✓ Update complete!\n"
|
||||
f"=== hermes-update completed {action_id} ===\n",
|
||||
|
||||
Reference in new issue
Block a user