OpenClawセッションバインディングの実装ガイド:チャネル非依存のルーティング
開発を進めていると、特定のプラットフォーム(例えば Discord)に依存したロジックがコアのコードに混ざり込み、メンテナンスが難しくなることがありますよね。特にセッション管理やメッセージのルーティングが複雑になると、予期しない挙動やエッジケースに悩まされることも少なくありません。
今回は、そうした課題を解決するために計画している「チャネルに依存しないセッションバインディング」の設計案についてご紹介します。
このドキュメントでは、長期的な Channel Agnostic(チャネルに依存しない)なセッションバインディングモデルと、次回の実装イテレーションにおける具体的なスコープを定義します。
目標:
- Subagent に紐付いたセッションルーティングをコア機能にする
- チャネル固有の挙動は Adapter 内に留める
- 通常の Discord の挙動でデグレード(先祖返り)を起こさない
なぜこれが必要なのか
Section titled “なぜこれが必要なのか”現在の挙動では、以下の要素が混在してしまっています。
- Completion のコンテンツポリシー
- 配信先のルーティングポリシー
- Discord 特有の詳細仕様
これにより、以下のようなエッジケースが発生する原因となっていました。
- 並列実行時にメインチャネルとスレッドの両方にメッセージが届いてしまう
- 再利用された Binding Manager で古いトークンが使用される
- Webhook 送信時にアクティビティのカウントが漏れる
イテレーション1のスコープ
Section titled “イテレーション1のスコープ”このイテレーションでは、範囲を意図的に限定しています。
1. チャネルに依存しないコアインターフェースの追加
Section titled “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 Completion 用のコア配信ルーターの追加
Section titled “2. Subagent Completion 用のコア配信ルーターの追加”Completion イベントのための単一の配信先解決パスを追加します。
ルーターのコントラクト:
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 を Adapter として維持
Section titled “3. Discord を Adapter として維持”Discord は引き続き最初の Adapter 実装として機能します。
Adapter の責務:
- スレッド会話の作成と再利用
- Webhook またはチャネル送信によるバインド済みメッセージの送信
- スレッドの状態(アーカイブ済み、削除済み)の検証
- Adapter メタデータのマッピング(Webhook の ID、スレッド ID)
4. 現在判明している正確性の問題の修正
Section titled “4. 現在判明している正確性の問題の修正”このイテレーションで必須となる修正:
- 既存のスレッド Binding Manager を再利用する際のトークン使用のリフレッシュ
- Webhook ベースの Discord 送信におけるアウトバウンドアクティビティの記録
- Session モードの Completion でバインドされたスレッドの配信先が選択されている場合、メインチャネルへの暗黙的なフォールバックを停止
5. 現在のランタイムセーフティのデフォルト設定を維持
Section titled “5. 現在のランタイムセーフティのデフォルト設定を維持”スレッドバインドによる Spawn が無効なユーザーに対して、挙動の変更は行いません。
デフォルト設定の維持:
channels.discord.threadBindings.spawnSubagentSessions = false
結果:
- 通常の Discord ユーザーは現在の挙動のままとなります
- 新しいコアパスは、有効化されているバインド済みセッションの Completion ルーティングにのみ影響します
イテレーション1に含まれないもの
Section titled “イテレーション1に含まれないもの”以下の項目は明示的に延期されます。
- ACP バインディングターゲット(
targetKind: "acp") - Discord 以外の新しいチャネル Adapter
- すべての配信パス(
spawn_ack、将来のsubagent_message)のグローバルな置き換え - プロトコルレベルの変更
- すべてのバインディング永続化のためのストア移行およびバージョニングの再設計
ACP に関する注意:
- インターフェース設計では ACP のための余地を残しています
- ACP の実装はこのイテレーションでは開始しません
ルーティングの不変条件
Section titled “ルーティングの不変条件”これらの不変条件は、イテレーション1において必須となります。
- 配信先の選択とコンテンツ生成は別々のステップであること
- Session モードの Completion がアクティブなバインド済み配信先に解決される場合、配信はその配信先をターゲットにしなければならない
- バインド済み配信先からメインチャネルへの隠れた再ルーティングを行わないこと
- フォールバックの挙動は明示的かつ観察可能であること
互換性とロールアウト
Section titled “互換性とロールアウト”互換性の目標:
- スレッドバインドによる Spawn がオフのユーザーにデグレードを発生させない
- このイテレーションでは Discord 以外のチャネルに変更を加えない
ロールアウト手順:
- 現在の Feature Gate の背後でインターフェースとルーターを実装する。
- Discord の Completion モードのバインド済み配信をルーター経由にする。
- バインドされていないフローについてはレガシーパスを維持する。
- ターゲットを絞ったテストとカナリア環境のランタイムログで検証する。
イテレーション1で必要なテスト
Section titled “イテレーション1で必要なテスト”以下のユニットテストおよび統合テストのカバレッジが必要です。
- Manager の再利用後、トークンのローテーションが最新のトークンを使用していること
- Webhook 送信によってチャネルのアクティビティタイムスタンプが更新されること
- 同じリクエスタチャネル内に 2 つのアクティブなバインド済みセッションがある場合、メインチャネルへの重複配信が発生しないこと
- バインド済み Session モード実行の Completion がスレッドの配信先にのみ解決されること
- Spawn フラグが無効な場合にレガシーな挙動が変更されないこと
実装予定のファイル
Section titled “実装予定のファイル”コア:
src/infra/outbound/session-binding-service.ts(新規)src/infra/outbound/bound-delivery-router.ts(新規)src/agents/subagent-announce.ts(Completion の配信先解決の統合)
Discord Adapter およびランタイム:
src/discord/monitor/thread-bindings.manager.tssrc/discord/monitor/reply-delivery.tssrc/discord/send.outbound.ts
テスト:
src/discord/monitor/provider*.test.tssrc/discord/monitor/reply-delivery.test.tssrc/agents/subagent-announce.format.test.ts
イテレーション1の完了条件
Section titled “イテレーション1の完了条件”- コアインターフェースが存在し、Completion ルーティングに組み込まれていること
- 上記の正確性の修正がテストと共にマージされていること
- Session モードのバインド済み実行において、メインとスレッドへの Completion の重複配信が発生しないこと
- バインド済み Spawn が無効なデプロイメントで挙動の変化がないこと
- ACP が明示的に延期されていること
次のステップ
Section titled “次のステップ”OpenClaw Expert
まだ解決しませんか?
このページで解決しない場合は、OpenClaw Expertに直接質問してください。