openclaw - 💡(How to fix) Fix Heartbeat sessions receive empty exec completion notifications due to sessionKey mismatch [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#69626Fetched 2026-04-22 07:49:58
View on GitHub
Comments
0
Participants
1
Timeline
0
Reactions
0
Author
Participants

Heartbeat sessions frequently receive exec completion notifications without the actual command output, causing the model to respond with confusion messages like:

I don't see any system messages or command output in the current context. 
This appears to be a completion notification without attached results.
Could you share what command was run, or if there's output I should be seeing?

This is caused by a sessionKey mismatch in the event routing logic between parent sessions and isolated heartbeat sessions.

Error Message

Example session showing the issue:

{
  "type": "message",
  "message": {
    "role": "user",
    "content": [
      {
        "type": "text",
        "text": "An async command you ran earlier has completed. The result is shown in the system messages above. Please relay the command output to the user in a helpful way."
      }
    ]
  }
}

But peekSystemEventEntries(heartbeatSessionKey) returns [].


Would appreciate any guidance on the preferred fix approach! Happy to submit a PR if you can point me to the right direction.

Root Cause

This is caused by a sessionKey mismatch in the event routing logic between parent sessions and isolated heartbeat sessions.

Fix Action

Workaround

Added exception handling in HEARTBEAT.md:

Code Example

I don't see any system messages or command output in the current context. 
This appears to be a completion notification without attached results.
Could you share what command was run, or if there's output I should be seeing?

---

exec background task completes
enqueueSystemEvent(text, { sessionKey: "agent:main:main" })
requestHeartbeatNow({ sessionKey: "agent:main:main", reason: "exec-event" })
runHeartbeat(isolatedSessionKey: "agent:main:main:heartbeat")
peekSystemEventEntries("agent:main:main:heartbeat")[]
hasExecCompletion detects event (from parent session?)
buildExecEventPrompt()"An async command you ran earlier has completed..."
Model: "I don't see any system messages..."

---

enqueueSystemEvent(output ? `Exec ${status} (${session.id.slice(0, 8)}, ${exitLabel}) :: ${output}` : ..., {
     sessionKey,  // This is the parent session key
     deliveryContext: session.notifyDeliveryContext,
     trusted: false
   });
   requestHeartbeatNow(scopedHeartbeatWakeOptions(sessionKey, { reason: "exec-event" }));

---

const pendingEventEntries = peekSystemEventEntries(session.sessionKey);
   // session.sessionKey is the isolated heartbeat session key
   const hasExecCompletion = pendingEvents.some(isExecCompletionEvent);

---

function peekSystemEventEntries(sessionKey) {
     return getSessionQueue(sessionKey)?.queue.map(cloneSystemEvent) ?? [];
   }

---

function requestHeartbeatNow(options) {
  const baseSessionKey = options.sessionKey;
  const heartbeatSessionKey = `${baseSessionKey}:heartbeat`;
  
  // Copy events to heartbeat session
  const events = peekSystemEventEntries(baseSessionKey);
  for (const event of events) {
    enqueueSystemEvent(event.text, {
      sessionKey: heartbeatSessionKey,
      deliveryContext: event.deliveryContext,
      trusted: event.trusted
    });
  }
  
  // Trigger heartbeat
  runHeartbeat(heartbeatSessionKey);
}

---

function runHeartbeat(isolatedSessionKey) {
  const baseSessionKey = isolatedSessionKey.replace(/:heartbeat$/, '');
  
  // Check events from parent session
  const pendingEventEntries = peekSystemEventEntries(baseSessionKey);
  
  if (pendingEventEntries.length > 0) {
    // Inject events into current context
    injectSystemMessages(pendingEventEntries);
  }
}

---

const pendingEventEntries = peekSystemEventEntries(session.sessionKey);
const hasExecCompletion = pendingEventEntries.some(isExecCompletionEvent);

// Only build prompt if current session actually has events
if (hasExecCompletion && pendingEventEntries.length > 0) {
  prompt = buildExecEventPrompt();
}

---

### Exception Handling
- **When receiving "An async command you ran earlier has completed" but no system messages in context**:
  - This is a known OpenClaw bug (sessionKey mismatch in event routing)
  - Reply with `HEARTBEAT_OK` directly
  - Do not attempt to explain, ask, or relay non-existent output
  - Do not create noise

---

{
  "type": "message",
  "message": {
    "role": "user",
    "content": [
      {
        "type": "text",
        "text": "An async command you ran earlier has completed. The result is shown in the system messages above. Please relay the command output to the user in a helpful way."
      }
    ]
  }
}
RAW_BUFFERClick to expand / collapse

Heartbeat sessions receive empty exec completion notifications due to sessionKey mismatch

Description

Heartbeat sessions frequently receive exec completion notifications without the actual command output, causing the model to respond with confusion messages like:

I don't see any system messages or command output in the current context. 
This appears to be a completion notification without attached results.
Could you share what command was run, or if there's output I should be seeing?

This is caused by a sessionKey mismatch in the event routing logic between parent sessions and isolated heartbeat sessions.

Environment

  • OpenClaw Version: 2026.4.15 (041266a)
  • Node Version: v24.13.1
  • OS: Linux (WSL2)
  • Affected Sessions:
    • agent:main:main:heartbeat
    • agent:main:cron:*:heartbeat
    • agent:main:feishu:group:*:heartbeat

Reproduction Steps

  1. Start a background exec command in a main session (e.g., agent:main:main)
  2. Wait for the command to complete
  3. The system triggers a heartbeat wake with reason: "exec-event"
  4. Observe the heartbeat session (agent:main:main:heartbeat) receives the prompt but no system messages

Expected Behavior

When a background exec command completes:

  1. The completion event should be enqueued to the heartbeat session
  2. The system messages (command output) should be available in the heartbeat context
  3. The model should be able to see and relay the command output

Actual Behavior

  1. The completion event is enqueued to the parent session (agent:main:main)
  2. The heartbeat runs in an isolated session (agent:main:main:heartbeat)
  3. peekSystemEventEntries(isolatedSessionKey) returns an empty array
  4. But hasExecCompletion detects an event (possibly from parent session check)
  5. The prompt is built: "An async command you ran earlier has completed..."
  6. The model receives the prompt but finds no system messages in context
  7. The model responds with confusion

Root Cause Analysis

Call Chain

exec background task completes
enqueueSystemEvent(text, { sessionKey: "agent:main:main" })
requestHeartbeatNow({ sessionKey: "agent:main:main", reason: "exec-event" })
runHeartbeat(isolatedSessionKey: "agent:main:main:heartbeat")
peekSystemEventEntries("agent:main:main:heartbeat") → []
hasExecCompletion detects event (from parent session?)
buildExecEventPrompt() → "An async command you ran earlier has completed..."
Model: "I don't see any system messages..."

Affected Code Files

  1. bash-tools.exec-runtime-DNTWYnIA.js - maybeNotifyOnExit function

    enqueueSystemEvent(output ? `Exec ${status} (${session.id.slice(0, 8)}, ${exitLabel}) :: ${output}` : ..., {
      sessionKey,  // This is the parent session key
      deliveryContext: session.notifyDeliveryContext,
      trusted: false
    });
    requestHeartbeatNow(scopedHeartbeatWakeOptions(sessionKey, { reason: "exec-event" }));
  2. heartbeat-runner-CInrztfM.js - resolveHeartbeatRunPrompt function

    const pendingEventEntries = peekSystemEventEntries(session.sessionKey);
    // session.sessionKey is the isolated heartbeat session key
    const hasExecCompletion = pendingEvents.some(isExecCompletionEvent);
  3. system-events-Dq_M0n12.js - Event queue management

    function peekSystemEventEntries(sessionKey) {
      return getSessionQueue(sessionKey)?.queue.map(cloneSystemEvent) ?? [];
    }

Impact

  • Frequency: Observed 6 times in production
  • Affected Sessions: 29 sessions
  • User Experience: Creates noise and confusion in heartbeat responses
  • Severity: Medium (workaround available, but degrades heartbeat quality)

Proposed Solutions

Solution 1: Fix Event Routing (Recommended)

Copy events from parent session to heartbeat session when triggering heartbeat:

function requestHeartbeatNow(options) {
  const baseSessionKey = options.sessionKey;
  const heartbeatSessionKey = `${baseSessionKey}:heartbeat`;
  
  // Copy events to heartbeat session
  const events = peekSystemEventEntries(baseSessionKey);
  for (const event of events) {
    enqueueSystemEvent(event.text, {
      sessionKey: heartbeatSessionKey,
      deliveryContext: event.deliveryContext,
      trusted: event.trusted
    });
  }
  
  // Trigger heartbeat
  runHeartbeat(heartbeatSessionKey);
}

Solution 2: Fix Event Check Logic

Check parent session's event queue in heartbeat:

function runHeartbeat(isolatedSessionKey) {
  const baseSessionKey = isolatedSessionKey.replace(/:heartbeat$/, '');
  
  // Check events from parent session
  const pendingEventEntries = peekSystemEventEntries(baseSessionKey);
  
  if (pendingEventEntries.length > 0) {
    // Inject events into current context
    injectSystemMessages(pendingEventEntries);
  }
}

Solution 3: Fix Prompt Building Logic

Verify events exist in current session before building prompt:

const pendingEventEntries = peekSystemEventEntries(session.sessionKey);
const hasExecCompletion = pendingEventEntries.some(isExecCompletionEvent);

// Only build prompt if current session actually has events
if (hasExecCompletion && pendingEventEntries.length > 0) {
  prompt = buildExecEventPrompt();
}

Workaround

Added exception handling in HEARTBEAT.md:

### Exception Handling
- **When receiving "An async command you ran earlier has completed" but no system messages in context**:
  - This is a known OpenClaw bug (sessionKey mismatch in event routing)
  - Reply with `HEARTBEAT_OK` directly
  - Do not attempt to explain, ask, or relay non-existent output
  - Do not create noise

This prevents the model from generating confusion messages, but doesn't fix the underlying routing issue.

Additional Context

  • This issue only affects isolated heartbeat sessions (:heartbeat suffix)
  • Parent sessions receive and process exec completion events correctly
  • The issue is deterministic and reproducible with background exec commands
  • No data loss occurs; events remain in parent session queue

Related Files

  • dist/bash-tools.exec-runtime-DNTWYnIA.js
  • dist/heartbeat-runner-CInrztfM.js
  • dist/system-events-Dq_M0n12.js

Logs

Example session showing the issue:

{
  "type": "message",
  "message": {
    "role": "user",
    "content": [
      {
        "type": "text",
        "text": "An async command you ran earlier has completed. The result is shown in the system messages above. Please relay the command output to the user in a helpful way."
      }
    ]
  }
}

But peekSystemEventEntries(heartbeatSessionKey) returns [].


Would appreciate any guidance on the preferred fix approach! Happy to submit a PR if you can point me to the right direction.

extent analysis

TL;DR

The most likely fix for the sessionKey mismatch issue in event routing between parent sessions and isolated heartbeat sessions is to copy events from the parent session to the heartbeat session when triggering a heartbeat.

Guidance

  1. Verify the sessionKey mismatch: Confirm that the issue is indeed caused by the sessionKey mismatch by checking the sessionKey used in enqueueSystemEvent and peekSystemEventEntries functions.
  2. Choose a fix approach: Decide on one of the proposed solutions: Fix Event Routing, Fix Event Check Logic, or Fix Prompt Building Logic, considering the trade-offs and implications of each approach.
  3. Test the fix: Implement the chosen fix and test it thoroughly to ensure that the sessionKey mismatch is resolved and the heartbeat sessions receive the correct events.
  4. Monitor the fix: After deploying the fix, monitor the system to ensure that the issue is resolved and no new problems are introduced.

Example

The proposed Solution 1: Fix Event Routing provides a code example that demonstrates how to copy events from the parent session to the heartbeat session:

function requestHeartbeatNow(options) {
  const baseSessionKey = options.sessionKey;
  const heartbeatSessionKey = `${baseSessionKey}:heartbeat`;
  
  // Copy events to heartbeat session
  const events = peekSystemEventEntries(baseSessionKey);
  for (const event of events) {
    enqueueSystemEvent(event.text, {
      sessionKey: heartbeatSessionKey,
      deliveryContext: event.deliveryContext,
      trusted: event.trusted
    });
  }
  
  // Trigger heartbeat
  runHeartbeat(heartbeatSessionKey);
}

Notes

The choice of fix approach depends on the specific requirements and constraints of the system. It is essential to carefully evaluate the trade-offs and implications of each approach before implementing a fix.

Recommendation

Apply Solution 1: Fix Event Routing, as it directly addresses the sessionKey mismatch issue and ensures that the heartbeat sessions receive the correct events. This approach is straightforward to implement and test, making it a preferred choice for resolving the issue.

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…

Still need to ship something?

×6

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

Back to top recommendations

TRENDING

openclaw - 💡(How to fix) Fix Heartbeat sessions receive empty exec completion notifications due to sessionKey mismatch [1 participants]