Skip to content

feat(kernel/heartbeat): deliver HealthCheckFailed to notification.alert_channels#3218

Merged
houko merged 2 commits into
librefang:mainfrom
neo-wanderer:feat/heartbeat-alert-channels
Apr 26, 2026
Merged

feat(kernel/heartbeat): deliver HealthCheckFailed to notification.alert_channels#3218
houko merged 2 commits into
librefang:mainfrom
neo-wanderer:feat/heartbeat-alert-channels

Conversation

@neo-wanderer

Copy link
Copy Markdown
Contributor

Summary

  • Closes Heartbeat: HealthCheckFailed events are not delivered to notification.alert_channels #3217
  • Heartbeat monitor already publishes SystemEvent::HealthCheckFailed, but operators on notification.alert_channels never see unresponsive-agent alerts — only tool_failure and task_failed are routed there. This patch makes heartbeat symmetric.
  • Two small changes in kernel/mod.rs:
    1. In start_heartbeat_monitor, on the unresponsive-transition gate that already de-dupes the WARN log and the event-bus publish, additionally call push_notification(agent_id, "health_check_failed", msg).
    2. Extend the global-fallback match arm in push_notification to include "health_check_failed" alongside task_completed / task_failed / tool_failure, so a deployment with only a bare [notification.alert_channels] block (no agent_rules) still receives heartbeat alerts.

Routing example

Operators who want a dedicated topic/channel for heartbeats can now use:

[[notification.agent_rules]]
agent_pattern = "*"
events = ["health_check_failed"]
[[notification.agent_rules.channels]]
channel_type = "telegram"
recipient = "-1001234567890"
thread_id = "7"

Or they can rely on the global fallback by leaving agent_rules empty and configuring a single [notification.alert_channels] block.

Design notes

  • Recovery is intentionally silent — no "agent recovered" notification — to avoid the noise that an N-agent deployment would generate during normal restart churn. The existing INFO log line "Agent recovered from unresponsive state" is sufficient for log-based observability.
  • The push happens inside the same known_unresponsive.insert() gate that protects the WARN log and the event-bus publish, so we get one notification per transition (not one per heartbeat tick while the agent stays down).
  • Quiet-hours filtering (is_quiet_hours) still applies because the new push is downstream of that early-continue.

Test plan

  • cargo build -p librefang-kernel --lib
  • cargo test -p librefang-kernel --lib heartbeat — 10 passed (existing tests unchanged).
  • cargo clippy -p librefang-kernel --lib --all-targets -- -D warnings — clean.
  • Live test: configure [notification.alert_channels] to point at a Telegram topic, force an autonomous agent into an unresponsive state, confirm a single message arrives in the topic and a single message arrives on recovery (well — none, by design).

Follow-up worth tracking separately

AutonomousConfig.heartbeat_channel: Option<String> is declared but unused. With this PR, per-agent routing is available via notification.agent_rules instead. The vestigial field could be either wired up or removed; not in scope for this PR. Tracked in #3217.

…rt_channels

The heartbeat monitor already publishes `SystemEvent::HealthCheckFailed` to
the internal event bus, but nothing routes those events to the operator
notification layer. As a result, an unresponsive agent shows up only as a
WARN log line — Telegram/Slack/Discord/etc. operators never see it,
unlike `tool_failure` and `task_failed`, which are pushed to
`notification.alert_channels` (and matching `notification.agent_rules`)
via `push_notification`.

This patch makes heartbeat alerts symmetric with the other failure-class
notifications:

1. In `start_heartbeat_monitor`, on the *transition* to unresponsive (same
   gate that already de-dupes the WARN log and the event-bus publish), call
   `push_notification(agent_id, "health_check_failed", msg)` so configured
   channels receive a human-readable alert.
2. Extend the global-fallback match arm in `push_notification` to include
   `"health_check_failed"` alongside `task_completed` / `task_failed` /
   `tool_failure`, so a deployment with only a global `[notification.alert_channels]`
   block (no per-agent rules) still gets these alerts.

`agent_rules` already filters by event-type string, so operators can route
heartbeat alerts to a dedicated topic with:

  [[notification.agent_rules]]
  agent_pattern = "*"
  events = ["health_check_failed"]
  [[notification.agent_rules.channels]]
  channel_type = "telegram"
  recipient = "..."
  thread_id = "..."

Recovery is left intentionally silent (no "agent recovered" notification)
to avoid the noise that an N-agent deployment would generate during normal
restart churn — the WARN/INFO log pair already covers it for log readers.
@github-actions github-actions Bot added size/S 10-49 lines changed area/kernel Core kernel (scheduling, RBAC, workflows) ready-for-review PR is ready for maintainer review labels Apr 26, 2026
@github-actions
github-actions Bot requested a review from houko April 26, 2026 06:51

@houko houko left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM. Direction and implementation both correct — minimal diff, gated on the same transition boundary as the WARN log and event-bus publish, quiet-hours continue is upstream so silence still applies.

Two follow-ups worth tracking (not blocking this merge):

  1. Add a regression test pinning the transition behavior — one tick unresponsive → one push, second tick still unresponsive → no further push, recovery → no push. Prevents parallel work from silently disabling the wiring (per CLAUDE.md "integration tests at the injection site").
  2. The live test box in the PR body is still unchecked. Recommend running it post-merge against a real Telegram alert_channels topic before relying on this in prod.

Separate cleanup (out of scope here): docs/configuration/core/page.mdx is drifted from the actual NotificationConfig schema (kind/target/agent/targets vs real channel_type/recipient/agent_pattern/channels/events), and "health_check_failed" event_type isn't enumerated. Worth a separate docs PR.

@houko
houko merged commit 84f8f2f into librefang:main Apr 26, 2026
7 checks passed
@github-actions github-actions Bot removed the ready-for-review PR is ready for maintainer review label Apr 26, 2026
houko added a commit that referenced this pull request Apr 26, 2026
…heck_failed (#3222)

Follow-up for #3218 — locks the global-fallback match arm so a future
refactor cannot silently disable heartbeat alerts.

Adds four pure routing tests against LibreFangKernel::push_notification:
- health_check_failed falls back to alert_channels when no agent_rule
  matches
- a matching agent_rule overrides alert_channels
- no configured targets produce zero sends (silent, not panicking)
- unknown event_type yields zero sends (typo guard — never page on
  unrecognized event_type)

Reuses the RecordingChannelAdapter pattern already used by
test_notify_escalated_approval_prefers_request_route_to. No production
code changes.
@houko houko mentioned this pull request Apr 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/kernel Core kernel (scheduling, RBAC, workflows) size/S 10-49 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Heartbeat: HealthCheckFailed events are not delivered to notification.alert_channels

2 participants