April 23, 2026 โ€ข Version: v0.14.x - v1.x

Coding-Agent Skill Notifications Silently Dropped Due to Default heartbeat.target Configuration

Background task completion notifications from the coding-agent skill fail silently because heartbeat.target defaults to 'none', causing LLM responses to be discarded before delivery.

๐Ÿ” Symptoms

Primary Manifestation

When a background coding-agent task completes, the expected notification message never arrives in any channel (terminal, UI, or external integration).

Technical Error Output

The heartbeat fires and LLM generates a response, but delivery is silently suppressed. No error is logged to console. Debug-mode inspection reveals:

// Verbose log output (if DEBUG=openclaw:heartbeat is enabled)
[openclaw:heartbeat] Resolving delivery target for system event heartbeat
[openclaw:heartbeat] target config: "none" (default)
[openclaw:heartbeat] Delivering to: NoHeartbeatDeliveryTarget { reason: "target-none" }
[openclaw:heartbeat] Response generated but discarded - no valid delivery target

// Standard log output - nothing appears
// User sees: (silence)

Reproduction Steps

# 1. Verify default configuration
openclaw config get heartbeat.target
# Output: none

# 2. Trigger a background coding-agent task with completion notification
openclaw exec --background -- coding-agent "Run slow analysis..."

# 3. Wait for completion (task finishes successfully)
# 4. Observe: No "Done: ..." message received
# 5. Check task status
openclaw task status --last
# Output: status: "completed", notifications: []

Secondary Indicators

  • The maybeNotifyOnExit() handler for backgrounded exec processes also exhibits silent failure
  • Manual invocation of openclaw system event --text "Test" --mode now produces no visible output
  • Config inspection confirms no heartbeat.target override exists in user’s config file

๐Ÿง  Root Cause

Architectural Overview

The notification flow involves three interconnected components:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    NOTIFICATION FLOW DIAGRAM                         โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                     โ”‚
โ”‚  Coding-Agent Skill                                                 โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
โ”‚  โ”‚ openclaw system event --text "Done: ..." --mode now         โ”‚   โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
โ”‚                              โ”‚                                      โ”‚
โ”‚                              โ–ผ                                      โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
โ”‚  โ”‚ enqueueSystemEvent({ text, mode: "now" })                  โ”‚   โ”‚
โ”‚  โ”‚ Source: pi-embedded-*.js:maybeNotifyOnExit()                โ”‚   โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
โ”‚                              โ”‚                                      โ”‚
โ”‚                              โ–ผ                                      โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
โ”‚  โ”‚ requestHeartbeatNow()                                       โ”‚   โ”‚
โ”‚  โ”‚ Source: heartbeat system                                    โ”‚   โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
โ”‚                              โ”‚                                      โ”‚
โ”‚                              โ–ผ                                      โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
โ”‚  โ”‚ Heartbeat fires โ†’ LLM processes system event                โ”‚   โ”‚
โ”‚  โ”‚ Source: reply-*.js:heartbeat handler                        โ”‚   โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
โ”‚                              โ”‚                                      โ”‚
โ”‚                              โ–ผ                                      โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
โ”‚  โ”‚ resolveHeartbeatDeliveryTarget()                            โ”‚   โ”‚
โ”‚  โ”‚ Source: reply-*.js:resolveHeartbeatDeliveryTarget()        โ”‚   โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
โ”‚                              โ”‚                                      โ”‚
โ”‚         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                โ”‚
โ”‚         โ–ผ                    โ–ผ                    โ–ผ                โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”          โ”‚
โ”‚  โ”‚ target:      โ”‚    โ”‚ target:      โ”‚    โ”‚ target:      โ”‚          โ”‚
โ”‚  โ”‚ "last"       โ”‚    โ”‚ "none"       โ”‚    โ”‚ "session"    โ”‚          โ”‚
โ”‚  โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค    โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค    โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค          โ”‚
โ”‚  โ”‚ DELIVER      โ”‚    โ”‚ DISCARD      โ”‚    โ”‚ DELIVER      โ”‚          โ”‚
โ”‚  โ”‚ response     โ”‚    โ”‚ response     โ”‚    โ”‚ response     โ”‚          โ”‚
โ”‚  โ”‚ silently     โ”‚    โ”‚ silently     โ”‚    โ”‚ to session   โ”‚          โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜          โ”‚
โ”‚                                                                     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Failure Sequence

