通知ガバナンス
すべてのプッシュ通知にレベルを付け、クワイエットアワーで急ぎでないものを保留し、1日1通のダイジェストにまとめ、通知タイプごとに「実際に反応されているか」を計測する。
問題:すべての通知が同じ経路で届く
Section titled “問題:すべての通知が同じ経路で届く”DuDuClawのAI社員は自分からあなたに連絡してきます:タスクが行き詰まった、予算を使い切った、高リスクな操作に承認が必要、進化ループが停滞した、チャネルからメッセージを送れない。どのメッセージも単体では妥当ですが、v1.54以前は共通の問題を抱えていました——レベル分けがないことです。深夜3時の「進化イベント」と深夜3時の「タスクが判断待ちで停止中」が同じ経路でプッシュされ、同じように画面を光らせ、同じように音を鳴らしていました。
通知ガバナンスは、すべてのプッシュ出口の手前に立つレイヤーです。役割は4つ:各通知にレベルを付ける、クワイエットアワー中は急ぎでないものを保留する、1日1通のダイジェストにまとめる、そして「このタイプの通知は実際に反応されているのか」を計測することです。
すべての通知は、生成から配信(または保留)までこの経路をたどります:
通知の生成(NotifyLevel は必須、デフォルト値なし) | v L3 か? --はい--> 即時配信(クワイエットアワーは L3 を止めない) | いいえ | v クワイエットアワー中? --いいえ--> 即時配信 | はい | v ~/.duduclaw/notify_queue.jsonl に書き込み(ファイルロック付き) | v 時間帯終了後、バックグラウンドスケジューラが配信: - 同じ宛先の L1 は 1 通にマージ - L2 の決定カードは個別配信 - 上限:最大 500 件、36 時間超は破棄(warn ログを記録)4段階のエスカレーションラダー
Section titled “4段階のエスカレーションラダー”すべてのプッシュポイントはコード内でレベル(NotifyLevel)を明示しなければならず、デフォルト値はありません——新しいプッシュ出口を追加するたびに「これは人を起こす価値があるか」という問いに答えることを強制されます。
| レベル | 判定基準 | クワイエットアワー中の挙動 | DuDuClawでの例 |
|---|---|---|---|
| L0 | 状態変化なし | そもそもこのレイヤーに入らない | ハートビート、異常なしのパトロール |
| L1 周知 | 起きたが、人は不要 | 保留し、時間帯終了後にマージ | 進化イベント、SOUL統合、スキルギャップ要約、デイリーダイジェスト、予算回復 |
| L2 要確認 | 人が知って1回クリックすべき | 保留し、時間帯終了後に個別配信 | ディスパッチ承認(タスク未開始)、自動化ルールのサーキットブレーカー停止 |
| L3 要対応 | 緊急かつ重要かつ行動可能かつ本物 | そのまま配信、止めない | 自律タスクが判断待ち、高リスク承認、インストール承認、予算による稼働停止、チャネル障害 |
判定基準はGoogle SREのアラート3分類(Page / Ticket / Report)を借用しています。実務上の境界線は「朝まで待つコストがあるか」:ディスパッチ承認が朝まで待っても開始が8時間遅れるだけですが、タスクが途中で判断待ちのまま止まっていれば、自律ループ全体が停止します。
L3は決してクワイエットアワーの影響を受けません。これは意図的です——設定でオフにできる緊急通知は、緊急通知とは呼べないからです。
クワイエットアワー
Section titled “クワイエットアワー”# ~/.duduclaw/agents/<agent>/agent.toml[proactive]quiet_hours = "22:00-08:00" # 任意;未設定 = 無効timezone = "Asia/Taipei" # 任意;パース不能時はホストのシステムタイムゾーン# ~/.duduclaw/config.toml — グローバルフォールバック[notify]quiet_hours = "22:00-08:00"AI社員自身の設定が優先され、なければグローバル値が使われます。深夜をまたぐ区間(22:00-08:00)は正常に動作します。区間は右半開なので、終了を08:00に設定すると08:00ちょうどに配信が再開されます。
パース失敗は常に「無効」扱い
Section titled “パース失敗は常に「無効」扱い”不正なフォーマット、空文字列、start == end(例:00:00-00:00)はすべて「クワイエットアワーなし」と見なされ、どの値が無視されたかをwarnログに記録します。理由は単純です:パーサーのバグが、あるデプロイメントを一晩中通知が届かない状態にしてはならないからです。設定ミスの代償は「通知が通常どおり届く」ことであり、「静かに何も届かなくなる」ことではありません。
レガシーフィールドは引き継がない
Section titled “レガシーフィールドは引き継がない”[proactive]の下には古いquiet_hours_start / quiet_hours_end(数値の時刻)のペアが残っており、そのデフォルトは23–8で、しかもすべてのAI社員が持っています。ガバナンスレイヤーは意図的にこれを読みません——読んでしまうと、既存のすべてのインストールが、誰も望んでいないのに突然一晩中ミュートされてしまうからです。このペアは本来の狭い役割を維持します:[proactive]のプロアクティブチェックの実行時間帯のスケジューリングです。
保留であって破棄ではない
Section titled “保留であって破棄ではない”ブロックされた通知は~/.duduclaw/notify_queue.jsonlに書き込まれ(ファイルロック付き)、時間帯終了後にバックグラウンドスケジューラが配信します:
- 同じ宛先のL1通知は1通にマージされます(
🌙 勿擾時段收到 3 則通知:+ 番号付きリスト)。3通の別々のメッセージにはなりません。 - 決定カードは個別に配信されます。各カードに固有のボタンがあるためです。
- キューには二重の上限があります:最大500件、36時間を超えたものは破棄(1日半前の「タスクが完了しました」はもうニュースではありません)。破棄時は必ず
warnログを記録し、無音で消えることはありません。
既知の制限:クワイエットアワー中にダッシュボードから処理された決定は、朝になっても期限切れのカードとして配信されます。押しても、その決定のストレージレイヤーが「この決定は処理済み」としてブロックする(fail-closed)ため、代償は余分なカード1枚であり、二重実行は起きません。
ダッシュボードは明示する
Section titled “ダッシュボードは明示する”agent.get RPCのproactiveブロックは2つのフィールドを追加で返します:
quiet_hours— 有効な区間文字列("22:00-08:00")、クワイエットアワーがない場合はnullquiet_hours_note— そのままレンダリングできる繁体字中国語の説明文で、「何が保留され、何がそのまま届くか」を明記
何がサイレンスされるのかは、ユーザーが見える場所に開示されなければなりません。これは必須要件であり、加点項目ではありません。
デイリーダイジェスト
Section titled “デイリーダイジェスト”[notify]daily_digest = false # デフォルト無効、明示的に有効化が必要daily_digest_at = "09:00" # ローカル時刻1日1通、[general] default_agentの[proactive]宛先にプッシュされ、過去24時間をまとめます:
- 完了タスク数
- 判断待ち件数(承認 + インストール申請 +
needs_humanで止まっているタスク) - 学習イベント数(
gvu_*/playbook_*/soul_*/evolution_*アクティビティフィードイベント) - 支出
- チャネル異常アラート回数
- 行動率が低すぎる通知タイプ(次節参照)
何もない日は送らない
Section titled “何もない日は送らない”すべてがゼロの日は、メッセージを送りません——「本日異常なし」の1通すら送りません。毎日読み飛ばせるダイジェストは、読むべき日にも読み飛ばすことをあなたに学習させてしまうからです。
上限は厳格に1日1通:状態ファイルが「最後に送信したローカル日付」を記録するため、再起動しても2通目は送られません。ただし09:00にgatewayが停止していて11:00に起動した場合、その日のダイジェストは1回だけ送信されます。
行動率の計測
Section titled “行動率の計測”通知を送信するたびに1件、誰かが実際に決定を処理するたびに1件記録し、~/.duduclaw/notify_events.jsonlに保存します。
duduclaw dashboard → notify.stats RPC{ "days": 30, "broken_threshold": 0.5, "min_sample": 10, "types": [ { "type": "decision.install", "pushed": 12, "actionable": 12, "acted": 5, "action_rate": 0.42, "broken": true }, { "type": "decision.goal", "pushed": 12, "actionable": 12, "acted": 12, "action_rate": 1.0, "broken": false } ]}brokenはGoogle SREの判定基準をそのまま採用しています:精度が50%を下回るアラートは壊れたアラート。条件は「押せる」プッシュが10件以上、かつ行動率が50%未満であることです。
意図的な設計がいくつかあります:
- 同じカードを2回押しても行動は1回(決定idで照合し、単純カウントはしない)。したがって行動率が100%を超えることはありません。
- 拒否された押下は行動に数えません——押したが権限や状態にブロックされたのは失敗したインタラクションであり、「この通知は有用だった」証拠にはなりません。
- 純粋な周知タイプ(押すボタンがない)は
actionable: 0を報告し、決してbrokenとマークされません。「FYIの行動率は0%」はトートロジーであり、発見ではありません。
ダッシュボードのチャートは実装済みです。場所は「ダッシュボードの入口」の節を参照してください。
チャネル障害レコードのスキーマ
Section titled “チャネル障害レコードのスキーマ”channel_failures.jsonlには2つの調整が入りました:
-
channelフィールドの普及:session idからプラットフォームを導出できるすべての書き込みポイント(channel_reply_silent/channel_reply_fallback/runtime_fallback_substitution/trajectory_anomaly/foresight_alarm)がchannelを記録するようになり、ダッシュボードの統合ログが「この失敗はどのプラットフォームで起きたか」に答えられるようになりました。プラットフォームを導出できないもの(cron / bus / heartbeatなどの内部session、および作業ディレクトリしか分からないPTY fallback)はnullを書くか省略します——帰属を捏造しません。 -
回復イベント:チャネルがアラート状態から正常に戻ったとき、1件書き込みます
{"event":"channel_recovered","channel":"telegram","reason":"recovered","resolved":true,"resolves":"telegram_send_failed","timestamp":"…"}古い失敗行は書き換えられません(append-onlyの監査ファイル)。コンシューマー側は「同じchannelにより新しい
channel_recoveredがあるか」でその障害がまだ有効かを判断します。
⚠️ 付随する挙動修正:チャネル障害アラートの判定条件が「channelフィールドがある」から「eventが送信失敗の許可リストに含まれ、かつchannelフィールドがある」に変わりました。このステップがなければ、1点目の変更によってLLMタイムアウトや軌跡異常のすべてがチャネル断としてアラートされてしまいます。許可リストは現在telegram_send_failedのみ。新しい項目を追加するには、そのイベントが本当に「このチャネルからメッセージを送れない」ことを意味していなければなりません。
ダッシュボードの入口
Section titled “ダッシュボードの入口”これらの設定・情報にはすべてRPCがありますが、入口は複数のページに分散しています:
| 設定・情報 | 場所 | 説明 |
|---|---|---|
クワイエットアワー(AI社員自身のquiet_hours、フルコンテキスト) |
AI社員一覧 → 社員を選択 → 編集(/agents/:id/edit)→「自動化」タブ |
詳細ブロックにHH:MM-HH:MM入力欄があり、フォーマットエラーはその場で赤くマークされます。その下に、この社員の現在有効な説明文(quiet_hours_note)がリアルタイム表示されます。 |
| クワイエットアワー(社員をまたいだ素早い切り替え) | システム設定 →「主動行為」(プロアクティブ動作)タブ | 同じ[proactive] quiet_hoursで、ページ上部の社員ドロップダウンで切り替え可能。新フォーマットのフィールドが空で、レガシーの数値式quiet_hours_start/quiet_hours_endがデフォルト以外に変更されていた場合(オペレーターが過去にこのページでクワイエットアワーを設定していた証拠)、変換プロンプトが表示され新フォーマットに自動プレフィルされます。保存して初めて有効になり、「クワイエットアワーを変えたつもりが、ガバナンスレイヤーからは全く見えていなかった」というサイレントな罠を回避します(W2-9)。 |
デイリーダイジェストのスイッチ([notify] daily_digest / daily_digest_at) |
システム設定 →「系統」(システム)タブ | スイッチ + 時刻フィールド。グローバルで1通、社員ごとの設定ではありません。 |
通知効果(notify.stats行動率) |
分析レポートページ →「通知成效」(通知効果)カード | 「行動率の計測」の節のSRE 50%基準に基づき、通知タイプごとのプッシュ数/行動率を一覧表示。行動率が低すぎるタイプはバーが赤くなり、ヒント文が1行付きます(色だけに頼らず、色覚多様性や白黒印刷でも判別可能)。純粋な周知タイプ(押すボタンがない)は決して壊れているとマークされません。 |
同じ[proactive] quiet_hoursに編集入口が2つあります:社員編集フォームの「自動化」タブはフルコンテキスト(通知宛先やcheck-inスケジュールと並んでいる)で、システム設定の「主動行為」タブは社員詳細を開かずに複数の社員のクワイエットアワーを素早く確認・切り替えするためのショートカットです。どちらも同じagent.tomlフィールドに書き込み、保存すれば即座に相互反映され、食い違いは起きません。
- 設計根拠:
commercial/docs/ux-redesign-2026-08/02-ux-methodology.mdテーマ4(P4-1、P4-4、P4-5、P4-6)および03-analogous-products.mdC1/C7/C8/C12 - コード:
crates/duduclaw-gateway/src/notify_governance.rs、notify_stats.rs、notify_digest.rs - フロントエンド:
web/src/pages/agent-form/EditAgentPage.tsx(自動化タブ)、web/src/components/settings/sections/ProactiveTab.tsx、SystemTab.tsx、web/src/pages/ReportPage.tsx - 関連機能:34-goal-loop.md(needs_human決定カード)、23-autopilot-engine.md(ルールのサーキットブレーカー通知)