跳到內容

通知治理

每則推播先標級別,勿擾時段把不急的延後,每天彙整一則摘要,並量測每類通知到底有沒有人理。


DuDuClaw 的 AI 員工會主動找你:任務卡關、預算用完、高風險動作要你點頭、演化迴圈停滯、通道送不出訊息。這些訊息各自都合理,但在 v1.54 之前它們共用一個問題:沒有分級。凌晨三點的「演化事件」和凌晨三點的「任務卡住等你決定」用同一條路推出去,同樣會亮螢幕、同樣會出聲。

通知治理是擋在所有推播出口前面的那一層。它做四件事:把每則通知標上級別、在勿擾時段把不急的延後、每天彙整一則摘要、並且量測「這類通知到底有沒有人理」。


每則通知從產生到送達(或被延後)走這條路:

通知產生(NotifyLevel 必填,沒有預設值)
|
v
是 L3 嗎? --是--> 立即投遞(勿擾時段不擋 L3)
|
否
|
v
勿擾時段生效中? --否--> 立即投遞
|
是
|
v
寫入 ~/.duduclaw/notify_queue.jsonl(附檔案鎖)
|
v
時段結束後由背景排程器投遞:
- 同一收件目的地的 L1 合併成一則
- L2 決定卡單獨投遞
- 上限:最多 500 則、超過 36 小時丟棄(寫 warn 日誌)

決定卡有四種——目標停在 needs_human、待核可事項、安裝簽核、自動規則斷路器跳閘——各自有一個模組知道該怎麼措辭、該送到哪裡。措辭與目的地決定之後的每一步,現在都收斂成一個函式 notify_push::push(card, dest):

goal_notify ─┐
approval_notify ┤
install_notify ├──> notify_push::push(card, dest)
autopilot_notify┘ |
v
依 dest 解析該通道的 bot token
|
v
decision_notify::deliver_outcome(等級與勿擾時段,見上節)
|
v
channel_sender::send_with_markup / send_plain_text

dest 決定用哪一套 token,而這正是以前四份實作各自寫錯的地方:卡片送給AI 員工時,用的是該員工自己的 [channels.<ch>] token,背後還有 reports_to 往上繼承;卡片送給儀表板上的真人時,用的是部署層的私訊 bot token,並逐一嘗試到其中一支真的能觸及這個人為止。

有兩件事刻意留在原本的四個模組、沒有搬進共用入口:卡片內容(只有各自的領域知道該怎麼寫),以及誰有權按下按鈕(decision_notify::authorize_press 加上各模組自己的送達紀錄)。推播是對外的副作用,絕不該有機會放寬誰能做決定。

每個推播點都必須在程式碼裡標明級別(NotifyLevel),沒有預設值。新增一個推播出口時,你被強迫回答「這件事值不值得吵醒人」。

級別 判準 勿擾時段行為 DuDuClaw 實例
L0 沒有狀態變化 根本不進這一層 心跳、巡邏無事
L1 週知 發生了,但不需要人 延後,醒來後合併 演化事件、SOUL 整併、技能缺口摘要、每日摘要、預算恢復
L2 待確認 需要人知道並按一下 延後,醒來後單獨投遞 派工核准(任務還沒開始)、自動規則跳閘暫停
L3 須處理 又急、又重要、又可行動、又是真的 照發,不擋 自主任務等你決定、高風險審批、安裝簽核、預算停工、通道故障

判準借自 Google SRE 的告警三分法(Page / Ticket / Report)。實務上的分界線是「等到早上有沒有代價」:派工核准等到早上只是晚八小時開工;任務卡在半路等你決定,是整條自主迴圈停擺。

L3 永遠不受勿擾時段影響,這是刻意的:一個可以被設定關掉的緊急通知不叫緊急通知。


# ~/.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"

員工自己的設定優先,沒有才吃全域值。跨午夜的區間(22:00-08:00)正常運作;區間是左閉右開,所以設 08:00 結束代表 08:00 整就會投遞。

格式錯誤、空字串、start == end(例如 00:00-00:00)全部視為沒有勿擾時段,並在日誌留下 warn 記錄告訴你哪個值被忽略了。理由很直接:解析器的 bug 不該讓一個部署整晚收不到通知。設定寫錯的代價是「照常收到通知」,而非「靜悄悄什麼都收不到」。

[proactive] 底下還有一組舊的 quiet_hours_start / quiet_hours_end(數字小時),它的預設值是 23–8,而且每個員工都有。治理層刻意不讀它,否則每個既有安裝都會在沒人要求的情況下,突然被靜音一整晚。那組欄位維持原本較窄的職責:排程 [proactive] 主動檢查的時間。

被擋下的通知寫進 ~/.duduclaw/notify_queue.jsonl(附檔案鎖),時段結束後由背景排程器投遞:

  • 同一個收件目的地的 L1 通知合併成一則(🌙 勿擾時段收到 3 則通知: + 編號清單),而非三則分開的訊息。
  • 決定卡單獨投遞,因為每張卡有自己的按鈕。
  • 佇列有雙重上限:最多 500 則、超過 36 小時的直接丟棄(一天半前的「任務完成了」已經算不上新聞)。丟棄一定會寫 warn 日誌,不會無聲消失。