Step 1: Default Configuration The heartbeat.target configuration option defaults to "none" in src/config/defaults.ts:

// src/config/defaults.ts (line ~47)
export const defaultConfig = {
  // ...
  heartbeat: {
    target: "none",  // โ† THIS IS THE CULPRIT
    interval: 30000,
    // ...
  },
  // ...
};

Step 2: Delivery Target Resolution When resolveHeartbeatDeliveryTarget() is invoked, it reads the config:

// reply-*.js (line ~26974 in dist, or src/core/reply.ts)
function resolveHeartbeatDeliveryTarget(context) {
  const target = config.heartbeat?.target ?? "none";
  
  switch (target) {
    case "last":
      return buildLastChannelTarget(context);
    case "session":
      return buildSessionTarget(context);
    case "none":
    default:
      return buildNoHeartbeatDeliveryTarget({ reason: "target-none" });
  }
}

Step 3: Silent Discard The NoHeartbeatDeliveryTarget object instructs the delivery subsystem to:

// reply-*.js
function buildNoHeartbeatDeliveryTarget({ reason }) {
  return {
    type: "none",
    reason,
    deliver: (response) => {
      // Silently discard - no logging at INFO level
      debug(`Heartbeat response discarded: ${reason}`);
      return { delivered: false, reason };
    }
  };
}

Step 4: Affected Code Paths

Both the coding-agent skill and internal handlers share this code path:

// pi-embedded-*.js (line ~15413)
// maybeNotifyOnExit() - handles background exec process completion
function maybeNotifyOnExit(pid, exitCode, backgroundContext) {
  if (shouldNotify(exitCode, backgroundContext)) {
    enqueueSystemEvent({
      type: "process-exit",
      pid,
      exitCode,
      timestamp: Date.now(),
      sessionId: backgroundContext.sessionId
    });
    requestHeartbeatNow();
  }
}

Why This Wasn’t Caught

  1. The heartbeat mechanism was designed primarily for internal timing, not user-facing notifications
  2. The "none" default ensures silent operation for background heartbeat maintenance tasks
  3. The coding-agent skill was added later without awareness of this delivery constraint
  4. No validation warning exists when skill uses heartbeat-triggering commands without proper config

๐Ÿ› ๏ธ Step-by-Step Fix

Step 1: Check Current Configuration

# View current heartbeat configuration
openclaw config get heartbeat
# Expected output (default):
# { "target": "none", "interval": 30000 }

# Or view specific target
openclaw config get heartbeat.target
# Expected output: none

Step 2: Update Configuration

For global config (~/.config/openclaw/config.yaml):

# Before (default)
# No heartbeat.target entry (or implicit "none")

# After
heartbeat:
  target: "last"
  interval: 30000

Via CLI:

openclaw config set heartbeat.target last
# Output: Configuration updated successfully

# Verify
openclaw config get heartbeat.target
# Output: last

For project config (openclaw.yaml in workspace):

# openclaw.yaml
# Before
version: "1"

# After
version: "1"
heartbeat:
  target: "last"

Step 3: Restart OpenClaw Daemon (if running)

# For Homebrew-installed OpenClaw
brew services restart openclaw

# For npm-installed
openclaw daemon stop
openclaw daemon start

Solution B: Use Session Target (For Multi-User Setups)

If running in a multi-user or session-based environment:

# openclaw.yaml
version: "1"
heartbeat:
  target: "session"  # Delivers to originating session instead of last channel
  interval: 30000

Solution C: Modify Coding-Agent Skill (For Developers)

If you control the skill and want to avoid requiring user configuration:

# skills/coding-agent/SKILL.md
# Modify the Auto-Notify section from:

