Session 綁定頻道無關計畫

概覽

本文件定義長期的頻道無關 session 綁定模型,以及下一次實作迭代的具體範圍。

目標:

  • 將 subagent 綁定 session 路由設為核心能力
  • 將頻道專屬行為保留在轉接器中
  • 避免一般 Discord 行為的回歸

為何需要

目前的行為混合了:

  • 完成內容策略
  • 目的地路由策略
  • Discord 專屬細節

這導致了邊緣案例如:

  • 並行執行下的主頻道與執行緒重複投遞
  • 重用綁定管理器時的過時 token 使用
  • webhook 傳送缺少活動記帳

第一次迭代範圍

本次迭代刻意限縮。

1. 新增頻道無關的核心介面

新增核心型別和綁定與路由的服務介面。

提議的核心型別:

export type BindingTargetKind = "subagent" | "session";
export type BindingStatus = "active" | "ending" | "ended";

export type ConversationRef = {
  channel: string;
  accountId: string;
  conversationId: string;
  parentConversationId?: string;
};

export type SessionBindingRecord = {
  bindingId: string;
  targetSessionKey: string;
  targetKind: BindingTargetKind;
  conversation: ConversationRef;
  status: BindingStatus;
  boundAt: number;
  expiresAt?: number;
  metadata?: Record<string, unknown>;
};

核心服務合約:

export interface SessionBindingService {
  bind(input: {
    targetSessionKey: string;
    targetKind: BindingTargetKind;
    conversation: ConversationRef;
    metadata?: Record<string, unknown>;
    ttlMs?: number;
  }): Promise<SessionBindingRecord>;

  listBySession(targetSessionKey: string): SessionBindingRecord[];
  resolveByConversation(ref: ConversationRef): SessionBindingRecord | null;
  touch(bindingId: string, at?: number): void;
  unbind(input: {
    bindingId?: string;
    targetSessionKey?: string;
    reason: string;
  }): Promise<SessionBindingRecord[]>;
}

2. 新增 subagent 完成的核心投遞路由器

新增完成事件的單一目的地解析路徑。

路由器合約:

export interface BoundDeliveryRouter {
  resolveDestination(input: {
    eventKind: "task_completion";
    targetSessionKey: string;
    requester?: ConversationRef;
    failClosed: boolean;
  }): {
    binding: SessionBindingRecord | null;
    mode: "bound" | "fallback";
    reason: string;
  };
}

本次迭代:

  • task_completion 透過此新路徑路由
  • 其他事件類型的既有路徑保持不變

3. 保留 Discord 作為轉接器

Discord 仍為第一個轉接器實作。

轉接器職責:

  • 建立/重用執行緒對話
  • 透過 webhook 或頻道傳送綁定訊息
  • 驗證執行緒狀態(已歸檔/已刪除)
  • 對應轉接器中繼資料(webhook 身分、執行緒 id)

4. 修復目前已知的正確性問題

本次迭代必須修復:

  • 重用既有執行緒綁定管理器時重新整理 token 使用
  • 為基於 webhook 的 Discord 傳送記錄出站活動
  • 停止在已為 session 模式完成選擇綁定執行緒目的地時隱式回退至主頻道

5. 保留現行的執行時安全預設值

停用執行緒綁定 spawn 的使用者不會有行為改變。

預設值保持:

  • channels.discord.threadBindings.spawnSubagentSessions = false

結果:

  • 一般 Discord 使用者維持現行行為
  • 新的核心路徑僅影響啟用時的綁定 session 完成路由

不在第一次迭代中

明確延後:

  • ACP 綁定目標(targetKind: "acp"
  • Discord 以外的新頻道轉接器
  • 全面取代所有投遞路徑(spawn_ack、未來的 subagent_message
  • 協定層級變更
  • 所有綁定持久化的儲存遷移/版本重新設計

ACP 備註:

  • 介面設計保留 ACP 的空間
  • 本次迭代未開始 ACP 實作

路由不變量

這些不變量在第一次迭代中為必要。

  • 目的地選擇和內容生成為分開的步驟
  • 若 session 模式完成解析為活躍的綁定目的地,投遞必須以該目的地為目標
  • 不從綁定目的地隱式重路由至主頻道
  • 回退行為必須明確且可觀察

相容性與上線

相容性目標:

  • 關閉執行緒綁定 spawn 的使用者不會有回歸
  • 本次迭代不改變非 Discord 頻道

上線:

  1. 在現行功能開關後面落地介面和路由器。
  2. 透過路由器路由 Discord 完成模式的綁定投遞。
  3. 保留非綁定流程的舊有路徑。
  4. 以目標測試和金絲雀執行時日誌驗證。

第一次迭代所需的測試

必要的單元和整合覆蓋:

  • manager token 輪換在 manager 重用後使用最新 token
  • webhook 傳送更新頻道活動時間戳
  • 同一請求者頻道中的兩個活躍綁定 session 不重複投遞至主頻道
  • 綁定 session 模式執行的完成僅解析至執行緒目的地
  • 停用的 spawn 旗標保持舊有行為不變

提議的實作檔案

核心:

  • src/infra/outbound/session-binding-service.ts(新增)
  • src/infra/outbound/bound-delivery-router.ts(新增)
  • src/agents/subagent-announce.ts(完成目的地解析整合)

Discord 轉接器和執行時:

  • src/discord/monitor/thread-bindings.manager.ts
  • src/discord/monitor/reply-delivery.ts
  • src/discord/send.outbound.ts

測試:

  • src/discord/monitor/provider*.test.ts
  • src/discord/monitor/reply-delivery.test.ts
  • src/agents/subagent-announce.format.test.ts

第一次迭代的完成標準

  • 核心介面已存在並連接完成路由
  • 上述正確性修復已合併且有測試
  • session 模式綁定執行中無主頻道和執行緒的重複完成投遞
  • 停用綁定 spawn 的部署無行為改變
  • ACP 明確延後