已知限制:在勿擾時段內被人從儀表板處理掉的決定,早上仍會投遞一張過期的卡片。按下去會被該決定的儲存層以「此決定已處理」擋掉(fail-closed),所以代價是一張多餘的卡片,不會重複執行。

agent.get RPC 的 proactive 區塊多回傳兩個欄位:

  • quiet_hours — 生效中的區間字串("22:00-08:00"),沒有勿擾時段時為 null
  • quiet_hours_note — 一句可直接渲染的繁中說明,明確寫出「哪些會延後、哪些照常送達」

會被靜默的條件必須攤在使用者看得到的地方,這是硬要求,算不上加分項。


~/.duduclaw/config.toml
[notify]
daily_digest = false # 預設關閉,需明確開啟
daily_digest_at = "09:00" # 本地時間

每天一則,推到 [general] default_agent 的 [proactive] 目的地,內容彙整過去 24 小時:

  • 完成任務數
  • 待你決定的件數(審批 + 安裝申請 + 卡在 needs_human 的任務)
  • 學習事件數(gvu_* / playbook_* / soul_* / evolution_* 活動流事件)
  • 花費
  • 通道異常告警次數
  • 行動率過低的通知類別(見下節)

全部歸零的一天,不發訊息,也不會發一則「今日無事」。一個你每天都可以不看的摘要,會訓練你連該看的那天也不看。

上限硬性一則:狀態檔記錄「最後送出的本地日期」,重啟不會補送第二則;但 09:00 時 gateway 沒開機、11:00 才起來的話,當天的摘要仍會送出一次。


員工摘要(「你不在的時候」)

Section titled “員工摘要(「你不在的時候」)”

2026-10-08 新增(尚未發行),與上面全部署的 [notify] daily_digest 分開。

~/.duduclaw/config.toml
[digest]
enabled = true # 預設開啟;設 false 關閉
hour = 8 # 當地時(0–23)
timezone = "Asia/Taipei" # IANA 時區,預設 UTC
exclude_agents = [] # 不列入的員工

每天在 timezone 的 hour 之後,持有 gateway 實例鎖(duduclaw_core::gateway_instance)的 gateway 不呼叫任何模型,組出每位員工的摘要(duduclaw-gateway/src/digest.rs):上次摘要以來(最多回溯 7 天)完成的任務與目標、動態、待核准、已結算的持續任務執行與花費。沒事可報的員工不列。摘要存成 <home>/digest/<date>.json,顯示在儀表板首頁(RPC digest.latest,只列觀看者可讀的員工與任務),並送到每位啟用中管理員已驗證的連結通道。<home>/digest/state.json 在送出前用檔案鎖認領當地日期,所以重啟或第二個 gateway 都不會重送(認領後、送出前當機則跳過當天)。預設開啟(2026-10-08 擁有者決定):沒有 [digest] 區段時於 UTC 08:00 執行。enabled = false 可關閉;config.toml 存在但無法解析,或 [digest] 任一值不對,也視為關閉。

首頁上每個完成項目都有 👍/👎/「需要修改」(可附備註)。RPC digest.feedback 需要對該員工的 Operator 並通過任務可見範圍檢查;只有 done 的任務可以回饋。回饋附加到 <home>/feedback.jsonl(演化反思的使用者回饋訊號原本就讀這個檔),type 為 positive/negative/correction,並帶 source = "deliverable"、item_kind、item_id、verdict、user_id、note。主動訊息的「被忽略」判斷會略過這些列。

檔案。 每位員工的段落也列出這段期間交付的檔案(artifacts.jsonl 帳本中對外交付的列,最新五個),每個檔案都能給同樣的回饋:digest.feedback { artifact: { agent_id, archived_name }, verdict, note? }。屬於某個任務的檔案照該任務授權(不要求 done);沒有任務的檔案需要對該員工的 Operator。回饋列的 item_kind 為 artifact,item_id 為 artifact:<員工>/<封存檔名>。不存在的檔案與被拒絕的回覆相同。

通道按鈕。 通道訊息為顯示的項目編號([1]、[2]…;每位員工最多三個完成項目與三個檔案),在 Telegram、Discord、Slack、LINE 上為前四項附上 👍/👎/✏️ 按鈕(Discord 每則訊息最多五列按鈕,LINE 最多十三個快速回覆)。按下時的授權與 digest.feedback 相同:按的人的通道身分必須對應到已驗證、啟用中的儀表板帳號(按下時重新讀取),且對該員工有 Operator;任務可見範圍必須同時允許這個帳號與這個通道;任務必須是 done。在 Telegram、Discord、Slack 上還會先通過該通道本身的存取設定。按鈕無法附備註,所以從通道按「需要修改」不帶備註,回覆會請對方到儀表板補充。通道文字中,可見範圍沒有列出該通道的任務只顯示編號、不顯示標題(channel_may_read_task)。

