Skip to content

feat(gateway): built-in health heartbeat (adapters, dashboard, nightly sync) - #20

Merged
vashkartik merged 4 commits into
ace/patchesfrom
fix/hermes-durable-background-work
Aug 4, 2026
Merged

feat(gateway): built-in health heartbeat (adapters, dashboard, nightly sync)#20
vashkartik merged 4 commits into
ace/patchesfrom
fix/hermes-durable-background-work

Conversation

@vashkartik

Copy link
Copy Markdown
Owner

Why

The nightly upstream sync has been failing for weeks — Aug 2 on SSH fetch, Aug 3 on a 31-file conflict merge — and the only record was last-run.json on disk. The dashboard backend dies/respawns on a moving port with nothing watching. Nobody gets told. Owner directive: health checks + heartbeats for Hermes (Ace Coder + Telegram), built into Hermes.

What

gateway/heartbeat.py — config-gated HeartbeatService (off by default) started from start_gateway:

Probe Signal
gateway live platform adapters attached (running at all proves process + config + outbound HTTP)
dashboard the desktop/Ace backend, via a startup portfile (<hermes_home>/dashboard.portfile.json, written by web_server at bind — ports change every respawn, so discovery beats config): PID alive + HTTP root answers
nightly_sync last-run.json: failed alerts, and silence past nightly_max_age_hours also alerts (a daily job that stops running is a failure too)

Cadence: immediate alert on unhealthy transition → re-alert every realert_hours while broken → one recovery message → periodic all-healthy beat every heartbeat_hours (so silence ≠ dead monitor). Failed sends leave cadence state untouched and retry next tick. Delivery uses the standalone platform sender (same as cron), so beats get out even when a live adapter object is wedged.

heartbeat:
  enabled: true
  channel: "telegram:<chat_id>"
  check_interval_minutes: 5
  heartbeat_hours: 24
  realert_hours: 6
  nightly_status_file: /abs/path/last-run.json   # for cross-HERMES_HOME setups

Checks

  • New tests/gateway/test_heartbeat.py: 22 tests (config parsing, all three probes incl. a real HTTP server, alert/realert/recovery/beat cadence, failed-send retry, render). All green; ruff clean.
  • Per-file runs of neighboring gateway test files green with the change; test_complete_path_at_filter.py / test_systemd_notify.py fail identically on the clean base locally (env-dependent: AF_UNIX tmp-path length) — pre-existing, green in CI.

Also (outside this diff): live repo upstream remote switched SSH→HTTPS so the 4 AM launchd fetch stops flaking without an ssh-agent.

🤖 Generated with Claude Code

vectorcmd and others added 3 commits August 4, 2026 00:52
…tion hotfix

Production evidence (gui.log, desktop.log, live dirty tree) showed why the
merged durable-session fix still wasn't enough for "it only starts working
when I open the session":

1. Accepted-but-unstarted prompts (`session["queued_prompt"]`) lived only in
   memory and only drained from client-driven hooks. The 20s WS-orphan reaper
   and the TTL/LRU evictors happily tore down a detached session holding one,
   silently dropping a message the server had said yes to. All three reapers
   now treat queued work as pending work and spare the session.

2. When the end-of-turn drain lost its update-guard race (or the turn thread
   died between clearing `running` and draining), nothing ever retried without
   a client attached. A background sweeper now starts accepted prompts on idle
   sessions every 15s, kicking the agent build first when the prompt was
   queued during the build window. Work the server accepted runs whether or
   not anyone is watching.

3. The live install carried two hand-applied hotfixes that only existed as
   uncommitted edits (lost on every nightly sync): the compacted-transcript
   fix (include_compacted in SessionDB.get_messages + REST/api_server/compute
   host readers — history vanishing after in-place compression) and a
   client_request_id dedup bridge. The transcript fix is ported verbatim; the
   bridge is superseded by the request ledger, with wire compat kept:
   `client_request_id` is accepted as an alias for `request_id`, duplicate
   responses carry `client_request_id`/`deduplicated`, and `prompt.status`
   answers alongside `request.status`, so clients built against the hotfix
   keep working unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two TestFetchOpenRouterModels tests mocked the live /v1/models HTTP call but
not get_curated_openrouter_models(), so preferred_ids silently tracked the
DEPLOYED catalog manifest — when it dropped qwen/qwen3.7-max the tests broke
on every branch (pre-existing on ace/patches, verified). The sibling
fallback test already pins the manifest out with exactly this patch and a
comment saying why; apply the same pin to both.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…y sync)

The nightly upstream sync had been failing for weeks (SSH fetch errors, then
31-file merge conflicts) with status only in last-run.json — nobody told. The
dashboard backend dies and respawns on a moving port with nothing watching
it. Hermes now monitors itself from the gateway — the always-on process —
and reports to the chat surface the user actually reads:

- gateway/heartbeat.py: config-gated HeartbeatService (off by default).
  Probes: live platform adapters, the dashboard backend (via a startup
  portfile — ports change every respawn, so discovery beats config), and the
  nightly-sync last-run.json (failed OR silent-past-deadline both alert).
  Cadence: alert immediately on unhealthy transition, re-alert every
  realert_hours while broken, recovery message once, periodic all-healthy
  beat every heartbeat_hours so silence is distinguishable from a dead
  monitor. A failed send leaves cadence state untouched so the next tick
  retries. Delivery reuses the standalone platform sender cron uses, so a
  beat can still get out when a live adapter object is wedged.
- gateway/run.py: start the service after cron/housekeeping in
  start_gateway, wired to runner.adapters.
- hermes_cli/web_server.py: write <hermes_home>/dashboard.portfile.json
  (port + PID) once the HTTP server binds, enabling out-of-process probes.

Config:
  heartbeat:
    enabled: true
    channel: "telegram:<chat_id>"
    check_interval_minutes: 5
    heartbeat_hours: 24
    realert_hours: 6
    nightly_status_file: /path/to/last-run.json   # cross-HERMES_HOME setups

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vashkartik
vashkartik requested a review from vectorcmd as a code owner August 4, 2026 05:51
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@vectorcmd vectorcmd left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Owner-authorized merge (established flow, PRs #17/#19). CI fully green. Recorded as CODEOWNERS approval at the repo owner's instruction — not an independent human review.

@vashkartik
vashkartik merged commit e121eb8 into ace/patches Aug 4, 2026
37 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants