Skip to content

Heartbeat has no content contract on the CAP path — every BYO agent is told to read a HEARTBEAT.md that is never provisioned (ADR-008) #800

Description

@lilyshen0722

Observed

Every BYO wrapper agent is told, on every heartbeat, to read a file that does not exist.

The kernel's heartbeat prompt (611B, p.content from schedulerService) ends with:

Read your HEARTBEAT.md workspace file and follow it exactly.
HEARTBEAT_OK is a return value — never post it or any narration to the pod chat.

No wrapper agent has that file:

pod-architect  MISSING     ux-lead   MISSING     sprint-review  MISSING
fable-lead     MISSING     sprint-impl MISSING

Delivery counts from the live session stores: pod-architect has received the
instruction 207 times, sprint-review 238 times. Each one is a real
subprocess spawn that executes an instruction with nothing behind it. The agents
cope (they return HEARTBEAT_OK), so this has been silently wasteful rather than
visibly broken.

Root cause — a kernel/driver contract that only one driver implements

HEARTBEAT.md is an openclaw convention: registry.js owns the templates and
the provisioner writes the file onto the agent's PVC, so the instruction resolves
there. Nothing in commonly agent attach provisions it, so on the CAP path the
prompt is a pointer that outlives its target.

The dispatch half is already driver-agnostic and works correctly — schedulerService
cron → per-agent config.heartbeat.everyMinutes (default 60, sha256 stagger) →
AgentEvent{type:'heartbeat'} → any CAP poller. It is the content half that was
never lifted out of openclaw.

Why this is ADR-008

ADR-008 declares the driver-agnostic agent environment. Its surface is
workspace / sandbox / skills / mcp — and it does not mention heartbeat once.
That omission is the defect: the primitive that exists to keep environment concerns
driver-neutral never covered the one concern that stayed driver-specific.

  • ADR-008 — owner. Add heartbeat as a declaration alongside the existing four.
  • ADR-005 — consumer. The wrapper seeds and reads it (mountSkills already
    seeds workspace files at spawn, so the mechanism exists).
  • ADR-012 — adjacent. Already prepends the cycles takeaway to every heartbeat;
    must not end up as a competing second source of heartbeat instruction.

Proposal (needs design — filing to think, not to implement)

commonly agent attach seeds a default heartbeat routine into the agent workspace,
overridable per agent.

The default must be silent. A heartbeat that reads recent pod messages and reacts
reintroduces two classes we have already paid to fix:

Invariant worth designing around: a tick that cannot post is a tick you can safely
run on twenty agents.
Proposed default work — memory consolidation (already
kernel-injected via ADR-012) plus checking state that emits no event (PR merged, CI
red, assigned issue changed). That last one is the genuine case for a timer rather
than a trigger.

Open design questions

  1. Where does the definition live? Server-side (config.heartbeat.prompt) survives
    reinstall and machine change and is UI-visible, but the agent cannot revise its own
    routine. A local file matches the openclaw convention and is self-editable, but dies
    on re-attach to a new machine — a real scenario for BYO. Leaning: local file seeded
    from the server at attach
    , so it is re-derivable and locally editable.
  2. One source or two? Today the server sends the prompt and the prompt delegates to
    a local file. That indirection is exactly what produced this bug. Resolve to one.
  3. Should these agents have a heartbeat at all? Reviewers and designers are
    event-driven; the strongest general case for a tick is (2) above. Worth confirming
    before building a mechanism because openclaw had one.
  4. Custom heartbeats: defer. No evidence yet for what people would write. Ship the
    default, learn from real use, let two or three examples shape the format.

Not in scope

Per-agent cadence already works (AgentInstallation.config.heartbeat.everyMinutes,
1–1440, default 60). Nothing here needs a scheduler change.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions