Discord 채널 및 Telegram 토픽을 위한 ACP 영구 바인딩

상태: 초안

요약

다음을 매핑하는 영구 ACP 바인딩을 도입합니다:

  • Discord 채널(및 필요 시 기존 스레드)
  • Telegram 그룹/슈퍼그룹의 포럼 토픽(chatId:topic:topicId)

이를 장기 ACP 세션에 매핑하며, 바인딩 상태는 명시적 바인딩 타입을 사용하는 최상위 bindings[] 항목에 저장됩니다.

이를 통해 고트래픽 메시징 채널에서의 ACP 사용을 예측 가능하고 안정적으로 만들어, 사용자가 codex, claude-1, claude-myrepo 같은 전용 채널/토픽을 생성할 수 있습니다.

배경

현재 스레드 기반 ACP 동작은 일시적인 Discord 스레드 워크플로에 최적화되어 있습니다. Telegram은 동일한 스레드 모델이 없으며, 그룹/슈퍼그룹의 포럼 토픽이 있습니다. 사용자는 임시 스레드 세션뿐 아니라, 채팅 인터페이스에서 안정적이고 항시 활성화된 ACP “워크스페이스”를 원합니다.

목표

  • 다음에 대한 영구 ACP 바인딩 지원:
    • Discord 채널/스레드
    • Telegram 포럼 토픽(그룹/슈퍼그룹)
  • 바인딩 진실의 원천을 설정 기반으로 유지.
  • /acp, /new, /reset, /focus 및 전달 동작을 Discord와 Telegram 간에 일관되게 유지.
  • 임시 사용을 위한 기존 임시 바인딩 흐름 보존.

비목표

  • ACP 런타임/세션 내부의 전면 재설계.
  • 기존 임시 바인딩 흐름 제거.
  • 첫 번째 반복에서 모든 채널로 확장.
  • 이 단계에서 Telegram 채널 다이렉트 메시지 토픽(direct_messages_topic_id) 구현.
  • 이 단계에서 Telegram 비공개 채팅 토픽 변형 구현.

UX 방향

1) 두 가지 바인딩 타입

  • 영구 바인딩: 설정에 저장, 시작 시 조정, “명명된 워크스페이스” 채널/토픽용.
  • 임시 바인딩: 런타임 전용, 유휴/최대 기간 정책에 의해 만료.

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로만 키로 사용하지 않음.

설정 모델 (제안)

최상위 bindings[]에 명시적 type 구분자를 사용하여 라우팅과 영구 ACP 바인딩 설정을 통합:

{
  "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
  • mode, cwd, label은 동일한 오버라이드 패턴을 따름 (바인딩 오버라이드 -> 에이전트 런타임 기본값 -> 전역/기본 동작).
  • 임시 바인딩 정책을 위해 기존 session.threadBindings.*channels.discord.threadBindings.* 유지.
  • 영구 항목은 원하는 상태를 선언; 런타임이 실제 ACP 세션/바인딩으로 조정.
  • 대화 노드당 하나의 활성 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 오버라이드 객체 (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 및 unbind 흐름.
  • 통합:
    • 인바운드 Telegram 토픽 -> 바인딩된 ACP 세션 해석,
    • 인바운드 Discord 채널/스레드 -> 영구 바인딩 우선순위.
  • 회귀:
    • 임시 바인딩 계속 작동,
    • 바인딩되지 않은 채널/토픽은 현재 라우팅 동작 유지.

미결 질문

  • Telegram 토픽에서 /acp spawn --thread auto가 기본적으로 here여야 하는가?
  • 영구 바인딩은 바인딩된 대화에서 항상 멘션 게이팅을 우회해야 하는가, 아니면 명시적 requireMention=false가 필요한가?
  • /focus/acp bind --persist의 별칭으로 --persist를 가져야 하는가?

출시

  • 대화별 옵트인으로 출시 (bindings[].type="acp" 항목 존재).
  • Discord + Telegram만으로 시작.
  • 다음 예시와 함께 문서 추가:
    • “에이전트당 하나의 채널/토픽”
    • “다른 cwd를 가진 동일 에이전트에 여러 채널/토픽”
    • “팀 명명 패턴 (codex-1, claude-repo-x)”.