openclaw - ✅(Solved) Fix [Bug]: Empty HEARTBEAT.md still costs $20+/week due to full session history replay [1 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#61690Fetched 2026-04-08 02:55:50
View on GitHub
Comments
0
Participants
1
Timeline
2
Reactions
0
Author
Participants
Timeline (top)
labeled ×2

Version: 2026.4.2

Root Cause

Version: 2026.4.2

PR fix notes

PR #63434: fix(heartbeat): widen empty-detection to skip API calls for comment-only HEARTBEAT.md (#61690)

Description (problem / solution / changelog)

Summary

  • Problem: A default-installed HEARTBEAT.md could fire a full heartbeat LLM call every interval even when the file was effectively just scaffold text.
  • Root cause: docs/reference/templates/HEARTBEAT.md was changed in 198de10523 to wrap the scaffold body in a fenced ```markdown block so Mintlify would render the # lines as code instead of page headings. Runtime loadTemplate() strips frontmatter but not fenced markers, so those ``` lines are written verbatim into the user workspace. isHeartbeatContentEffectivelyEmpty() treated those marker lines as actionable content, so the preflight skip path never fired.
  • What changed: isHeartbeatContentEffectivelyEmpty() now treats Markdown fence marker lines such as ``` and ```markdown as format-only noise, while still treating real content inside the fence as actionable. The PR also adds regression coverage for the current fenced template shape, the pre-fence legacy comment-only template shape, and runner-level skip behavior.
  • What did NOT change: This PR does not add HTML comment, YAML frontmatter, or bare tasks: handling. Those patterns are not part of the shipped heartbeat template path that caused the regression.

Change Type

  • Bug fix

Scope

  • Gateway / orchestration

Linked Issue/PR

  • Closes #61690
  • Related #61347
  • This PR fixes a bug or regression

Root Cause

  • Root cause: the runtime heartbeat scaffold and the docs template share the same source file. After 198de10523, the shipped HEARTBEAT.md scaffold started including fenced Markdown markers for docs rendering, but runtime template loading still wrote those markers into workspaces.
  • Missing detection / guardrail: isHeartbeatContentEffectivelyEmpty() skipped headers and blank lines but not fence marker lines, so a comment-only scaffold created from the current template looked non-empty.
  • Why old installs also matter: pre-198de10523 installs may still have the old comment-only scaffold shape, so the regression tests keep both shapes covered.

Regression Test Plan

  • Coverage level that should have caught this:
    • Unit test
    • Integration / runner test
  • Target tests or files:
    • src/auto-reply/heartbeat.test.ts
    • src/agents/workspace.test.ts
    • src/infra/heartbeat-runner.returns-default-unset.test.ts
  • Scenarios the tests lock in:
    • the current fenced default template body is treated as effectively empty
    • the legacy pre-fence comment-only template body is still treated as effectively empty
    • fenced content with a real task still runs and does not skip
    • new workspaces really do seed the fenced HEARTBEAT.md template body from docs/reference/templates/HEARTBEAT.md
  • Why this is the smallest reliable guardrail: it covers the actual docs-template-to-workspace path plus the runner preflight gate, not just the helper in isolation.

User-visible / Behavior Changes

For users whose HEARTBEAT.md still contains only the shipped scaffold text, heartbeat ticks now return {status: "skipped", reason: "empty-heartbeat-file"} again instead of making an unnecessary LLM call. Fenced templates with real task content still run normally.

Diagram

Before (default shipped scaffold after 198de10523):
[interval tick] -> workspace HEARTBEAT.md contains ```markdown / ``` lines
                -> isHeartbeatContentEffectivelyEmpty: false
                -> full LLM call

After:
[interval tick] -> fence marker lines ignored as formatting noise
                -> scaffold-only HEARTBEAT.md classified empty
                -> {status: "skipped"}

Security Impact

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Repro + Verification

Environment

  • OS: local repo test environment
  • Runtime: Node 22+, pnpm 10.x
  • Model/provider: provider-agnostic; the fix is in heartbeat preflight gating before any LLM call

Steps

  1. Inspect docs/reference/templates/HEARTBEAT.md and confirm the scaffold body is fenced with ```markdown.
  2. Confirm src/agents/workspace.ts strips frontmatter but not fence markers when seeding the workspace template.
  3. Run:
    • pnpm test src/auto-reply/heartbeat.test.ts
    • pnpm test src/agents/workspace.test.ts
    • pnpm test src/infra/heartbeat-runner.returns-default-unset.test.ts

Expected

  • Current fenced scaffold-only HEARTBEAT.md skips heartbeat runs.
  • Legacy comment-only scaffold still skips.
  • Fenced heartbeat files with real tasks still run.

Actual

Matches expected.

Evidence

  • Failing test/log before + passing after
pnpm test src/auto-reply/heartbeat.test.ts
pnpm test src/agents/workspace.test.ts
pnpm test src/infra/heartbeat-runner.returns-default-unset.test.ts

All passed locally on the final PR head.

Human Verification

  • Verified scenarios:
    • current fenced template scaffold is classified empty
    • legacy pre-fence comment-only scaffold is classified empty
    • fenced scaffold with real tasks is not classified empty
    • workspace seeding still writes the fenced heartbeat template body from the docs template source
  • What I did NOT verify: live end-to-end billing/provider behavior against a real model API. The change is confined to local preflight gating, and runner-level tests cover the skip path.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No

Risks and Mitigations

  • Risk: fence marker lines are ignored even outside the shipped scaffold.
    • Mitigation: only the marker lines themselves are ignored; content inside the fence is still evaluated normally, and runner tests cover fenced actionable content.
  • Risk: future template formatting changes could drift from runtime assumptions again.
    • Mitigation: workspace seeding and runner gating are now both covered by regression tests.

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • src/agents/workspace.test.ts (modified, +16/-0)
  • src/auto-reply/heartbeat.test.ts (modified, +36/-3)
  • src/auto-reply/heartbeat.ts (modified, +9/-3)
  • src/infra/heartbeat-runner.returns-default-unset.test.ts (modified, +66/-1)
RAW_BUFFERClick to expand / collapse

Bug type

Behavior bug (incorrect output/state without crash)

Beta release blocker

No

Summary

Version: 2026.4.2

Problem

Heartbeat polls run inside the main agent session and replay the entire conversation history on every tick — even when HEARTBEAT.md is completely empty and the only possible response is HEARTBEAT_OK.

This is pure waste with zero benefit.

Real-world cost

  • Heartbeat interval: 30 minutes
  • HEARTBEAT.md contents: empty (template comment only)
  • Session age: ~3 days
  • Input tokens per heartbeat: ~27,000
  • Model: claude-sonnet-4-6
  • Estimated cost: ~$20 USD in 3 days from heartbeats alone

The session had ~100 messages of conversation history. Every heartbeat replayed all of it just to check an empty file and reply HEARTBEAT_OK.

Expected behavior

A heartbeat with an empty HEARTBEAT.md should cost near-zero. There is no reason to send 27k tokens to ask "is there anything to do?" and receive "no."

Related

#61347 covers the system prompt side of token waste (workspace files injected unnecessarily). This issue is the conversation history side — and likely the larger cost driver in practice.

Expected behavior

It costs (almost) nothing while it's idle since heartbeat is empty

Actual behavior

Spent $20 in few days

OpenClaw version

2026.4.2

Operating system

Ubuntu 22.04

Install method

No response

Model

Sonnet 4.6

Provider / routing chain

default, anthropic API KEY

Additional provider/model setup details

No response

Logs, screenshots, and evidence

We see requests every 30 mins

Image

Impact and severity

I think its high if it impacts everyone

Additional information

No response

extent analysis

TL;DR

Optimize the heartbeat mechanism to prevent replaying the entire conversation history when the HEARTBEAT.md file is empty.

Guidance

  • Investigate the current implementation of the heartbeat poll to understand why it replays the conversation history unnecessarily.
  • Consider adding a conditional check to see if the HEARTBEAT.md file is empty before deciding whether to replay the conversation history.
  • Evaluate the cost-benefit tradeoff of reducing the heartbeat interval versus optimizing the heartbeat mechanism to minimize token waste.
  • Review related issues, such as #61347, to ensure that all aspects of token waste are addressed.

Example

# Pseudo-code example of a conditional check
if is_heartbeat_md_empty():
    # Only send a minimal request to check for updates
    send_heartbeat_ok()
else:
    # Replay conversation history as needed
    replay_conversation_history()

Notes

The provided information suggests that the issue is specific to the OpenClaw version 2026.4.2 and the Sonnet 4.6 model. However, without more details about the implementation, it is difficult to provide a comprehensive solution.

Recommendation

Apply a workaround to optimize the heartbeat mechanism, as the root cause of the issue appears to be related to the unnecessary replaying of conversation history. This can help minimize token waste and reduce costs until a more permanent fix is available.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

It costs (almost) nothing while it's idle since heartbeat is empty

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING