Skip to content

RFC: On-demand heartbeat trigger for debugging #3437

Description

@basil

Problem / Motivation

The heartbeat is currently observable only via timer ticks (default 30 min) and logs. There's no way to fire one from a known channel context, and no way to fire Phase 1 (the skip/run decision) without paying for Phase 2 (execution). This makes iteration on HEARTBEAT.md slow and makes several open issues hard to reproduce or diagnose:

Across these, the missing feature is the same: a way to fire a heartbeat on demand, optionally without executing it, optionally scoped to a specific channel.

Concrete use cases:

  1. Iterating on HEARTBEAT.md phrasing. Edit the file, fire a Phase-1-only check, see the skip/run decision and the extracted task summary. Iterate without paying for Phase 2 and without delivering noise to a real channel.
  2. Reproducing Heartbeat Issue #2797 / The two-phase heartbeat mechanism will lose lots of task detail #1310-style behavior. Run a full tick on demand to confirm a candidate fix instead of waiting up to 30 minutes per cycle.
  3. Validating a proposed pre-check ([Feature] Skip heartbeat LLM call when HEARTBEAT.md has no active tasks #2406). Compare what a static parse decides vs. what the LLM decides on the same file, across many real HEARTBEAT.md examples.
  4. Per-channel debugging. Trigger from Telegram chat A and have the response delivered to A: useful when debugging delivery issues on a specific channel.
  5. End-to-end test harnesses. Tests (and the agent itself, when asked) can drive the full heartbeat flow without monkey-patching the timer.

Proposed Solution

Expose an on-demand trigger with two actions, sharing the same underlying mechanics as a timer tick:

  • dry_run: Phase 1 only: read HEARTBEAT.md, ask the LLM, return the decision and extracted tasks. No execution.
  • run: Phase 1 + Phase 2: same path as a timer tick.
  • A shared lock with the timer so manual and timer fires can't collide.

Expose this via either (or both):

  • Agent-callable tool. Reachable from any channel, so users can debug from inside a conversation. The calling session's channel/chat is used as the delivery target for run.
  • CLI subcommand. Something like nanobot heartbeat trigger [--dry-run]. Fits a "developer tool" framing and stays out of the tool list the LLM sees on every turn.

Open questions:

  1. Interface: tool, CLI, or both? A tool is reachable from any channel but adds to what the LLM sees on every turn (and risks the LLM firing it unprompted). A CLI is cleaner for a debugging-only feature but doesn't help users debugging from inside a chat.
  2. Scope of dry_run. Ship Phase-1-only mode, or just run? Phase-1-only is the cheaper iteration loop and the more useful developer tool for [Feature] Skip heartbeat LLM call when HEARTBEAT.md has no active tasks #2406-style work, but it's a second action to document and learn.
  3. Per-channel routing on timer ticks. Should all heartbeats remember an origin (e.g. the channel that last edited HEARTBEAT.md), or only manual triggers? The asymmetric option is simpler but means manual fires and timer fires can deliver to different chats from the same file.
  4. Concurrency policy. When a manual trigger arrives mid-tick, should it refuse, queue, or coalesce? Refuse is simplest. Queueing risks runaway under repeated manual fires. Coalescing is the hardest to explain.

Alternatives Considered

Related Component

Agent / Prompts

Additional Context

Linked issues that motivate this: #2797, #1310, #2406, #1899.

Particularly interested in input from anyone who has hit #2797, #1310, or #2406: would on-demand triggering (and especially Phase-1-only dry_run) have meaningfully shortened your debugging cycle?

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions