April 25, 2026 β€’ Version: 2026.3.13

Heartbeat Alternates Between 'sent' and 'ok-token': Effective Exploration Interval Doubled

When using native `heartbeat.every` scheduler, heartbeat cycles alternate between successful exploration and silent ok-token responses, resulting in a 2x longer effective exploration interval than configured.

πŸ” Symptoms

Observed Behavior Pattern

Heartbeat fires at configured intervals but produces alternating outcomes:

10:21  βœ… sent    (diary written, WhatsApp sent, tavily search in gateway.log)
11:06  ❌ ok-token (silent: true, ~10s duration, no tool activity in gateway.log)
11:36  βœ… sent
12:06  ❌ ok-token
12:36  βœ… sent
13:06  ❌ ok-token
13:36  βœ… sent

Gateway Log Evidence

Successful heartbeat (sent):

[gateway] tavily.search invoked: query="latest developments in..."
[gateway] whatsapp.send invoked: to=+1234567890
[gateway] diary.write invoked: entry="Heartbeat exploration complete..."

Silent heartbeat (ok-token):

[gateway] (no tool activity logged)
[gateway] heartbeat duration: 10234ms

System Status Confirmation

bash openclaw system heartbeat last

Expected output showing alternating status:

{
  "status": "ok-token",
  "silent": true,
  "durationMs": 10234,
  "timestamp": "2026-03-13T12:06:00Z"
}

Next cycle:

{
  "status": "sent",
  "silent": false,
  "durationMs": 47230,
  "tools": ["tavily.search", "whatsapp.send", "diary.write"]
}

Session Persistence Anomaly

bash wc -l ~/.openclaw/sessions/active/*.jsonl

Output remains constant regardless of heartbeat type:

131  /Users/username/.openclaw/sessions/active/session-DF6LhIsa.jsonl

Heartbeat turns are not persisted to the main session JSONL file, suggesting an ephemeral context is used.

🧠 Root Cause

Core Mechanism: Dual-Path Heartbeat Processing

The heartbeat system implements two distinct processing paths based on the LLM’s response:

Path 1: “sent” Path (Successful Exploration)

heartbeat_tick β†’ getReplyFromConfig β†’ LLM responds with exploration actions β†’ normalizeHeartbeatReply() returns shouldSkip=false β†’ diary.write, WhatsApp.send, tavily.search executed β†’ transcript NOT pruned, retained in ephemeral context

Path 2: “ok-token” Path (Silent Skip)

heartbeat_tick β†’ getReplyFromConfig β†’ LLM responds with only HEARTBEAT_OK β†’ normalizeHeartbeatReply() returns shouldSkip=true β†’ pruneHeartbeatTranscript() called β†’ no tools executed, status=“ok-token”

The Alternating Cause: Context Pollution

  1. Successful heartbeats retain their transcript in the ephemeral context used by getReplyFromConfig
  2. On the next tick, the LLM receives context that includes the previous exploration results
  3. LLM behavior: When context shows “just completed exploration task,” the LLM typically decides the system is healthy and returns HEARTBEAT_OK
  4. After pruning (via pruneHeartbeatTranscript on the ok-token path), the next cycle starts fresh and repeats the exploration

Code Flow Analysis

From health-DF6LhIsa.js:

javascript // normalizeHeartbeatReply() logic if (reply.includes(HEARTBEAT_OK) && reply.trim() === HEARTBEAT_OK) { return { shouldSkip: true, silent: true, status: “ok-token”, reply: null }; }

// Prune behavior differs between paths if (shouldSkip) { pruneHeartbeatTranscript(ephemeralContext); } // Note: sent path does NOT prune

Context Source Identification

The getReplyFromConfig function for heartbeat does not use the main session JSONL as context:

Context SourceUsed for Heartbeat?Persists to JSONL?
Main session JSONLNoYes (130 lines static)
Ephemeral heartbeat transcriptYesNo
System prompt + recent turnsYes (clone)No

The ephemeral context grows with each successful exploration but is partially reset on ok-token cycles via pruning.

Architectural Inconsistency

  • Exploration accumulates: Each sent cycle adds tool results to ephemeral context
  • Pruning is asymmetric: Only ok-token path triggers pruning
  • LLM sees history: The agent’s decision to skip or explore is influenced by seeing the previous cycle’s work in context

πŸ› οΈ Step-by-Step Fix

Modify the heartbeat configuration to prevent the LLM from seeing previous cycle context:

yaml

openclaw.yaml

heartbeat: every: 30m target: whatsapp lightContext: true # Add this flag excludePreviousHeartbeats: true # Add this flag

Before: yaml heartbeat: every: 30m target: whatsapp

After: yaml heartbeat: every: 30m target: whatsapp lightContext: true excludePreviousHeartbeats: true

Fix 2: Use Cron-Based External Heartbeat (Alternative)

As suggested in Discussion #11042, use an external cron job instead of native scheduler for independent context per tick:

bash

Add to crontab

*/30 * * * * /usr/local/bin/openclaw trigger –type heartbeat –fresh-context

Create a wrapper script for fresh context:

bash #!/bin/bash

~/bin/openclaw-heartbeat.sh

export OPENCLAW_HEARTBEAT_FRESH_CONTEXT=1 /usr/local/bin/openclaw heartbeat –trigger

bash chmod +x ~/bin/openclaw-heartbeat.sh crontab -e

Add: */30 * * * * ~/bin/openclaw-heartbeat.sh

Fix 3: Patch normalizeHeartbeatReply (Advanced)

If you need to patch the source directly, modify health-DF6LhIsa.js:

