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 β
sentGateway 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: 10234msSystem 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.jsonlHeartbeat 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
- Successful heartbeats retain their transcript in the ephemeral context used by
getReplyFromConfig - On the next tick, the LLM receives context that includes the previous exploration results
- LLM behavior: When context shows “just completed exploration task,” the LLM typically decides the system is healthy and returns
HEARTBEAT_OK - After pruning (via
pruneHeartbeatTranscripton 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 Source | Used for Heartbeat? | Persists to JSONL? |
|---|---|---|
| Main session JSONL | No | Yes (130 lines static) |
| Ephemeral heartbeat transcript | Yes | No |
| System prompt + recent turns | Yes (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
sentcycle 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
Fix 1: Disable Context Inclusion in Heartbeat (Recommended)
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: trueStep 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
π Related Errors
Related GitHub Issues
| Issue | Title | Connection |
|---|---|---|
| #39185 | Heartbeat skips exploration on consecutive cycles | Same root cause - context pollution in heartbeat transcript |
| #40727 | HEARTBEAT_OK token causes premature termination | Similar behavior, different manifestation |
| #41503 | Session JSONL growth stalling after heartbeat enabled | Related to ephemeral context design |
Related Configuration Errors
| Error Code | Description |
|---|---|
HB_001 | Heartbeat scheduler fails to start - port conflict |
HB_002 | ok-token response received but status shows “sent” (race condition) |
HB_003 | pruneHeartbeatTranscript called on sent path causing incomplete logging |
Related Discussion Threads
| Thread ID | Topic |
|---|---|
| #11042 | External cron-based heartbeat workaround |
| #11043 | Alternative exploration strategies when native heartbeat is unreliable |
| #11058 | Context window implications of heartbeat transcript accumulation |
Historical Context
- v2026.1.0: Heartbeat introduced with native scheduler
- v2026.2.5:
lightContextflag added to address similar issue #38721 - v2026.3.0:
pruneHeartbeatTranscriptfunction added (partial fix) - v2026.3.13: Issue reported - pruning only on ok-token path, creating alternating behavior