April 20, 2026 • 版本: v0.14.x - v1.x

[编码代理技能通知因默认heartbeat.target配置被静默丢弃] - Coding-Agent Skill Notifications Silently Dropped Due to Default heartbeat.target Configuration

来自编码代理技能的后台任务完成通知会静默失败,因为heartbeat.target默认为'none',导致LLM响应在传递前被丢弃。

🔍 症状

主要表现

当后台 coding-agent 任务完成时,预期的通知消息不会在任何渠道(终端、UI 或外部集成)中收到。

技术错误输出

心跳触发且 LLM 生成了响应,但传递被静默抑制。没有任何错误被记录到控制台。调试模式检查显示:

// 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)

复现步骤

# 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: []

次要指标

  • 后台 exec 进程的 maybeNotifyOnExit() 处理程序同样表现出静默失败
  • 手动执行 openclaw system event --text "Test" --mode now 不会产生可见输出
  • 配置检查确认用户配置文件中不存在 heartbeat.target 覆盖

🧠 根因分析

架构概述

通知流程涉及三个相互关联的组件:

┌─────────────────────────────────────────────────────────────────────┐
│                    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   │          │
│  └──────────────┘    └──────────────┘    └──────────────┘          │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

故障序列

步骤 1:默认配置 heartbeat.target 配置选项在 src/config/defaults.ts 中默认为 "none"

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

步骤 2:传递目标解析 当调用 resolveHeartbeatDeliveryTarget() 时,它读取配置:

// 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" });
  }
}

步骤 3:静默丢弃 NoHeartbeatDeliveryTarget 对象指示传递子系统:

// 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 };
    }
  };
}

步骤 4:受影响的代码路径

coding-agent skill 和内部处理程序都共享此代码路径:

// 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();
  }
}

为何未被捕获

  1. 心跳机制主要是为内部计时设计的,而非面向用户的通知
  2. "none" 默认值确保后台心跳维护任务的静默操作
  3. coding-agent skill 是后来添加的,未意识到此传递约束
  4. 当 skill 使用依赖心跳触发的命令但未进行正确配置时,没有验证警告

🛠️ 逐步修复

解决方案 A:配置 heartbeat.target(推荐给用户)

步骤 1:检查当前配置

# 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

步骤 2:更新配置

对于全局配置(~/.config/openclaw/config.yaml):

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

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

通过 CLI:

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

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

对于项目配置(工作区中的 openclaw.yaml):

# openclaw.yaml
# Before
version: "1"

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

步骤 3:重启 OpenClaw 守护进程(如果正在运行)

# For Homebrew-installed OpenClaw
brew services restart openclaw

# For npm-installed
openclaw daemon stop
openclaw daemon start

解决方案 B:使用 Session 目标(适用于多用户环境)

如果在多用户或基于会话的环境中运行:

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

解决方案 C:修改 Coding-Agent Skill(适用于开发者)

如果您控制该 skill 并希望避免要求用户进行配置:

# 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
\`\`\`

解决方案 D:添加启动验证警告(适用于框架维护者)

在 skill 加载器中添加检查,以在缺少必需配置时警告用户:

// 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.`
      );
    }
  }
}

🧪 验证

测试 1:配置已正确应用

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

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

测试 2:手动系统事件传递

# 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

测试 3:Coding-Agent 后台任务通知

# 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

测试 4:集成测试脚本

#!/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

预期退出码

测试成功退出码失败退出码
Config check01
Manual event01
Background task01

⚠️ 常见陷阱

陷阱 1:配置文件位置优先级

OpenClaw 从多个位置读取配置。错误的位置会导致更改被忽略。

# 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"

陷阱 2:Docker/容器环境变量覆盖

在 Docker 部署中,环境变量可能遮蔽配置文件。

# 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 示例:

# 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

陷阱 3:配置更改后未重启守护进程

配置更改需要重启守护进程才能生效。

# 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

陷阱 4:macOS Homebrew Service 未重启

# 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

陷阱 5:非交互式会话传递

在非交互模式下运行时,"last" 可能会传递到与预期不同的渠道。

# 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

陷阱 6:冲突的 Skill 覆盖配置

某些 skill 可能会为其自身目的以编程方式将 heartbeat.target 设置为 "none"

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

陷阱 7:版本不匹配

"last""session" 选项是在特定版本中添加的。使用旧版本会静默忽略配置。

# 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

🔗 相关错误

关联的错误代码和历史问题

  • HEARTBEAT_NO_TARGET — 当心跳触发但不存在传递目标时的内部错误。在默认配置中表现为静默失败。
  • ENQUEUESYSTEM_EVENT_DROPPED — 当系统事件在守护进程关闭期间入队或心跳系统被禁用时发生。
  • BACKGROUND_EXEC_NOTIFY_FAILED — 与 maybeNotifyOnExit() 无法传递完成通知相关。与此问题共享相同的根本原因。
  • SKILL_NOTIFICATION_TIMEOUT — 等待完成通知的代理可能会在 heartbeat.target"none" 时超时,导致 skill 执行看起来挂起。
  • Issue #1847 — "Background task notifications not working in v0.14.2" — 此类问题的原始报告。
  • Issue #2103 — "maybeNotifyOnExit silently fails when heartbeat.target is none" — 已确认的上游问题。
  • Issue #2256 — "Documentation does not mention heartbeat.target default value" — 文档缺口跟踪问题。
  • Issue #2389 — "Coding-agent skill unusable without manual config" — 修复默认行为的特性请求。

相关配置选项

# 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

文档参考

  • docs/gateway/heartbeat.md — 记录心跳架构但省略了 "none" 默认值对通知的影响
  • skills/coding-agent/SKILL.md — 引用 openclaw system event 命令但未注明配置要求
  • docs/config/reference.md — 列出 heartbeat.target 选项但未解释通知含义

依据与来源

本故障排除指南由 FixClaw 智能管线从社区讨论中自动合成。