Discord 非同步入站 Worker 計畫
目標
消除 Discord 監聽器逾時作為使用者面對的失敗模式,將入站 Discord 回合改為非同步:
- Gateway 監聽器快速接受並正規化入站事件。
- Discord 執行佇列以與目前相同的排序邊界為鍵儲存序列化的工作。
- Worker 在 Carbon 監聽器生命週期之外執行實際的代理回合。
- 執行完成後將回覆投遞回原始頻道或執行緒。
這是排隊的 Discord 執行在 channels.discord.eventQueue.listenerTimeout 逾時(但代理回合本身仍在進行)的長期修復方案。
目前狀態
此計畫已部分實作。
已完成:
- Discord 監聽器逾時和 Discord 執行逾時現在是分開的設定。
- 已接受的入站 Discord 回合入隊至
src/discord/monitor/inbound-worker.ts。 - Worker 現在掌管長時間執行的回合,而非 Carbon 監聽器。
- 既有的逐路由排序透過佇列鍵保留。
- Discord worker 路徑的逾時回歸覆蓋已存在。
白話說明:
- 正式環境的逾時 bug 已修復
- 長時間執行的回合不再僅因 Discord 監聽器預算到期而中止
- worker 架構尚未完成
仍然缺少:
DiscordInboundJob仍僅部分正規化,仍攜帶活躍的執行時參照- 指令語義(
stop、new、reset、未來的 session 控制)尚未完全 worker 原生 - worker 可觀測性和操作者狀態仍很精簡
- 尚未有重啟持久性
為何需要
目前的行為將完整的代理回合綁定在監聽器生命週期中:
src/discord/monitor/listeners.ts套用逾時和 abort 邊界。src/discord/monitor/message-handler.ts將排隊的執行保持在該邊界內。src/discord/monitor/message-handler.process.ts在行內執行媒體載入、路由、分派、打字指示、草稿串流和最終回覆投遞。
該架構有兩個糟糕的特性:
- 長時間但健康的回合可能被監聽器看門狗中止
- 使用者看不到回覆,即使下游執行時已經可以產出
提高逾時有幫助但不改變失敗模式。
非目標
- 本次不重新設計非 Discord 頻道。
- 第一次實作不將其擴展為通用的全頻道 worker 框架。
- 尚不抽取共用的跨頻道入站 worker 抽象;僅在重複明顯時共用低階原始元件。
- 第一次不加入持久的崩潰復原,除非安全落地所需。
- 本計畫不改變路由選擇、綁定語義或 ACP 策略。
目前的限制
目前的 Discord 處理路徑仍依賴一些不應留在長期工作 payload 中的活躍執行時物件:
- Carbon
Client - 原始 Discord 事件結構
- 記憶體中的公會歷史對應
- 執行緒綁定管理器回呼
- 活躍的打字指示和草稿串流狀態
我們已將執行移至 worker 佇列,但正規化邊界仍不完整。目前 worker 是「稍後在同一行程中使用部分相同的活躍物件執行」,而非完全的純資料工作邊界。
目標架構
1. 監聽器階段
DiscordMessageListener 仍為進入點,但其工作變為:
- 執行預檢和策略檢查
- 將已接受的輸入正規化為可序列化的
DiscordInboundJob - 將工作入隊至逐 session 或逐頻道的非同步佇列
- 入隊成功後立即回傳給 Carbon
監聽器不應再擁有端對端的 LLM 回合生命週期。
2. 正規化的工作 payload
引入可序列化的工作描述器,僅包含稍後執行回合所需的資料。
最小結構:
- 路由身分
agentIdsessionKeyaccountIdchannel
- 投遞身分
- 目標頻道 id
- 回覆目標訊息 id
- 執行緒 id(若有)
- 發送者身分
- 發送者 id、標籤、使用者名稱、tag
- 頻道上下文
- 公會 id
- 頻道名稱或 slug
- 執行緒中繼資料
- 解析的系統提示覆寫
- 正規化的訊息內容
- 基礎文字
- 有效訊息文字
- 附件描述器或解析的媒體參照
- 閘門決策
- 提及需求結果
- 指令授權結果
- 已綁定的 session 或代理中繼資料(若適用)
工作 payload 不得包含活躍的 Carbon 物件或可變閉包。
目前實作狀態:
- 部分完成
src/discord/monitor/inbound-job.ts已存在並定義 worker 交接- payload 仍包含活躍的 Discord 執行時上下文,應進一步精簡
3. Worker 階段
新增 Discord 專屬的 worker 執行器,負責:
- 從
DiscordInboundJob重建回合上下文 - 載入媒體和執行所需的額外頻道中繼資料
- 分派代理回合
- 投遞最終回覆 payload
- 更新狀態和診斷
建議位置:
src/discord/monitor/inbound-worker.tssrc/discord/monitor/inbound-job.ts
4. 排序模型
對給定的路由邊界,排序必須與目前等同。
建議鍵值:
- 使用與
resolveDiscordRunQueueKey(...)相同的佇列鍵邏輯
這保留了既有行為:
- 一個綁定的代理對話不會與自身交錯
- 不同 Discord 頻道仍可獨立進行
5. 逾時模型
轉換後有兩個獨立的逾時類別:
- 監聽器逾時
- 僅涵蓋正規化和入隊
- 應設定較短
- 執行逾時
- 選用、worker 擁有、明確且使用者可見
- 不應意外繼承 Carbon 監聽器設定
這消除了目前「Discord gateway 監聽器保持存活」與「代理執行健康」之間的意外耦合。
建議的實作階段
第 1 階段:正規化邊界
- 狀態:部分實作
- 已完成:
- 抽取
buildDiscordInboundJob(...) - 新增 worker 交接測試
- 抽取
- 剩餘:
- 使
DiscordInboundJob為純資料 - 將活躍的執行時依賴移至 worker 擁有的服務,而非逐工作 payload
- 停止透過將活躍監聽器 ref 重新拼回工作來重建處理上下文
- 使
第 2 階段:記憶體內 worker 佇列
- 狀態:已實作
- 已完成:
- 新增以解析的執行佇列鍵為鍵的
DiscordInboundWorkerQueue - 監聽器入隊工作而非直接等待
processDiscordMessage(...) - worker 在行程內、僅記憶體中執行工作
- 新增以解析的執行佇列鍵為鍵的
這是第一個功能性的轉換。
第 3 階段:行程分割
- 狀態:尚未開始
- 將投遞、打字指示和草稿串流的擁有權移至 worker 面向的轉接器後方。
- 以 worker 上下文重建取代直接使用活躍預檢上下文。
- 必要時暫時保留
processDiscordMessage(...)作為外觀,然後分割。
第 4 階段:指令語義
-
狀態:尚未開始 確保原生 Discord 指令在工作排隊時仍正常運作:
-
stop -
new -
reset -
任何未來的 session 控制指令
worker 佇列必須公開足夠的執行狀態,讓指令可以定位活躍或排隊的回合。
第 5 階段:可觀測性與操作者 UX
- 狀態:尚未開始
- 在監控狀態中發出佇列深度和活躍 worker 計數
- 記錄入隊時間、開始時間、完成時間和逾時或取消原因
- 在日誌中清楚呈現 worker 擁有的逾時或投遞失敗
第 6 階段:選用的持久性後續
-
狀態:尚未開始 僅在記憶體版本穩定後:
-
決定排隊的 Discord 工作是否應在 gateway 重啟後存活
-
若是,持久化工作描述器和投遞檢查點
-
若否,記載明確的記憶體邊界
除非落地需要重啟復原,否則應為獨立的後續工作。
檔案影響
目前的主要檔案:
src/discord/monitor/listeners.tssrc/discord/monitor/message-handler.tssrc/discord/monitor/message-handler.preflight.tssrc/discord/monitor/message-handler.process.tssrc/discord/monitor/status.ts
目前的 worker 檔案:
src/discord/monitor/inbound-job.tssrc/discord/monitor/inbound-worker.tssrc/discord/monitor/inbound-job.test.tssrc/discord/monitor/message-handler.queue.test.ts
可能的下一步接觸點:
src/auto-reply/dispatch.tssrc/discord/monitor/reply-delivery.tssrc/discord/monitor/thread-bindings.tssrc/discord/monitor/native-command.ts
目前的下一步
下一步是讓 worker 邊界成為真正的邊界,而非部分的。
接下來做:
- 將活躍的執行時依賴從
DiscordInboundJob移出 - 改為保留在 Discord worker 實例上
- 將排隊的工作精簡為純 Discord 專屬資料:
- 路由身分
- 投遞目標
- 發送者資訊
- 正規化的訊息快照
- 閘門和綁定決策
- 在 worker 內部從該純資料重建 worker 執行上下文
實務上這意味著:
clientthreadBindingsguildHistoriesdiscordRestFetch- 其他可變的僅執行時 handle
應停止存在於每個排隊的工作中,改為存在於 worker 本身或 worker 擁有的轉接器後方。
落地後,下一個後續工作應是 stop、new 和 reset 的指令狀態清理。
測試計畫
保留既有的逾時重現覆蓋:
src/discord/monitor/message-handler.queue.test.ts
新增測試:
- 監聽器在入隊後即回傳,無需等待完整回合
- 逐路由排序得到保留
- 不同頻道仍可併發執行
- 回覆投遞至原始訊息目的地
stop取消 worker 擁有的活躍執行- worker 失敗產生可見的診斷而不阻塞後續工作
- ACP 綁定的 Discord 頻道在 worker 執行下仍正確路由
風險與緩解
-
風險:指令語義偏離目前的同步行為 緩解:在同一轉換中落地指令狀態串接,而非之後
-
風險:回覆投遞遺失執行緒或回覆目標上下文 緩解:將投遞身分設為
DiscordInboundJob中的一級欄位 -
風險:重試或佇列重啟期間的重複傳送 緩解:第一版維持純記憶體,或在持久化前新增明確的投遞冪等性
-
風險:遷移期間
message-handler.process.ts變得更難推理 緩解:在 worker 轉換前或期間分割為正規化、執行和投遞輔助函式
驗收標準
本計畫完成的條件:
- Discord 監聽器逾時不再中止健康的長時間執行回合。
- 監聽器生命週期和代理回合生命週期在程式碼中是分開的概念。
- 既有的逐 session 排序得到保留。
- ACP 綁定的 Discord 頻道透過相同的 worker 路徑運作。
stop定位 worker 擁有的執行,而非舊有的監聽器擁有的呼叫堆疊。- 逾時和投遞失敗成為明確的 worker 結果,而非靜默的監聽器丟棄。
剩餘的落地策略
在後續 PR 中完成:
- 使
DiscordInboundJob為純資料並將活躍的執行時 ref 移至 worker - 清理
stop、new和reset的指令狀態擁有權 - 新增 worker 可觀測性和操作者狀態
- 決定是否需要持久性或明確記載記憶體邊界
若保持 Discord 專屬且繼續避免過早的跨頻道 worker 抽象,這仍是有界的後續工作。