ACP永続バインディング — DiscordチャンネルとTelegramトピック
ステータス: ドラフト
概要
永続的なACPバインディングを導入し、以下をマッピングする:
- Discordチャンネル(および必要に応じて既存のスレッド)
- Telegramのグループ/スーパーグループにおけるフォーラムトピック(
chatId:topic:topicId)
長期間存続するACPセッションに紐づけ、バインディング状態はトップレベルのbindings[]エントリに明示的なバインディングタイプで格納する。
高トラフィックのメッセージングチャンネルでのACP利用を予測可能かつ永続的にし、ユーザーがcodex、claude-1、claude-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にバインディングがpersistentかtemporaryかを表示。
- バインドされた会話では、
/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はオプション。バックエンドのフォールバック順序:bindings[].acp.backendagents.list[].runtime.acp.backend- グローバル
acp.backend
mode、cwd、labelも同じオーバーライドパターン(バインディングオーバーライド → エージェントランタイムデフォルト → グローバル/デフォルト動作)。- 一時バインディングポリシー用の既存の
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は
MessageThreadIdとchatIdでトピック/スレッドコンテキストを既に伝搬。
新規/拡張コンポーネント
- Telegramバインディングアダプター(Discordアダプターと並行):
- Telegramアカウントごとにアダプターを登録、
- 正規会話IDで解決/一覧/バインド/アンバインド/タッチ。
- 型付きバインディングリゾルバー/インデックス:
bindings[]をrouteとacpビューに分割、resolveAgentRouteをrouteバインディングのみに適用、- 永続ACPインテントを
acpバインディングのみから解決。
- Telegram用インバウンドバインディング解決:
- ルート確定前にバインドされたセッションを解決(Discordは既に実施)。
- 永続バインディングリコンサイラー:
- 起動時: 設定されたトップレベルの
type: "acp"バインディングを読み込み、ACPセッションの存在を確認、バインディングの存在を確認。 - 設定変更時: デルタを安全に適用。
- 起動時: 設定されたトップレベルの
- カットオーバーモデル:
- チャンネルローカルACPバインディングフォールバックは読み取らない、
- 永続ACPバインディングはトップレベルの
bindings[].type="acp"エントリからのみ取得。
段階的な配信
フェーズ1: 型付きバインディングスキーマ基盤
- 設定スキーマを拡張して
bindings[].typeディスクリミネーターをサポート:route、acp(オプションのacpオーバーライドオブジェクト:mode、backend、cwd、label)。
- エージェントスキーマをランタイムディスクリプターで拡張し、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-1、claude-repo-x)」