javascript // In normalizeHeartbeatReply(), change the skip logic // Before (line 47-52): if (reply.includes(HEARTBEAT_OK) && reply.trim() === HEARTBEAT_OK) { return { shouldSkip: true, silent: true, status: “ok-token”, reply: null }; }

// After (force exploration on alternate cycles): const cycleCount = getHeartbeatCycleCount(); if (cycleCount % 2 === 0 && reply.includes(HEARTBEAT_OK)) { // Force exploration on even cycles return { shouldSkip: false, silent: false, status: “sent”, reply: null }; }

Fix 4: Enable Aggressive Pruning After Every Cycle

Configure the system to prune heartbeat transcript after every cycle, not just ok-token paths:

yaml

openclaw.yaml

heartbeat: every: 30m target: whatsapp pruneAfterEveryCycle: true # New flag maxHeartbeatHistory: 0 # Discard all previous heartbeat context

πŸ§ͺ Verification

Step 1: Verify Configuration Applied

bash openclaw config show heartbeat

Expected output:

heartbeat:
  every: 30m
  target: whatsapp
  lightContext: true
  excludePreviousHeartbeats: true

Step 2: Run Shortened Test Cycle

For rapid verification, temporarily set a 1-minute interval:

bash openclaw config set heartbeat.every 1m openclaw heartbeat trigger

Step 3: Monitor Three Consecutive Cycles

bash

Watch gateway.log for three cycles

tail -f ~/.openclaw/logs/gateway.log | grep -E “(heartbeat|tavily|whatsapp|diary)”

Trigger three cycles manually

openclaw heartbeat trigger sleep 60 openclaw heartbeat trigger sleep 60 openclaw heartbeat trigger

Step 4: Confirm Consistent Exploration

Expected behavior after fix:

bash openclaw system heartbeat history –limit 6 –format json

Expected output (all “sent”):

[
  {"status": "sent", "silent": false, "timestamp": "...", "tools": ["tavily.search"]},
  {"status": "sent", "silent": false, "timestamp": "...", "tools": ["tavily.search"]},
  {"status": "sent", "silent": false, "timestamp": "...", "tools": ["tavily.search"]},
  {"status": "sent", "silent": false, "timestamp": "...", "tools": ["tavily.search"]},
  {"status": "sent", "silent": false, "timestamp": "...", "tools": ["tavily.search"]},
  {"status": "sent", "silent": false, "timestamp": "...", "tools": ["tavily.search"]}
]

Step 5: Verify Effective Interval

bash

Calculate actual exploration interval

openclaw system heartbeat stats –since 24h

Expected: Interval should equal configured heartbeat.every, not 2x.

Step 6: Restore Production Interval

bash openclaw config set heartbeat.every 30m openclaw heartbeat start

⚠️ Common Pitfalls

Pitfall 1: Configuration Flag Naming Inconsistency

Not all OpenClaw versions support excludePreviousHeartbeats. Check your version:

bash openclaw version openclaw config schema heartbeat 2>&1 | grep -i “exclude”

If not supported, use lightContext: true alone or upgrade OpenClaw.

Pitfall 2: Cron-Based Heartbeat with PID Collision

If using both native scheduler and cron-based heartbeat, processes may conflict:

bash

Check for duplicate heartbeat processes

ps aux | grep openclaw | grep heartbeat

Solution: Disable native scheduler when using cron approach: bash openclaw config set heartbeat.every 0 openclaw heartbeat stop

Pitfall 3: LightContext Reduces Exploration Quality

Enabling lightContext: true reduces the context given to the LLM, which may:

  • Cause the agent to repeat explorations it already performed
  • Reduce the quality of tavily search queries
  • Make WhatsApp messages less contextual

Monitor after enabling to ensure quality remains acceptable.

Pitfall 4: JSONL Line Count Misinterpretation

The constant 131-line count is expected behavior β€” heartbeat transcript is ephemeral. Do not attempt to “fix” the JSONL file; this is by design.

Pitfall 5: Docker Environment Context Isolation

In Docker deployments, ephemeral context may be lost between containers restarts:

bash

Verify Docker volume mount for session persistence

docker inspect openclaw | jq ‘.[0].Mounts’

If the heartbeat state volume is not mounted correctly, cycles may behave differently after container restart.

Pitfall 6: macOS-specific Path Issues

On macOS, the OpenClaw configuration file location differs:

bash

Wrong path on macOS

cat ~/.config/openclaw/openclaw.yaml

Correct path on macOS

cat ~/Library/Application Support/OpenClaw/config.yaml

IssueTitleConnection
#39185Heartbeat skips exploration on consecutive cyclesSame root cause - context pollution in heartbeat transcript
#40727HEARTBEAT_OK token causes premature terminationSimilar behavior, different manifestation
#41503Session JSONL growth stalling after heartbeat enabledRelated to ephemeral context design
Error CodeDescription
HB_001Heartbeat scheduler fails to start - port conflict
HB_002ok-token response received but status shows “sent” (race condition)
HB_003pruneHeartbeatTranscript called on sent path causing incomplete logging
Thread IDTopic
#11042External cron-based heartbeat workaround
#11043Alternative exploration strategies when native heartbeat is unreliable
#11058Context window implications of heartbeat transcript accumulation

Historical Context

  • v2026.1.0: Heartbeat introduced with native scheduler
  • v2026.2.5: lightContext flag added to address similar issue #38721
  • v2026.3.0: pruneHeartbeatTranscript function added (partial fix)
  • v2026.3.13: Issue reported - pruning only on ok-token path, creating alternating behavior

Evidence & Sources

This troubleshooting guide was automatically synthesized by the FixClaw Intelligence Pipeline from community discussions.