[编码代理技能通知因默认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();
}
}为何未被捕获
- 心跳机制主要是为内部计时设计的,而非面向用户的通知
"none"默认值确保后台心跳维护任务的静默操作- coding-agent skill 是后来添加的,未意识到此传递约束
- 当 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 check | 0 | 1 |
| Manual event | 0 | 1 |
| Background task | 0 | 1 |
⚠️ 常见陷阱
陷阱 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 fileDocker 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选项但未解释通知含义