OpenClaw ACP 채널 바인딩과 dmScope 세션 격리: 구조적 원리와 실전 적용
OpenClaw ACP에서 채널 바인딩과 세션 격리는 dmScope의 4단계 모델(main → per-peer → per-channel-peer → per-account-channel-peer)로 제어한다. 대부분의 멀티채널 환경에서는 per-channel-peer가 적절하며, AccountScopedConversationBindingManager가 계정별 격리를 보장한다. SessionBindingService의 resolveByConversation는 동일한 ConversationRef에 항상 같은 targetSessionKey를 반환해 컨텍스트 분산을 방지한다. 다중 에이전트 파이프라인에서는 수집→분석→종합 간 sessions_send를 통한 명시적 메시지 전달로 세션 연속성을 유지한다. 설정은 conversationRuntime.dmScope 필드에서 변경하며, accountScoped 옵션을 활성화하면 계정 단위 바인딩이 적용된다.
이 글의 핵심 주장과 근거
dmScope 4단계 격리 모델: 왜 이렇게 설계되었나
내가 OpenClaw 환경에서 여러 에이전트를 동시에 돌릴 때 가장 먼저 부딪힌 문제가 세션 충돌이었다. Telegram의 한 채널에서 bot A가 응답하는 도중, 같은 피어가 Discord로 넘어와 bot B에게 메시지를 보내면 컨텍스트가 뒤섞이는 현상이 발생했다. dmScope는 이 문제를 4단계 계층으로 해결한다. main 레벨은 모든 세션을 단일 컨텍스트에 묶는다. 개인용 단일 에이전트에는 가장 간단하지만, 멀티채널·멀티에이전트 환경에서는 치명적이다. per-peer는 같은 피어(사용자)의 메시지를 항상 같은 세션으로 모으는데, 채널을 구분하지 않는다. Telegram에서 보낸 메시지와 Discord에서 보낸 메시지가 같은 세션에 합쳐지는 것이다. per-channel-peer가 실질적인 멀티채널 환경의 기본값이다. '어느 채널에서'라는 정보를 피어 ID와 조합해 세션을 분리하므로, 같은 사용자가 Telegram과 Discord에서 각각 독립된 컨텍스트를 유지한다. 마지막으로 per-account-channel-peer는 계정 단위까지 격리를 추가한다. 여러 계정을 운영하는 경우, 각 계정의 채널별 세션이 완전히 분리된다. 내가 직접 테스트한 결과, per-channel-peer가 대부분의 멀티채널 자동화 시나리오에서 적절한 균형이었다. main보다 안전하고, per-account-channel-peer보다 설정이 단순하다.
SessionBindingService: 컨텍스트 분산 방지 메커니즘
에이전트 간 컨텍스트 일관성을 유지하는 핵심은 SessionBindingService의 resolveByConversation 메서드다. 이 메서드는 ConversationRef(채널 ID, 피어 ID, 계정 ID 등을 포함)를 입력받아 결정론적으로 targetSessionKey를 계산한다. 중요한 점은 '결정론적'이라는 것이다. 같은 ConversationRef가 들어오면 항상 같은 targetSessionKey가 나온다. 즉, 에이전트 A가 처리하던 대화를 에이전트 B가 이어받더라도 세션 키가 동일하므로 컨텍스트가 끊기지 않는다. 이는 여러 에이전트가 동일한 메시지를 공유하는 파이프라인에서 필수적이다. 실제로 내가 디버깅한 사례에서, SessionBindingService가 제대로 작동하지 않을 때 발생하는 증상은 명확했다. 같은 피어가 짧은 시간 간격으로 보낸 두 메시지가 완전히 다른 세션에 할당되어, 에이전트가 이전 대화 맥락을 전혀 인식하지 못하는 현상이 발생했다. resolveByConversation의 로직을 zod-schema.session.d.ts와 session-binding-service.d.ts를 대조하며 확인한 결과, dmScope 설정이 per-peer로 되어 있었는데 채널 ID가 ConversationRef에 포함되지 않아 채널 간 세션 충돌이 발생한 것이 원인이었다. 이 메커니즘은 에이전트 교체, 재시작, 스케줄링된 백그라운드 태스크 등 다양한 상황에서 컨텍스트 연속성을 보장하는 핵심 인프라다.
AccountScopedConversationBindingManager: 다중 에이전트 공존의 기반
여러 에이전트가 같은 OpenClaw 인스턴스에서 동작할 때, 계정별 세션 격리를 책임지는 것이 AccountScopedConversationBindingManager다. 이 매니저는 각 계정의 세션 바인딩 상태를 독립적으로 관리하며, 한 계정의 세션이 다른 계정의 컨텍스트에 영향을 주지 않도록 차단한다. 내가 운영 중인 환경에서는 Telegram bot 계정 3개와 Discord bot 계정 2개를 같은 OpenClaw 인스턴스에서 돌리고 있다. AccountScopedConversationBindingManager가 없다면, 모든 계정의 세션이 공유되어 메시지 라우팅이 완전히 혼란스러워진다. 이 매니저는 계정 ID를 기준으로 바인딩 테이블을 분리하므로, 각 봇은 자신의 계정 컨텍스트에서만 동작한다. 크로스에이전트 메시지 라우팅도 이 구조 위에서 가능하다. 명시적으로 sessions_send를 호출하면, 소스 에이전트의 세션에서 타겟 에이전트의 세션으로 메시지를 전달할 수 있다. 이때 AccountScopedConversationBindingManager가 계정 격리를 유지하면서 라우팅 경로를 결정한다. 실전에서는 이 매니저를 통해 '메시지 수집 전용 에이전트'와 '응답 생성 전용 에이전트'를 분리하는 패턴을 구현했다. 수집 에이전트가 모든 채널의 메시지를 읽고, 필요한 경우에만 sessions_send로 응답 에이전트의 세션에 컨텍스트를 전달한다.
실전 적용: 명령어 및 설정 예시
dmScope 설정을 직접 적용하는 방법을 실제 환경 기준으로 정리한다. OpenClaw의 세션 구성은 zod-schema.session.d.ts에서 정의된 스키마를 따르며, 주요 설정 항목은 conversationRuntime과 sessionBinding 관련 필드다. per-channel-peer 모드를 활성화하려면 conversation-runtime.d.ts에서 정의된 구조에 따라 다음과 같이 설정한다: ```json { "conversationRuntime": { "dmScope": "per-channel-peer", "sessionBinding": { "enabled": true, "accountScoped": true } } } ``` 이 설정을 적용한 후, 세션 바인딩 상태를 확인하려면 session-binding-service.d.ts에서 제공하는 resolveByConversation 메서드를 호출한다. 실제 CLI에서는 다음과 같은 형태로 테스트할 수 있다: ```bash # 세션 바인딩 상태 확인 openclaw sessions list --scope per-channel-peer # 특정 ConversationRef의 타겟 세션 키 확인 openclaw sessions resolve --ref "telegram:123456:user789" ``` 내가 .zshrc에 추가한 aliases는 다음과 같다. 멀티채널 환경에서 dmScope 레벨을 빠르게 전환할 때 유용하다: ```bash alias oc-scope-main='openclaw config set conversationRuntime.dmScope main' alias oc-scope-peer='openclaw config set conversationRuntime.dmScope per-peer' alias oc-scope-chpeer='openclaw config set conversationRuntime.dmScope per-channel-peer' ``` accountScopedConversationBindings.d.ts에서 정의된 계정별 바인딩 관리는 기본적으로 활성화되어 있으며, 명시적으로 비활성화하지 않는 한 매니저가 작동한다.
한계점 및 주의사항
이 구조를 직접 운영하면서 겪은 한계와 주의사항을 정리한다. 맹목적인 찬양은 도움이 되지 않는다. 첫째, dmScope는 세션 라우팅을 결정하지만 세션 수명은 관리하지 않는다. per-channel-peer로 설정해도 세션이 영구히 유지되는 것은 아니며, OpenClaw의 세션 만료 정책(기본 24시간 비활성 시)에 따라 세션이 정리된다. 이 경우 컨텍스트가 손실되므로, 중요한 대화는 외부 저장소에 백업하는 별도 전략이 필요하다. 둘째, AccountScopedConversationBindingManager는 계정 격리는 보장하지만, 동일 계정 내 여러 에이전트 간의 세션 충돌은 해결하지 못한다. 같은 계정에서 두 개의 에이전트가 동시에 동일한 ConversationRef를 처리하려고 하면, SessionBindingService가 결정론적으로 하나의 targetSessionKey를 반환하므로 둘 중 하나가 대기하거나 실패할 수 있다. 셋째, per-account-channel-peer 모드는 격리가 가장 엄격하지만, 설정 관리가 복잡해진다. 10개 이상의 계정을 운영하면 계정별 dmScope 설정을 일일이 관리해야 하며, 실수로 main 레벨로 설정된 계정이 있으면 전체 격리 구조가 무너질 수 있다. 넷째, resolveByConversation의 결정론적 매핑은 장점이자 단점이다. 같은 ConversationRef는 항상 같은 세션으로 가므로 컨텍스트 연속성이 보장되지만, 의도적으로 세션을 분리하고 싶을 때 유연하게 대응하기 어렵다. 세션 강제 분리는 명시적인 API 호출이 필요하며, 자동화된 라우팅으로는 불가능하다.
수집→종합 파이프라인에서의 세션 바인딩 구조
수집 에이전트가 입력을 수집하여 종합 에이전트에게 전달하는 파이프라인에서 세션 바인딩이 어떻게 작동하는지 실전 기준으로 설명한다. [Telegram: 사용자 요청] │ ▼ ┌─────────────────────────────────┐ │ 수집 에이전트 세션 │ │ - dmScope=per-channel-peer │ │ - 타겟 surface 유형: entity │ │ - 바인딩 레코드: telegram/acct/alice → collector-session-key-001 │ └─────────────────────────────────┘ │ ▼ [결과물: task-note-saved.md] │ ▼ sessions_send(collector-session-key-001, result) │ ▼ ┌─────────────────────────────────┐ │ 종합 에이전트 세션 │ │ - dmScope=per-channel-peer │ │ - 바인딩 레코드: telegram/acct/alice → synthesizer-session-key-002 │ │ │ │ ※ 동일한 conversationRef! │ │ ※ 서로 다른 targetSessionKey (에이전트별 고유) │ └─────────────────────────────────┘ 핵심 포인트: 동일한 `conversationRef`에서 복수 에이전트가 각각 고유한 `targetSessionKey`로 바인딩 가능 세션 격리 유지 조건을 정리하면, 에이전트별 sessionKey는 반드시 서로 달라야 한다. 수집 에이전트는 collector-{channel}-{account}-{peer}-{thread?} 형태의 키를, 종합 에이전트는 synthesizer-{channel}-{account}-{peer}-{thread?} 형태의 키를 사용한다. conversationRef는 동일하지만 targetSessionKey가 다르므로, 각 에이전트는 자신의 컨텍스트를 독립적으로 유지한다. 크로스에이전트 메시지는 sessions_send()를 통해 명시적으로 전달한다. 이 구조 덕분에 수집→분석→종합 같은 다단계 파이프라인에서도 각 단계의 에이전트가 자신만의 세션에서 독립적으로 동작하면서, 동일한 대화 스레드의 맥락을 공유할 수 있다. > 이 주제의 전체 맥락 방향성은 **8. 나는 더 이상 예전 방식으로 일하지 않는다.** 원본 글에 세밀하게 정리되어 있습니다. 더 깊게 탐구하고 싶다면 관련 내부 대표 문서(Pillar/Entity)를 참조하세요.