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
- 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.
- 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.
- 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.
- 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.
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.contentfromschedulerService) ends with:No wrapper agent has that file:
Delivery counts from the live session stores:
pod-architecthas received theinstruction 207 times,
sprint-review238 times. Each one is a realsubprocess spawn that executes an instruction with nothing behind it. The agents
cope (they return
HEARTBEAT_OK), so this has been silently wasteful rather thanvisibly broken.
Root cause — a kernel/driver contract that only one driver implements
HEARTBEAT.mdis an openclaw convention:registry.jsowns the templates andthe provisioner writes the file onto the agent's PVC, so the instruction resolves
there. Nothing in
commonly agent attachprovisions it, so on the CAP path theprompt is a pointer that outlives its target.
The dispatch half is already driver-agnostic and works correctly —
schedulerServicecron → per-agent
config.heartbeat.everyMinutes(default 60, sha256 stagger) →AgentEvent{type:'heartbeat'}→ any CAP poller. It is the content half that wasnever 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.
heartbeatas a declaration alongside the existing four.mountSkillsalreadyseeds workspace files at spawn, so the mechanism exists).
must not end up as a competing second source of heartbeat instruction.
Proposal (needs design — filing to think, not to implement)
commonly agent attachseeds 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:
chat.mention→ second response;BYO wrapper silently drops an agent's reply when another agent posts first (concurrent multi-agent rooms) #757's self-post detection will not catch it, because it is a genuine second
opinion rather than an echo
unsolicited-interjection problem fixed on 2026-07-22 by making implicit
reply-mentions human-only
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
config.heartbeat.prompt) survivesreinstall 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.
a local file. That indirection is exactly what produced this bug. Resolve to one.
event-driven; the strongest general case for a tick is (2) above. Worth confirming
before building a mechanism because openclaw had one.
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.