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
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
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:
System info (OS, Python version, Hermes version, provider, which API keys
are configured NOT the actual keys)
Recent log lines (agent.log, errors.log, gateway.log may contain
conversation fragments and file paths)
Full agent.log and gateway.log (up to 512 KB each likely contains
conversation content, tool outputs, and file paths)
Recent log lines (agent.log, errors.log, gateway.log, desktop.log may
contain conversation fragments and file paths)
Full agent.log, gateway.log, and desktop.log (up to 512 KB each likely
contains conversation content, tool outputs, and file paths)
Pastes auto-delete after 6 hours.
"""
@ -503,6 +503,9 @@ def _capture_default_log_snapshots(
"gateway": _capture_log_snapshot(
"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(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")
return buf.getvalue()
@ -611,12 +618,15 @@ def run_debug_share(args):
)
agent_log = log_snapshots["agent"].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.
if agent_log:
agent_log = dump_text + "\n\n--- full agent.log ---\n" + agent_log
if 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
# 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
if gateway_log:
gateway_log = _REDACTION_BANNER + gateway_log
if desktop_log:
desktop_log = _REDACTION_BANNER + desktop_log
if local_only:
print(report)
@ -639,6 +651,11 @@ def run_debug_share(args):
print("FULL gateway.log")
print(f"{'=' * 60}\n")
print(gateway_log)
if desktop_log:
print(f"\n\n{'=' * 60}")
print("FULL desktop.log")
print(f"{'=' * 60}\n")
print(desktop_log)
return
print("Uploading...")
@ -668,6 +685,13 @@ def run_debug_share(args):
except Exception as 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
label_width = max(len(k) for k in urls)
print(f"\nDebug report uploaded:")

View file

@ -11,6 +11,7 @@ Usage examples::
hermes logs errors # last 50 lines of errors.log
hermes logs gateway -n 100 # last 100 lines of gateway.log
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 --session abc123 # filter by session ID substring
hermes logs --component tools # only tool-related lines
@ -33,6 +34,7 @@ LOG_FILES = {
"errors": "errors.log",
"gateway": "gateway.log",
"gui": "gui.log",
"desktop": "desktop.log",
}
# 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",
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,
epilog="""\
Examples:
@ -15201,6 +15201,7 @@ Examples:
hermes logs errors Show last 50 lines of errors.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 desktop -f Follow desktop.log (Electron app boot/backend)
hermes logs --level WARNING Only show WARNING and above
hermes logs --session abc123 Filter by session ID
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(
"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
@ -451,6 +454,15 @@ class TestCollectDebugReport:
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):
home = tmp_path / ".hermes"
home.mkdir()
@ -526,8 +538,8 @@ class TestRunDebugShare:
assert "FULL agent.log" in out
assert "FULL gateway.log" in out
def test_share_uploads_three_pastes(self, hermes_home, capsys):
"""Successful share uploads report + agent.log + gateway.log."""
def test_share_uploads_four_pastes(self, hermes_home, capsys):
"""Successful share uploads report + agent.log + gateway.log + desktop.log."""
from hermes_cli.debug import run_debug_share
args = MagicMock()
@ -549,14 +561,16 @@ class TestRunDebugShare:
run_debug_share(args)
out = capsys.readouterr().out
# Should have 3 uploads: report, agent.log, gateway.log
assert call_count[0] == 3
# Should have 4 uploads: report, agent.log, gateway.log, desktop.log
assert call_count[0] == 4
assert "paste.rs/paste1" in out # Report
assert "paste.rs/paste2" in out # agent.log
assert "paste.rs/paste3" in out # gateway.log
assert "paste.rs/paste4" in out # desktop.log
assert "Report" in out
assert "agent.log" in out
assert "gateway.log" in out
assert "desktop.log" in out
# Each log paste should start with the dump header
agent_paste = uploaded_content[1]
@ -565,6 +579,9 @@ class TestRunDebugShare:
gateway_paste = uploaded_content[2]
assert "--- hermes dump ---" 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):
"""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) |
| `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 |
| `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