feat(debug): include desktop.log in hermes debug share / /debug / hermes logs (#38203)
The Electron desktop app writes boot failures, backend spawn output, and Python tracebacks to HERMES_HOME/logs/desktop.log, but debug-share only captured agent/errors/gateway — so desktop boot issues never made it into shared debug reports. - logs.py: register desktop -> desktop.log (enables 'hermes logs desktop') - debug.py: capture desktop snapshot, add to summary report, upload full desktop.log in 'share', update privacy notice - gateway /debug inherits the desktop tail via collect_debug_report() - main.py + docs: help text and log-name table (also adds missing gui row) - tests: desktop seed in fixture, new report test, three_pastes -> four_pastes
This commit is contained in:
@ -191,10 +191,10 @@ _PRIVACY_NOTICE = """\
|
|||||||
⚠️ This will upload the following to a public paste service:
|
⚠️ This will upload the following to a public paste service:
|
||||||
• System info (OS, Python version, Hermes version, provider, which API keys
|
• System info (OS, Python version, Hermes version, provider, which API keys
|
||||||
are configured — NOT the actual keys)
|
are configured — NOT the actual keys)
|
||||||
• Recent log lines (agent.log, errors.log, gateway.log — may contain
|
• Recent log lines (agent.log, errors.log, gateway.log, desktop.log — may
|
||||||
conversation fragments and file paths)
|
contain conversation fragments and file paths)
|
||||||
• Full agent.log and gateway.log (up to 512 KB each — likely contains
|
• Full agent.log, gateway.log, and desktop.log (up to 512 KB each — likely
|
||||||
conversation content, tool outputs, and file paths)
|
contains conversation content, tool outputs, and file paths)
|
||||||
|
|
||||||
Pastes auto-delete after 6 hours.
|
Pastes auto-delete after 6 hours.
|
||||||
"""
|
"""
|
||||||
@ -503,6 +503,9 @@ def _capture_default_log_snapshots(
|
|||||||
"gateway": _capture_log_snapshot(
|
"gateway": _capture_log_snapshot(
|
||||||
"gateway", tail_lines=errors_lines, redact=redact
|
"gateway", tail_lines=errors_lines, redact=redact
|
||||||
),
|
),
|
||||||
|
"desktop": _capture_log_snapshot(
|
||||||
|
"desktop", tail_lines=errors_lines, redact=redact
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -569,6 +572,10 @@ def collect_debug_report(
|
|||||||
|
|
||||||
buf.write(f"--- gateway.log (last {errors_lines} lines) ---\n")
|
buf.write(f"--- gateway.log (last {errors_lines} lines) ---\n")
|
||||||
buf.write(log_snapshots["gateway"].tail_text)
|
buf.write(log_snapshots["gateway"].tail_text)
|
||||||
|
buf.write("\n\n")
|
||||||
|
|
||||||
|
buf.write(f"--- desktop.log (last {errors_lines} lines) ---\n")
|
||||||
|
buf.write(log_snapshots["desktop"].tail_text)
|
||||||
buf.write("\n")
|
buf.write("\n")
|
||||||
|
|
||||||
return buf.getvalue()
|
return buf.getvalue()
|
||||||
@ -611,12 +618,15 @@ def run_debug_share(args):
|
|||||||
)
|
)
|
||||||
agent_log = log_snapshots["agent"].full_text
|
agent_log = log_snapshots["agent"].full_text
|
||||||
gateway_log = log_snapshots["gateway"].full_text
|
gateway_log = log_snapshots["gateway"].full_text
|
||||||
|
desktop_log = log_snapshots["desktop"].full_text
|
||||||
|
|
||||||
# Prepend dump header to each full log so every paste is self-contained.
|
# Prepend dump header to each full log so every paste is self-contained.
|
||||||
if agent_log:
|
if agent_log:
|
||||||
agent_log = dump_text + "\n\n--- full agent.log ---\n" + agent_log
|
agent_log = dump_text + "\n\n--- full agent.log ---\n" + agent_log
|
||||||
if gateway_log:
|
if gateway_log:
|
||||||
gateway_log = dump_text + "\n\n--- full gateway.log ---\n" + gateway_log
|
gateway_log = dump_text + "\n\n--- full gateway.log ---\n" + gateway_log
|
||||||
|
if desktop_log:
|
||||||
|
desktop_log = dump_text + "\n\n--- full desktop.log ---\n" + desktop_log
|
||||||
|
|
||||||
# Visible banner so reviewers reading the public paste know redaction
|
# Visible banner so reviewers reading the public paste know redaction
|
||||||
# was applied at upload time. Banner is omitted under --no-redact.
|
# was applied at upload time. Banner is omitted under --no-redact.
|
||||||
@ -626,6 +636,8 @@ def run_debug_share(args):
|
|||||||
agent_log = _REDACTION_BANNER + agent_log
|
agent_log = _REDACTION_BANNER + agent_log
|
||||||
if gateway_log:
|
if gateway_log:
|
||||||
gateway_log = _REDACTION_BANNER + gateway_log
|
gateway_log = _REDACTION_BANNER + gateway_log
|
||||||
|
if desktop_log:
|
||||||
|
desktop_log = _REDACTION_BANNER + desktop_log
|
||||||
|
|
||||||
if local_only:
|
if local_only:
|
||||||
print(report)
|
print(report)
|
||||||
@ -639,6 +651,11 @@ def run_debug_share(args):
|
|||||||
print("FULL gateway.log")
|
print("FULL gateway.log")
|
||||||
print(f"{'=' * 60}\n")
|
print(f"{'=' * 60}\n")
|
||||||
print(gateway_log)
|
print(gateway_log)
|
||||||
|
if desktop_log:
|
||||||
|
print(f"\n\n{'=' * 60}")
|
||||||
|
print("FULL desktop.log")
|
||||||
|
print(f"{'=' * 60}\n")
|
||||||
|
print(desktop_log)
|
||||||
return
|
return
|
||||||
|
|
||||||
print("Uploading...")
|
print("Uploading...")
|
||||||
@ -668,6 +685,13 @@ def run_debug_share(args):
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
failures.append(f"gateway.log: {exc}")
|
failures.append(f"gateway.log: {exc}")
|
||||||
|
|
||||||
|
# 4. Full desktop.log (optional — Electron app boot + backend output)
|
||||||
|
if desktop_log:
|
||||||
|
try:
|
||||||
|
urls["desktop.log"] = upload_to_pastebin(desktop_log, expiry_days=expiry)
|
||||||
|
except Exception as exc:
|
||||||
|
failures.append(f"desktop.log: {exc}")
|
||||||
|
|
||||||
# Print results
|
# Print results
|
||||||
label_width = max(len(k) for k in urls)
|
label_width = max(len(k) for k in urls)
|
||||||
print(f"\nDebug report uploaded:")
|
print(f"\nDebug report uploaded:")
|
||||||
|
|||||||
@ -11,6 +11,7 @@ Usage examples::
|
|||||||
hermes logs errors # last 50 lines of errors.log
|
hermes logs errors # last 50 lines of errors.log
|
||||||
hermes logs gateway -n 100 # last 100 lines of gateway.log
|
hermes logs gateway -n 100 # last 100 lines of gateway.log
|
||||||
hermes logs gui -f # follow gui.log (dashboard/pty/ws)
|
hermes logs gui -f # follow gui.log (dashboard/pty/ws)
|
||||||
|
hermes logs desktop -f # follow desktop.log (Electron app boot/backend)
|
||||||
hermes logs --level WARNING # only WARNING+ lines
|
hermes logs --level WARNING # only WARNING+ lines
|
||||||
hermes logs --session abc123 # filter by session ID substring
|
hermes logs --session abc123 # filter by session ID substring
|
||||||
hermes logs --component tools # only tool-related lines
|
hermes logs --component tools # only tool-related lines
|
||||||
@ -33,6 +34,7 @@ LOG_FILES = {
|
|||||||
"errors": "errors.log",
|
"errors": "errors.log",
|
||||||
"gateway": "gateway.log",
|
"gateway": "gateway.log",
|
||||||
"gui": "gui.log",
|
"gui": "gui.log",
|
||||||
|
"desktop": "desktop.log",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Log line timestamp regex — matches "2026-04-05 22:35:00,123" or
|
# Log line timestamp regex — matches "2026-04-05 22:35:00,123" or
|
||||||
|
|||||||
@ -15192,7 +15192,7 @@ Examples:
|
|||||||
logs_parser = subparsers.add_parser(
|
logs_parser = subparsers.add_parser(
|
||||||
"logs",
|
"logs",
|
||||||
help="View and filter Hermes log files",
|
help="View and filter Hermes log files",
|
||||||
description="View, tail, and filter agent.log / errors.log / gateway.log / gui.log",
|
description="View, tail, and filter agent.log / errors.log / gateway.log / gui.log / desktop.log",
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
epilog="""\
|
epilog="""\
|
||||||
Examples:
|
Examples:
|
||||||
@ -15201,6 +15201,7 @@ Examples:
|
|||||||
hermes logs errors Show last 50 lines of errors.log
|
hermes logs errors Show last 50 lines of errors.log
|
||||||
hermes logs gateway -n 100 Show last 100 lines of gateway.log
|
hermes logs gateway -n 100 Show last 100 lines of gateway.log
|
||||||
hermes logs gui -f Follow gui.log in real time
|
hermes logs gui -f Follow gui.log in real time
|
||||||
|
hermes logs desktop -f Follow desktop.log (Electron app boot/backend)
|
||||||
hermes logs --level WARNING Only show WARNING and above
|
hermes logs --level WARNING Only show WARNING and above
|
||||||
hermes logs --session abc123 Filter by session ID
|
hermes logs --session abc123 Filter by session ID
|
||||||
hermes logs --component tools Only show tool-related lines
|
hermes logs --component tools Only show tool-related lines
|
||||||
|
|||||||
@ -31,6 +31,9 @@ def hermes_home(tmp_path, monkeypatch):
|
|||||||
(logs_dir / "gateway.log").write_text(
|
(logs_dir / "gateway.log").write_text(
|
||||||
"2026-04-12 17:00:10 INFO gateway.run: started\n"
|
"2026-04-12 17:00:10 INFO gateway.run: started\n"
|
||||||
)
|
)
|
||||||
|
(logs_dir / "desktop.log").write_text(
|
||||||
|
"2026-04-12 17:00:15 INFO desktop: backend spawned\n"
|
||||||
|
)
|
||||||
|
|
||||||
return home
|
return home
|
||||||
|
|
||||||
@ -451,6 +454,15 @@ class TestCollectDebugReport:
|
|||||||
|
|
||||||
assert "--- gateway.log" in report
|
assert "--- gateway.log" in report
|
||||||
|
|
||||||
|
def test_report_includes_desktop_log(self, hermes_home):
|
||||||
|
from hermes_cli.debug import collect_debug_report
|
||||||
|
|
||||||
|
with patch("hermes_cli.dump.run_dump"):
|
||||||
|
report = collect_debug_report(log_lines=50)
|
||||||
|
|
||||||
|
assert "--- desktop.log" in report
|
||||||
|
assert "backend spawned" in report
|
||||||
|
|
||||||
def test_missing_logs_handled(self, tmp_path, monkeypatch):
|
def test_missing_logs_handled(self, tmp_path, monkeypatch):
|
||||||
home = tmp_path / ".hermes"
|
home = tmp_path / ".hermes"
|
||||||
home.mkdir()
|
home.mkdir()
|
||||||
@ -526,8 +538,8 @@ class TestRunDebugShare:
|
|||||||
assert "FULL agent.log" in out
|
assert "FULL agent.log" in out
|
||||||
assert "FULL gateway.log" in out
|
assert "FULL gateway.log" in out
|
||||||
|
|
||||||
def test_share_uploads_three_pastes(self, hermes_home, capsys):
|
def test_share_uploads_four_pastes(self, hermes_home, capsys):
|
||||||
"""Successful share uploads report + agent.log + gateway.log."""
|
"""Successful share uploads report + agent.log + gateway.log + desktop.log."""
|
||||||
from hermes_cli.debug import run_debug_share
|
from hermes_cli.debug import run_debug_share
|
||||||
|
|
||||||
args = MagicMock()
|
args = MagicMock()
|
||||||
@ -549,14 +561,16 @@ class TestRunDebugShare:
|
|||||||
run_debug_share(args)
|
run_debug_share(args)
|
||||||
|
|
||||||
out = capsys.readouterr().out
|
out = capsys.readouterr().out
|
||||||
# Should have 3 uploads: report, agent.log, gateway.log
|
# Should have 4 uploads: report, agent.log, gateway.log, desktop.log
|
||||||
assert call_count[0] == 3
|
assert call_count[0] == 4
|
||||||
assert "paste.rs/paste1" in out # Report
|
assert "paste.rs/paste1" in out # Report
|
||||||
assert "paste.rs/paste2" in out # agent.log
|
assert "paste.rs/paste2" in out # agent.log
|
||||||
assert "paste.rs/paste3" in out # gateway.log
|
assert "paste.rs/paste3" in out # gateway.log
|
||||||
|
assert "paste.rs/paste4" in out # desktop.log
|
||||||
assert "Report" in out
|
assert "Report" in out
|
||||||
assert "agent.log" in out
|
assert "agent.log" in out
|
||||||
assert "gateway.log" in out
|
assert "gateway.log" in out
|
||||||
|
assert "desktop.log" in out
|
||||||
|
|
||||||
# Each log paste should start with the dump header
|
# Each log paste should start with the dump header
|
||||||
agent_paste = uploaded_content[1]
|
agent_paste = uploaded_content[1]
|
||||||
@ -565,6 +579,9 @@ class TestRunDebugShare:
|
|||||||
gateway_paste = uploaded_content[2]
|
gateway_paste = uploaded_content[2]
|
||||||
assert "--- hermes dump ---" in gateway_paste
|
assert "--- hermes dump ---" in gateway_paste
|
||||||
assert "--- full gateway.log ---" in gateway_paste
|
assert "--- full gateway.log ---" in gateway_paste
|
||||||
|
desktop_paste = uploaded_content[3]
|
||||||
|
assert "--- hermes dump ---" in desktop_paste
|
||||||
|
assert "--- full desktop.log ---" in desktop_paste
|
||||||
|
|
||||||
def test_share_keeps_report_and_full_log_on_same_snapshot(self, hermes_home, capsys):
|
def test_share_keeps_report_and_full_log_on_same_snapshot(self, hermes_home, capsys):
|
||||||
"""A mid-run rotation must not make full agent.log older than the report."""
|
"""A mid-run rotation must not make full agent.log older than the report."""
|
||||||
|
|||||||
@ -835,6 +835,8 @@ View, tail, and filter Hermes log files. All logs are stored in `~/.hermes/logs/
|
|||||||
| `agent` (default) | `agent.log` | All agent activity — API calls, tool dispatch, session lifecycle (INFO and above) |
|
| `agent` (default) | `agent.log` | All agent activity — API calls, tool dispatch, session lifecycle (INFO and above) |
|
||||||
| `errors` | `errors.log` | Warnings and errors only — a filtered subset of agent.log |
|
| `errors` | `errors.log` | Warnings and errors only — a filtered subset of agent.log |
|
||||||
| `gateway` | `gateway.log` | Messaging gateway activity — platform connections, message dispatch, webhook events |
|
| `gateway` | `gateway.log` | Messaging gateway activity — platform connections, message dispatch, webhook events |
|
||||||
|
| `gui` | `gui.log` | Dashboard / TUI-gateway / PTY-bridge / websocket events |
|
||||||
|
| `desktop` | `desktop.log` | Electron desktop app — boot, backend spawn output, and recent Python tracebacks |
|
||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user