ACP永続バインディング — DiscordチャンネルとTelegramトピック

ステータス: ドラフト

概要

永続的なACPバインディングを導入し、以下をマッピングする:

  • Discordチャンネル(および必要に応じて既存のスレッド)
  • Telegramのグループ/スーパーグループにおけるフォーラムトピック(chatId:topic:topicId

長期間存続するACPセッションに紐づけ、バインディング状態はトップレベルのbindings[]エントリに明示的なバインディングタイプで格納する。

高トラフィックのメッセージングチャンネルでのACP利用を予測可能かつ永続的にし、ユーザーがcodexclaude-1claude-myrepoなどの専用チャンネル/トピックを作成できるようにする。

なぜ必要か

現在のスレッドバウンドACP動作は、一時的なDiscordスレッドワークフロー向けに最適化されている。Telegramには同じスレッドモデルがなく、グループ/スーパーグループのフォーラムトピックがある。ユーザーは一時的なスレッドセッションだけでなく、チャットインターフェース上に安定した常時稼働のACP「ワークスペース」を求めている。

目標

  • 以下の永続ACPバインディングをサポート:
    • Discordチャンネル/スレッド
    • Telegramフォーラムトピック(グループ/スーパーグループ)
  • バインディングのソースオブトゥルースを設定駆動にする。
  • DiscordとTelegram全体で/acp/new/reset/focus、配信動作の一貫性を維持。
  • アドホック利用のための既存の一時バインディングフローを保持。

非目標

  • ACPランタイム/セッション内部の全面的な再設計。
  • 既存の一時バインディングフローの削除。
  • 最初のイテレーションですべてのチャンネルへの展開。
  • このフェーズでのTelegramチャンネルダイレクトメッセージトピック(direct_messages_topic_id)の実装。
  • このフェーズでのTelegramプライベートチャットのトピックバリアントの実装。

UXの方向性

1) 2種類のバインディング

  • 永続バインディング: 設定に保存、起動時に調整、「名前付きワークスペース」チャンネル/トピック向け。
  • 一時バインディング: ランタイムのみ、アイドル/最大期間ポリシーにより期限切れ。

2) コマンドの動作

  • /acp spawn ... --thread here|auto|offは引き続き利用可能。
  • 明示的なバインドライフサイクルコントロールを追加:
    • /acp bind [session|agent] [--persist]
    • /acp unbind [--persist]
    • /acp statusにバインディングがpersistenttemporaryかを表示。
  • バインドされた会話では、/new/resetがバインドされたACPセッションをその場でリセットし、バインディングを維持。

3) 会話のアイデンティティ

  • 正規の会話IDを使用:
    • Discord: チャンネル/スレッドID。
    • Telegramトピック: chatId:topic:topicId
  • Telegramバインディングを単独のトピックIDだけでキーにしない。

設定モデル(案)

ルーティングと永続ACPバインディング設定をトップレベルのbindings[]に統合し、明示的なtypeディスクリミネーターを使用:

{
  "agents": {
    "list": [
      {
        "id": "main",
        "default": true,
        "workspace": "~/.openclaw/workspace-main",
        "runtime": { "type": "embedded" },
      },
      {
        "id": "codex",
        "workspace": "~/.openclaw/workspace-codex",
        "runtime": {
          "type": "acp",
          "acp": {
            "agent": "codex",
            "backend": "acpx",
            "mode": "persistent",
            "cwd": "/workspace/repo-a",
          },
        },
      },
      {
        "id": "claude",
        "workspace": "~/.openclaw/workspace-claude",
        "runtime": {
          "type": "acp",
          "acp": {
            "agent": "claude",
            "backend": "acpx",
            "mode": "persistent",
            "cwd": "/workspace/repo-b",
          },
        },
      },
    ],
  },
  "acp": {
    "enabled": true,
    "backend": "acpx",
    "allowedAgents": ["codex", "claude"],
  },
  "bindings": [
    // ルートバインディング(既存の動作)
    {
      "type": "route",
      "agentId": "main",
      "match": { "channel": "discord", "accountId": "default" },
    },
    {
      "type": "route",
      "agentId": "main",
      "match": { "channel": "telegram", "accountId": "default" },
    },
    // 永続ACP会話バインディング
    {
      "type": "acp",
      "agentId": "codex",
      "match": {
        "channel": "discord",
        "accountId": "default",
        "peer": { "kind": "channel", "id": "222222222222222222" },
      },
      "acp": {
        "label": "codex-main",
        "mode": "persistent",
        "cwd": "/workspace/repo-a",
        "backend": "acpx",
      },
    },
    {
      "type": "acp",
      "agentId": "claude",
      "match": {
        "channel": "discord",
        "accountId": "default",
        "peer": { "kind": "channel", "id": "333333333333333333" },
      },
      "acp": {
        "label": "claude-repo-b",
        "mode": "persistent",
        "cwd": "/workspace/repo-b",
      },
    },
    {
      "type": "acp",
      "agentId": "codex",
      "match": {
        "channel": "telegram",
        "accountId": "default",
        "peer": { "kind": "group", "id": "-1001234567890:topic:42" },
      },
      "acp": {
        "label": "tg-codex-42",
        "mode": "persistent",
      },
    },
  ],
  "channels": {
    "discord": {
      "guilds": {
        "111111111111111111": {
          "channels": {
            "222222222222222222": {
              "enabled": true,
              "requireMention": false,
            },
            "333333333333333333": {
              "enabled": true,
              "requireMention": false,
            },
          },
        },
      },
    },
    "telegram": {
      "groups": {
        "-1001234567890": {
          "topics": {
            "42": {
              "requireMention": false,
            },
          },
        },
      },
    },
  },
}