勿擾時段。 通道投遞是一則 L1 通知,套用全部署的 [notify] quiet_hours(摘要不屬於任何單一員工)。時段內會以 digest 種類排入 notify_queue.jsonl,時段結束後由已存的摘要重新組出並送出,不與其他通知合併;每次送出在行動率統計記為 digest.daily。未在真實通道驗證。

2026-10-08 新增(尚未發行),預設開啟(2026-10-08 擁有者決定)。

[channel_reply]
interim_status = true # 慢的那一輪送一行狀態;設 false 關閉
interim_status_secs = 8 # 0 = 關閉(最多 600)
interim_status_show_task = false # 是否寫出員工正在跑的任務名稱

外部通道(Telegram、Discord、Slack、LINE、WhatsApp、飛書、Google Chat、Teams、企業微信、釘釘;不含 WebChat 與內部工作階段)的回覆在 interim_status_secs 秒後仍什麼都沒顯示時,回覆本身的進度回呼會收到一行不經模型組出的狀態:已等多久,以及員工若另有已認領的任務看板工作,那項工作已跑多久。任務名稱只有在 interim_status_show_task = true 且任務可見範圍允許該通道讀取時才會出現,因為通道使用者可能是外部客戶。每一輪最多一次;通道沒有提供進度回呼就不送。可原地編輯的通道會把它當成進度訊息的第一個狀態,之後被答覆取代;其他通道會另發一則訊息(LINE 與 WhatsApp 會用到推播額度)。多數 CLI 回覆超過 8 秒,這類通道幾乎每輪都會多一則訊息;在意額度的話請設 interim_status = false 或調高 interim_status_secs。config.toml 無法解析或型別不對時視為關閉(duduclaw-gateway/src/channel_reply/interim.rs)。

如果這一輪員工的模型已回報正在思考,同一行改寫成「💭 已收到,正在思考(已 N 秒)。」。思考內容一律不會送到通道,通道只知道有在思考。各 runtime 要到後續版本才開始回報思考,在那之前這一行仍是「⏳ 已收到,正在處理…」。所有通道的進度都走同一份允許清單(工具動作、任務看板、仍在處理、這行狀態;WhatsApp 和以前一樣只送任務看板);工具步驟邊界、實際模型名稱、思考這類只給 dashboard 的事件不會送到任何通道。

每則通知送出時記一筆,每次有人真的把決定按掉時記一筆,存在 ~/.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%。

幾個刻意的設計:

  • 同一張卡按兩次只算一次行動(用決定 id 比對,非單純計數),所以行動率不會超過 100%。
  • 被拒絕的按壓不算行動:按了但被權限或狀態擋掉,是失敗的互動,構不成「這則通知有用」的證據。
  • 純週知類(沒有按鈕可按)會回報 actionable: 0 且永遠不會被標記 broken。說「FYI 的行動率是 0%」是同義反覆,稱不上發現。

儀表板圖表已實作,位置見「儀表板介面位置」一節。


channel_failures.jsonl 有兩項調整:

  1. channel 欄位普及:所有能從 session id 推出平台的寫入點(channel_reply_silent / channel_reply_fallback / runtime_fallback_substitution / trajectory_anomaly / foresight_alarm)都補上 channel,讓儀表板統一日誌能回答「這次失敗發生在哪個平台」。推不出平台的(cron / bus / heartbeat 這類內部 session,以及只拿得到工作目錄的 PTY fallback)寫 null 或省略,不假造歸屬。

  2. 恢復事件:通道從告警狀態回到正常時,寫一筆

    {"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;要加入新項目,該事件必須真的代表「這個通道送不出訊息」。


這幾項設定/資訊都有 RPC,但入口分散在不同頁面,直接列出:

設定/資訊 位置 說明
勿擾時段(員工自己的 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) 系統設定 →「系統」分頁 開關 + 時間欄位;全域一則,非逐員工設定。
通知成效(notify.stats 行動率) 分析報表頁 →「通知成效」卡 依「行動率量測」一節的 SRE 50% 判準列出各通知類型的推播數/行動率,行動率過低的類別長條標紅並附一句提示文字(不只靠顏色,避免色弱或黑白列印看不出來);純週知類(沒有按鈕可按)永遠不會被標記壞掉。

同一顆 [proactive] quiet_hours 有兩個編輯入口:員工編輯表單的「自動化」分頁是完整脈絡(跟通知目的地、check-in 排程放在一起);系統設定的「主動行為」分頁是給操作者不進員工詳情就能快速切換、查看多位員工勿擾時段的捷徑。兩邊寫的是同一個 agent.toml 欄位,存檔後立即互相反映,不會各自為政。


  • 設計依據:commercial/design/reviews/ux-redesign-2026-08/02-ux-methodology.md 主題 4(P4-1、P4-4、P4-5、P4-6)與 03-analogous-products.md C1/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(規則跳閘通知)