OpenClaw 세션 바인딩 구현: 채널 독립적 라우팅 가이드
개발을 하다 보면 세션 관리와 메시지 라우팅이 복잡하게 꼬여서 고생할 때가 많죠? 특히 여러 채널을 지원해야 하는 환경에서는 로직이 서로 섞이면서 예상치 못한 버그가 발생하곤 합니다. 이런 문제를 해결하고 더 깔끔한 구조를 만들기 위해 세션 바인딩 모델을 개선하려고 해요.
이 문서는 장기적인 Channel Agnostic 세션 바인딩 모델의 방향성과 다음 구현 단계에서 진행할 구체적인 범위를 다룹니다.
이 문서는 장기적인 Channel Agnostic 세션 바인딩 모델과 다음 구현 반복(Iteration)을 위한 구체적인 범위를 정의합니다.
목표:
- Subagent에 종속된 세션 라우팅을 핵심 기능으로 구축
- 채널별 특이 동작은 Adapter에 유지
- 일반적인 Discord 동작에서 회귀 버그(Regression) 방지
이 기능이 필요한 이유
섹션 제목: “이 기능이 필요한 이유”현재의 동작은 다음 요소들이 혼재되어 있습니다:
- Completion Content Policy
- Destination Routing Policy
- Discord 관련 세부 사항
이로 인해 다음과 같은 에지 케이스가 발생했어요:
- 동시 실행 시 메인 채널과 스레드에 메시지가 중복 전달됨
- 재사용된 Binding Manager에서 오래된 Token을 사용함
- Webhook 전송 시 활동 기록(Activity Accounting)이 누락됨
첫 번째 반복 단계의 범위
섹션 제목: “첫 번째 반복 단계의 범위”이번 단계의 범위는 의도적으로 제한했습니다.
1. Channel Agnostic 핵심 인터페이스 추가
섹션 제목: “1. Channel Agnostic 핵심 인터페이스 추가”바인딩 및 라우팅을 위한 핵심 타입과 서비스 인터페이스를 추가합니다.
제안된 핵심 타입:
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>;};핵심 서비스 계약(Contract):
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을 위한 핵심 Delivery Router 추가
섹션 제목: “2. Subagent Completion을 위한 핵심 Delivery Router 추가”Completion 이벤트에 대한 단일 목적지 확인 경로를 추가합니다.
Router 계약:
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로 유지
섹션 제목: “3. Discord를 Adapter로 유지”Discord는 첫 번째 Adapter 구현체로 유지됩니다.
Adapter의 역할:
- Thread Conversation 생성 및 재사용
- Webhook 또는 채널 전송을 통해 바인딩된 메시지 발송
- 스레드 상태 확인 (보관됨/삭제됨)
- Adapter Metadata 매핑 (Webhook ID, Thread ID 등)
4. 현재 알려진 정확성 문제 수정
섹션 제목: “4. 현재 알려진 정확성 문제 수정”이번 단계에서 반드시 해결해야 할 사항입니다:
- 기존 Thread Binding Manager 재사용 시 Token 사용 갱신
- Webhook 기반 Discord 전송에 대한 아웃바운드 활동 기록
- 세션 모드 Completion에서 바인딩된 스레드 목적지가 선택되었을 때, 암시적으로 메인 채널로 되돌아가는(Fallback) 동작 중단
5. 현재 런타임 안전 기본값 유지
섹션 제목: “5. 현재 런타임 안전 기본값 유지”스레드 바인딩 생성(Spawn) 기능이 비활성화된 사용자에게는 동작 변화가 없습니다.
기본값 유지:
channels.discord.threadBindings.spawnSubagentSessions = false
결과:
- 일반 Discord 사용자는 현재 동작을 그대로 유지합니다.
- 새로운 핵심 경로는 기능이 활성화된 바인딩된 세션 Completion 라우팅에만 영향을 미칩니다.
이번 단계에서 제외되는 사항
섹션 제목: “이번 단계에서 제외되는 사항”다음 사항들은 명시적으로 연기되었습니다:
- ACP 바인딩 타겟 (
targetKind: "acp") - Discord 이외의 새로운 채널 Adapter
- 모든 전달 경로의 전역 교체 (
spawn_ack, 향후subagent_message등) - 프로토콜 수준의 변경
- 모든 바인딩 지속성을 위한 Store 마이그레이션 및 버전 관리 재설계
ACP 관련 참고 사항:
- 인터페이스 설계 시 ACP를 고려한 여지를 남겨두었습니다.
- 이번 단계에서 ACP 구현은 시작하지 않습니다.
라우팅 불변 원칙
섹션 제목: “라우팅 불변 원칙”다음 원칙들은 이번 단계에서 필수적으로 지켜져야 합니다.
- 목적지 선택과 콘텐츠 생성은 별개의 단계로 분리합니다.
- 세션 모드 Completion이 활성화된 바인딩 목적지로 확인되면, 반드시 해당 목적지로 전달해야 합니다.
- 바인딩된 목적지에서 메인 채널로 숨겨진 재라우팅(Reroute)을 하지 않습니다.
- Fallback 동작은 명시적이고 관찰 가능해야 합니다.
호환성 및 배포
섹션 제목: “호환성 및 배포”호환성 목표:
- 스레드 바인딩 생성이 꺼진 사용자에게 회귀 버그가 없어야 함
- 이번 단계에서 Discord가 아닌 채널에는 변경 사항이 없음
배포 순서:
- 현재 Feature Gate 뒤에 인터페이스와 Router를 배치합니다.
- Discord Completion 모드의 바인딩된 전달을 Router를 통해 라우팅합니다.
- 바인딩되지 않은 흐름은 기존 경로를 유지합니다.
- 타겟 테스트와 카나리(Canary) 런타임 로그로 검증합니다.
이번 단계에서 필요한 테스트
섹션 제목: “이번 단계에서 필요한 테스트”다음 항목들에 대한 Unit 및 Integration 테스트 커버리지가 필요합니다:
- Manager 재사용 후 Manager Token 로테이션이 최신 Token을 사용하는지 확인
- Webhook 전송이 채널 활동 타임스탬프를 업데이트하는지 확인
- 동일한 요청자 채널에 있는 두 개의 활성 바인딩 세션이 메인 채널로 중복 전달되지 않는지 확인
- 바인딩된 세션 모드 실행에 대한 Completion이 스레드 목적지로만 확인되는지 확인
- Spawn Flag가 비활성화된 경우 기존 동작이 변경되지 않고 유지되는지 확인
구현 제안 파일
섹션 제목: “구현 제안 파일”Core:
src/infra/outbound/session-binding-service.ts(신규)src/infra/outbound/bound-delivery-router.ts(신규)src/agents/subagent-announce.ts(Completion 목적지 확인 통합)
Discord Adapter 및 Runtime:
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
이번 단계의 완료 기준
섹션 제목: “이번 단계의 완료 기준”- 핵심 인터페이스가 존재하고 Completion 라우팅을 위해 연결됨
- 위에서 언급한 정확성 수정 사항이 테스트와 함께 머지됨
- 세션 모드 바인딩 실행 시 메인 채널과 스레드에 Completion이 중복 전달되지 않음
- 바인딩된 Spawn 기능이 비활성화된 배포 환경에서 동작 변화가 없음
- ACP는 명시적으로 연기된 상태를 유지함
다음 단계
섹션 제목: “다음 단계”OpenClaw Expert
아직 막혀 있나요?
이 문서에서 답을 못 찾았다면 OpenClaw Expert에게 바로 물어보세요.