## Auto-Notify on Completion
When the agent finishes a background task, it will automatically notify via:
\`\`\`bash
openclaw system event --text "Done: {summary}" --mode now
\`\`\`

# To a mechanism that doesn't depend on heartbeat delivery:

## Auto-Notify on Completion
When the agent finishes a background task, it will automatically notify via
the message tool:

1. Use the built-in message tool to send directly to the current session
2. Format: `message(to="session", content="Done: {summary}")`
3. This bypasses heartbeat delivery entirely
\`\`\`bash
# This approach is deprecated - relies on heartbeat.target config
# openclaw system event --text "Done: {summary}" --mode now
\`\`\`

Solution D: Add Startup Validation Warning (For Framework Maintainers)

Add a check in the skill loader to warn users when required config is missing:

// src/skills/skill-loader.ts
function validateSkillRequirements(skill, config) {
  const requirements = skill.configRequirements || [];
  
  for (const req of requirements) {
    if (req.key === "heartbeat.target" && config.heartbeat?.target === "none") {
      logger.warn(
        `Skill "${skill.name}" requires heartbeat notifications but ` +
        `heartbeat.target is set to "none". Add "heartbeat.target: last" to your config.`
      );
    }
  }
}

๐Ÿงช Verification

Test 1: Configuration Applied Correctly

# Verify config is set
openclaw config get heartbeat.target
# Expected: "last"

# Verify full heartbeat config
openclaw config get heartbeat
# Expected: { "target": "last", "interval": 30000 }

Test 2: Manual System Event Delivery

# Enable debug logging (optional)
export DEBUG=openclaw:heartbeat

# Send a test system event
openclaw system event --text "Test notification" --mode now

# Expected debug output:
# [openclaw:heartbeat] Resolving delivery target for system event heartbeat
# [openclaw:heartbeat] target config: "last"
# [openclaw:heartbeat] Delivering to: LastChannelTarget { channelId: "..." }
# [openclaw:heartbeat] Response delivered successfully

# Expected visible output in terminal:
# Test notification

Test 3: Coding-Agent Background Task Notification

# Start a background task with coding-agent
openclaw exec --background -- coding-agent "sleep 2 && echo 'Analysis complete'"

# Get the task ID
TASK_ID=$(openclaw task list --json | jq -r '.[0].id')

# Wait for completion (with timeout)
timeout 30 bash -c 'while openclaw task get '$TASK_ID' --json | jq -e ".status != \"completed\"" > /dev/null; do sleep 1; done'

# Check if notification was delivered
openclaw task get $TASK_ID --json | jq '.notifications'
# Expected: [ { "type": "system-event", "delivered": true, ... } ]

# Check logs for delivery confirmation
openclaw logs --tail 50 | grep -i "heartbeat.*delivered"
# Expected: [openclaw:heartbeat] Response delivered successfully

Test 4: Integration Test Script

#!/bin/bash
# test-notification.sh - Run after applying fix

set -e

echo "=== Testing Heartbeat Notification Fix ==="

# Check config
TARGET=$(openclaw config get heartbeat.target)
if [ "$TARGET" != "last" ] && [ "$TARGET" != "session" ]; then
  echo "FAIL: heartbeat.target is '$TARGET', expected 'last' or 'session'"
  exit 1
fi
echo "PASS: heartbeat.target is '$TARGET'"

# Send test event
RESULT=$(openclaw system event --text "Test $(date +%s)" --mode now --json)
DELIVERED=$(echo "$RESULT" | jq -r '.delivered // .success // false')

if [ "$DELIVERED" = "true" ]; then
  echo "PASS: Test notification delivered successfully"
else
  echo "FAIL: Test notification was not delivered"
  echo "Raw result: $RESULT"
  exit 1
fi

echo "=== All tests passed ==="
exit 0

Expected Exit Codes

TestSuccess Exit CodeFailure Exit Code
Config check01
Manual event01
Background task01

โš ๏ธ Common Pitfalls

Pitfall 1: Config File Location Priority

OpenClaw reads config from multiple locations. Wrong location causes changes to be ignored.

# Config priority (highest to lowest):
# 1. Project config: ./openclaw.yaml
# 2. User config: ~/.config/openclaw/config.yaml  (Linux)
#                 ~/Library/Preferences/openclaw/config.yaml  (macOS)
# 3. Environment: OPENCLAW_HEARTBEAT_TARGET=last
# 4. Default: "none"

# Verify which config is active
openclaw config show --source
# Output: /Users/you/.config/openclaw/config.yaml

# Check for conflicting project config
cat ./openclaw.yaml 2>/dev/null || echo "No project config"

Pitfall 2: Docker/Container Environment Variable Override

In Docker deployments, environment variables may shadow config files.

# Wrong - env var may be set but config shows different
$ echo $OPENCLAW_HEARTBEAT_TARGET
# (empty)

$ openclaw config get heartbeat.target
# none

# The default is coming from compiled defaults, not explicit config
# Fix: Either set the env var or create a config file

Docker Compose Example:

# docker-compose.yaml - Correct approach
services:
  openclaw:
    image: openclaw/openclaw:latest
    environment:
      - OPENCLAW_HEARTBEAT_TARGET=last  # Must be set for notifications
    volumes:
      - ./openclaw.yaml:/app/openclaw.yaml:ro  # Or use config file

Pitfall 3: Daemon Not Restarted After Config Change

Config changes require daemon restart to take effect.

# WRONG: Config changed but daemon still running with old config
openclaw config set heartbeat.target last
openclaw system event --text "Test" --mode now  # Still uses old config

# CORRECT: Restart daemon
openclaw config set heartbeat.target last
openclaw daemon restart
sleep 2
openclaw system event --text "Test" --mode now  # Now uses new config

Pitfall 4: macOS Homebrew Service Not Restarted

# Homebrew-managed services require explicit restart
brew services restart openclaw

# Verify service status
brew services list | grep openclaw
# Expected: openclaw started ... /Users/.../Library/LaunchAgents/...

# Check actual running config
openclaw config show | grep -A2 heartbeat

Pitfall 5: Non-Interactive Session Delivery

When running in non-interactive mode, "last" may deliver to a different channel than expected.

# CI/CD environments or detached processes
# "last" target resolves to whatever channel was last active
# which may be stale or non-existent

# Solution: Use "session" target for predictable delivery
# Or ensure session context is explicitly passed

Pitfall 6: Conflicting Skills Overwriting Config

Some skills may programmatically set heartbeat.target to "none" for their own purposes.

# Check if any skill modifies heartbeat config
grep -r "heartbeat.target" skills/
# or
grep -r "config.set.*heartbeat" ~/.local/share/openclaw/skills/

Pitfall 7: Version Mismatch

The "last" and "session" options were added in a specific version. Using an older version silently ignores the config.

# Check OpenClaw version
openclaw --version
# v0.14.x or earlier: "last"/"session" may not be available
# v0.15.x+: Full heartbeat target options supported

# Upgrade if needed
npm update -g openclaw
# or
brew upgrade openclaw

Connected Error Codes and Historical Issues

  • HEARTBEAT_NO_TARGET โ€” Internal error when heartbeat fires but no delivery target exists. Manifests as silent failure in default configurations.
  • ENQUEUESYSTEM_EVENT_DROPPED โ€” Occurs when system events are enqueued during daemon shutdown or when heartbeat system is disabled.
  • BACKGROUND_EXEC_NOTIFY_FAILED โ€” Related to maybeNotifyOnExit() failing to deliver completion notifications. Shares the same root cause as this issue.
  • SKILL_NOTIFICATION_TIMEOUT โ€” Agents waiting for completion notifications may time out if heartbeat.target is "none", causing skill execution to appear hung.
  • Issue #1847 โ€” "Background task notifications not working in v0.14.2" โ€” Original report of this class of issue.
  • Issue #2103 โ€” "maybeNotifyOnExit silently fails when heartbeat.target is none" โ€” Confirmed upstream.
  • Issue #2256 โ€” "Documentation does not mention heartbeat.target default value" โ€” Documentation gap tracking issue.
  • Issue #2389 โ€” "Coding-agent skill unusable without manual config" โ€” Feature request to fix default behavior.
# These related options may also affect notification behavior
heartbeat:
  target: "last"      # Required for notifications (the fix)
  interval: 30000     # How often heartbeat fires passively
  mode: "auto"        # When "manual", only explicit requests fire
  suppressOnIdle: true # May suppress notifications when no activity

system:
  eventBufferSize: 100  # Events dropped if buffer full and daemon busy

Documentation References

  • docs/gateway/heartbeat.md โ€” Documents heartbeat architecture but omits the "none" default impact on notifications
  • skills/coding-agent/SKILL.md โ€” References openclaw system event command without noting the config requirement
  • docs/config/reference.md โ€” Lists heartbeat.target options but doesn't explain notification implications

Evidence & Sources

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