[コーディングエージェント通知がデフォルトの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のオーバーライドが存在しないことが確認されます
🧠 原因
アーキテクチャ概要
通知フローは3つの相互接続されたコンポーネントで構成されています:
┌─────────────────────────────────────────────────────────────────────┐
│ 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スキルと内部ハンドラの両方が、このコードパスを共有しています:
// 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スキルは、この配信制約を認識しないまま後から追加された
- スキルがハートビートトリガーコマンドを使用しても、適切な設定がない場合に検証警告が発せられない
🛠️ 解決手順
解決策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: 30000CLI経由:
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:セッションターゲットを使用する(マルチユーザー環境向け)
マルチユーザーまたはセッションベースの環境で実行している場合:
# openclaw.yaml
version: "1"
heartbeat:
target: "session" # Delivers to originating session instead of last channel
interval: 30000解決策C:Coding-Agentスキルを修正する(開発者向け)
スキルを制御しており、ユーザー設定が必要ないようにする場合:
# 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:起動時の検証警告を追加する(フレームワークメンテナー向け)
スキルローダーに必要な設定がない場合にユーザーに警告するチェックを追加します:
// 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サービスの再起動忘れ
# 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:競合するスキルによる設定の上書き
一部のスキルは独自の 목적으로プログラム的に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🔗 関連するエラー
関連するエラーコードと過去のIssue
HEARTBEAT_NO_TARGET— ハートビートは発火したが配信先が存在しない場合の内部エラー。デフォルト設定ではサイレント失敗として現れる。ENQUEUESYSTEM_EVENT_DROPPED— デーモンのシャットダウン中またはハートビートシステムが無効になっている場合にシステムイベントがエンキューされたときに発生。BACKGROUND_EXEC_NOTIFY_FAILED—maybeNotifyOnExit()が完了通知の配信に失敗したことに関連する。このIssueと同じ根本原因を共有している。SKILL_NOTIFICATION_TIMEOUT— 完了通知を待機しているエージェントは、heartbeat.targetが"none"の場合にタイムアウトする可能性がある。スキルの実行が止まったように見える。- Issue #1847 — "v0.14.2でのバックグラウンドタスク通知が動作しない" — このクラスのIssueの最初の報告。
- Issue #2103 — "heartbeat.targetがnoneの場合、maybeNotifyOnExitがサイレントに失敗する" — 上流で確認済み。
- Issue #2256 — "ドキュメントにheartbeat.targetのデフォルト値が記載されていない" — ドキュメントのギャップ追跡Issue。
- Issue #2389 — "手動設定なしではcoding-agentスキルが使用できない" — デフォルト動作の修正を求める機能リクエスト。
関連する設定オプション
# 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オプションを記載しているが、通知への影響についての説明がない