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:
Teknium
2026-06-03 05:41:35 -07:00
committed by GitHub
parent 1d90b23982
commit 1b302a0474
5 changed files with 55 additions and 9 deletions

View File

@ -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:")

View File

@ -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

View File

@ -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

View File

@ -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."""

View File

@ -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