最小例(バインディングごとのACPオーバーライドなし)

{
  "agents": {
    "list": [
      { "id": "main", "default": true, "runtime": { "type": "embedded" } },
      {
        "id": "codex",
        "runtime": {
          "type": "acp",
          "acp": { "agent": "codex", "backend": "acpx", "mode": "persistent" },
        },
      },
      {
        "id": "claude",
        "runtime": {
          "type": "acp",
          "acp": { "agent": "claude", "backend": "acpx", "mode": "persistent" },
        },
      },
    ],
  },
  "acp": { "enabled": true, "backend": "acpx" },
  "bindings": [
    {
      "type": "route",
      "agentId": "main",
      "match": { "channel": "discord", "accountId": "default" },
    },
    {
      "type": "route",
      "agentId": "main",
      "match": { "channel": "telegram", "accountId": "default" },
    },

    {
      "type": "acp",
      "agentId": "codex",
      "match": {
        "channel": "discord",
        "accountId": "default",
        "peer": { "kind": "channel", "id": "222222222222222222" },
      },
    },
    {
      "type": "acp",
      "agentId": "claude",
      "match": {
        "channel": "discord",
        "accountId": "default",
        "peer": { "kind": "channel", "id": "333333333333333333" },
      },
    },
    {
      "type": "acp",
      "agentId": "codex",
      "match": {
        "channel": "telegram",
        "accountId": "default",
        "peer": { "kind": "group", "id": "-1009876543210:topic:5" },
      },
    },
  ],
}

注意事項:

  • bindings[].typeは明示的:
    • route: 通常のエージェントルーティング。
    • acp: マッチした会話に対する永続ACPハーネスバインディング。
  • type: "acp"の場合、match.peer.idは正規の会話キー:
    • Discordチャンネル/スレッド: 生のチャンネル/スレッドID。
    • Telegramトピック: chatId:topic:topicId
  • bindings[].acp.backendはオプション。バックエンドのフォールバック順序:
    1. bindings[].acp.backend
    2. agents.list[].runtime.acp.backend
    3. グローバル acp.backend
  • modecwdlabelも同じオーバーライドパターン(バインディングオーバーライド → エージェントランタイムデフォルト → グローバル/デフォルト動作)。
  • 一時バインディングポリシー用の既存のsession.threadBindings.*channels.discord.threadBindings.*を維持。
  • 永続エントリは望ましい状態を宣言し、ランタイムが実際のACPセッション/バインディングに調整。
  • 会話ノードごとに1つのアクティブACPバインディングが想定モデル。
  • 後方互換性: typeが欠落している場合、レガシーエントリとしてrouteと解釈。

バックエンド選択

  • ACPセッション初期化はspawn時に設定済みバックエンド選択を使用(現在のacp.backend)。
  • この提案はspawn/調整ロジックを拡張し、型付きACPバインディングオーバーライドを優先:
    • bindings[].acp.backend: 会話ローカルオーバーライド。
    • agents.list[].runtime.acp.backend: エージェントごとのデフォルト。
  • オーバーライドがない場合、現在の動作を維持(acp.backendデフォルト)。

現行システムへのアーキテクチャ適合

既存コンポーネントの再利用

  • SessionBindingServiceはチャンネル非依存の会話参照を既にサポート。
  • ACP spawn/bindフローはサービスAPIを通じたバインディングを既にサポート。
  • TelegramはMessageThreadIdchatIdでトピック/スレッドコンテキストを既に伝搬。

新規/拡張コンポーネント

  • Telegramバインディングアダプター(Discordアダプターと並行):
    • Telegramアカウントごとにアダプターを登録、
    • 正規会話IDで解決/一覧/バインド/アンバインド/タッチ。
  • 型付きバインディングリゾルバー/インデックス:
    • bindings[]routeacpビューに分割、
    • resolveAgentRouterouteバインディングのみに適用、
    • 永続ACPインテントをacpバインディングのみから解決。
  • Telegram用インバウンドバインディング解決:
    • ルート確定前にバインドされたセッションを解決(Discordは既に実施)。
  • 永続バインディングリコンサイラー:
    • 起動時: 設定されたトップレベルのtype: "acp"バインディングを読み込み、ACPセッションの存在を確認、バインディングの存在を確認。
    • 設定変更時: デルタを安全に適用。
  • カットオーバーモデル:
    • チャンネルローカルACPバインディングフォールバックは読み取らない、
    • 永続ACPバインディングはトップレベルのbindings[].type="acp"エントリからのみ取得。

段階的な配信

フェーズ1: 型付きバインディングスキーマ基盤

  • 設定スキーマを拡張してbindings[].typeディスクリミネーターをサポート:
    • route
    • acp(オプションのacpオーバーライドオブジェクト: modebackendcwdlabel)。
  • エージェントスキーマをランタイムディスクリプターで拡張し、ACPネイティブエージェントを識別(agents.list[].runtime.type)。
  • routeとACPバインディングのパーサー/インデクサー分割を追加。

フェーズ2: ランタイム解決+Discord/Telegramの同等性

  • トップレベルのtype: "acp"エントリから永続ACPバインディングを解決:
    • Discordチャンネル/スレッド、
    • Telegramフォーラムトピック(chatId:topic:topicId正規ID)。
  • TelegramバインディングアダプターとインバウンドバインドセッションオーバーライドのDiscordとの同等性を実装。
  • このフェーズではTelegramダイレクト/プライベートトピックバリアントは含まない。

フェーズ3: コマンドの同等性とリセット

  • バインドされたTelegram/Discord会話で/acp/new/reset/focusの動作を統一。
  • 設定に基づきリセットフローでバインディングが維持されることを保証。

フェーズ4: 堅牢化

  • より良い診断(/acp status、起動時の調整ログ)。
  • 競合処理とヘルスチェック。

ガードレールとポリシー

  • ACP有効化とサンドボックス制限を現行通り正確に遵守。
  • 明示的なアカウントスコーピング(accountId)でクロスアカウントの漏洩を防止。
  • 曖昧なルーティングではフェイルクローズ。
  • メンション/アクセスポリシーの動作はチャンネル設定ごとに明示的に維持。

テスト計画

  • ユニットテスト:
    • 会話ID正規化(特にTelegramトピックID)、
    • リコンサイラーの作成/更新/削除パス、
    • /acp bind --persistとアンバインドフロー。
  • インテグレーションテスト:
    • インバウンドTelegramトピック → バインドACPセッション解決、
    • インバウンドDiscordチャンネル/スレッド → 永続バインディング優先。
  • リグレッション:
    • 一時バインディングが引き続き動作、
    • バインドされていないチャンネル/トピックが現行のルーティング動作を維持。

未解決の問題

  • Telegramトピックでの/acp spawn --thread autoはデフォルトでhereにすべきか?
  • 永続バインディングはバインドされた会話でメンションゲートを常にバイパスすべきか、明示的なrequireMention=falseが必要か?
  • /focus/acp bind --persistのエイリアスとして--persistを持つべきか?

ロールアウト

  • 会話ごとのオプトインとして出荷(bindings[].type="acp"エントリの存在)。
  • まずDiscord+Telegramのみ。
  • 以下の例を含むドキュメントを追加:
    • 「1チャンネル/トピックにつき1エージェント」
    • 「異なるcwdで同じエージェントに複数チャンネル/トピック」
    • 「チーム命名パターン(codex-1claude-repo-x)」