Files
hermes-agent/website/docs/user-guide/features/heartbeat.md
teknium1 680735174a fix(gateway): scheduled heartbeats end silently, emit no progress and quote no stale message
Slim follow-up on the #112160 pick (KoNit-K) that also lands the #113031 mechanism:

- gateway/response_filters.py::display_kind_for_event maps the poller-stamped
  `_heartbeat_session_id` provenance to the machinery display kind, so a bare
  NO_REPLY / [SILENT] on a scheduled heartbeat turn is honoured instead of being
  replaced with the "model returned only a silence marker" warning every interval.
  The event stays non-internal on purpose: authorization and the emergency stop
  still apply (test_idle_wake_coalesces_intervals_while_adapter_owns_turn).
  Ported from #112279 (earliest filer), collapsed onto the existing stamp instead
  of a second flag.
- gateway/run_goals.py::_synthetic_prompt_event drops the stored source's
  message_id: a synthetic prompt is not a reply to the message that registered
  the watch, and every consumer (progress, status, footer, final reply) fell back
  to it as the Telegram reply anchor — the stale quote in #112149. Telegram DM
  topics route anchorless via direct_messages_topic_id (designed path), so this
  replaces #112160's allow_source_message_id_fallback plumbing across
  platforms/base.py, run.py and run_turn.py (dropped hunks).
- gateway/platforms/base.py: no typing indicator for a heartbeat turn (re-applied
  from #112160 after taking main's file back).
- Tests: poller test asserts the provenance + anchorless source (red on base);
  test_gateway_silence_tokens gains the heartbeat-silence invariant (red on base).
  #112160's _thread_metadata_for_event test dropped with its mechanism.
- Docs: heartbeat page documents the quiet turn.

Co-authored-by: Kevin Rajan <7121943+kvnloo@users.noreply.github.com>
Co-authored-by: MrAtkeens <99114477+MrAtkeens@users.noreply.github.com>
2026-09-16 17:23:01 -07:00

5.5 KiB

sidebar_position, title, description
sidebar_position title description
17 Session Heartbeats A recurring prompt that re-enters your current session whenever it's idle — /heartbeat every 10m Check the deployment.

Session Heartbeats (/heartbeat)

/heartbeat gives the current session one recurring instruction. Whenever the session is idle and the interval has elapsed, the prompt fires as a normal user turn — same conversation, same context, same prompt cache.

/heartbeat every 10m Check the deployment and report meaningful changes

Inspired by Prime-Agent's /heartbeat. The Hermes adaptation keeps the strict message-flow invariants: the heartbeat is injected only between turns (never mid-run), as a plain user-role message.

Heartbeat vs cron: which one do I want?

They look similar but serve different jobs:

/heartbeat hermes cron
Runs in This conversation — full context, memory of the discussion A fresh isolated session per tick
Survives process restart State survives (SessionDB); gateway watches resume automatically after restart Yes — fully durable scheduler
How many One per session Unlimited jobs
Best for "Keep an eye on X in this thread while we work" Standing jobs, reports, watchdogs, deliveries

Rule of thumb: if the recurring prompt needs the conversation's context, use /heartbeat. If it's a self-contained job, use cron.

Commands

Command What it does
/heartbeat every <interval> <prompt> Set (or replace) the session's heartbeat. Intervals: 90s, 10m, 2h, 1d (minimum 60s).
/heartbeat or /heartbeat status Show the heartbeat, its interval, and time to next fire.
/heartbeat pause Stop firing without clearing.
/heartbeat resume Resume (re-anchors the timer — no instant stale fire).
/heartbeat clear Remove the heartbeat.

/hb is an alias. Works on the CLI, the TUI / Desktop app, and gateway platforms (on Slack, use /hermes heartbeat …).

Behavior details

  • Idle-only. A heartbeat never interrupts a running turn. If the agent is busy when the tick comes due, it fires at the next idle poll. In the gateway, an idle watched session wakes proactively; no new inbound message is needed.
  • Missed ticks coalesce. If the session was busy (or the process wasn't running) through several intervals, you get one heartbeat turn, not a backlog. The timer re-anchors on every fire.
  • User messages win. A queued user message always takes priority; the heartbeat waits for the input queue to drain.
  • Owned by the surface that set it. A heartbeat set from a messaging chat fires from the gateway and replies into that chat even while the same session is open in the TUI / Desktop app; the viewer never claims the tick.
  • Cache-safe. The injected prompt is an ordinary user message. No system-prompt mutation, no toolset change.
  • Gateway recovery. Startup restores active heartbeats using the current persisted conversation and thread routing, in the owning profile. Each poll retries recovery after temporary storage failures or adapter downtime; paused and cleared heartbeats and suspended conversations do not restart. No new chat message is required.
  • Persistence and conversation boundaries. State lives in SessionDB.state_meta keyed by heartbeat:<session_id> and follows context-compression session rotations. In the messaging gateway, leaving a conversation through reset, switch, or suspension clears its heartbeat; resuming that archived conversation does not resurrect it. Firing requires the owning process (CLI session or gateway) to be running. An already-admitted gateway tick is checked again after session resolution and before agent execution: it may follow a compression child, but cannot carry its old instruction into a reset or switched conversation.
  • Execution accounting. The gateway reserves a due tick at adapter admission. If that exact attempt ends before entering the agent runner (including cancellation or a routing, authorization, emergency-stop, or preparation rejection), it refunds the tick unless the schedule has since changed. Once the agent runner is entered, the fire remains counted even if execution fails or is interrupted. This count is not proof of a successful model response or outbound delivery; abrupt process death can prevent the refund callback.
  • Quiet when there is nothing to say (gateway). A scheduled heartbeat turn may end with a bare silence marker (NO_REPLY / [SILENT]): the gateway sends nothing, unlike a human message that returns only a marker (that still gets the visible "try again" notice). While the heartbeat works, no typing indicator, tool-progress, streamed draft or "still working" bubble is posted, and a result it does deliver is routed to the chat/topic without quoting the message that set the heartbeat. Approval prompts and failures stay visible.
  • Don't-invent-work guard. The injected prompt tells the agent to reply briefly and stop when nothing meaningful changed, so an idle heartbeat doesn't generate busywork.

Example

You: /heartbeat every 15m Check whether the CI run for PR #1234 finished; summarize the result when it does

  ♥ Heartbeat set (every 15m): Check whether the CI run for PR #1234 finished; ...

[15 minutes of you working on other things in the same session]

Hermes: [Heartbeat — recurring instruction, fires every 15m]
  💻 gh pr checks 1234   (1.2s)
  CI is still running (14/37 checks complete). Nothing to report yet.

When the answer stops changing, /heartbeat clear it — or let it keep watch.