OpenClaw Discord 비동기 워커 도입: 타임아웃 오류 해결
Discord 봇을 개발하다 보면 가장 골치 아픈 게 바로 타임아웃 문제죠. Agent가 열심히 답변을 생성하고 있는데 Discord Gateway가 “너무 오래 걸려!”라며 연결을 끊어버리면 정말 허탈하거든요. 사용자 입장에서는 아무런 응답도 받지 못하게 되니까요.
이런 문제를 근본적으로 해결하기 위해 Discord 인바운드 처리를 비동기 방식으로 전환하려고 해요. Agent가 열심히 일하는 동안 Discord listener가 성급하게 연결을 끊지 않도록 만드는 것이 핵심입니다.
인바운드 Discord 턴(turn)을 비동기식으로 전환하여, 사용자에게 노출되는 Discord listener 타임아웃 오류를 제거하는 것이 목표예요.
- Gateway listener가 인바운드 이벤트를 빠르게 수락하고 정규화합니다.
- Discord 실행 큐(run queue)가 현재와 동일한 순서 보장 경계에 따라 직렬화된 job을 저장합니다.
- Worker가 Carbon listener 수명 주기 밖에서 실제 agent 턴을 실행합니다.
- 실행이 완료되면 원래의 채널이나 스레드로 답장을 전달합니다.
이 방식은 agent가 여전히 작업을 진행 중임에도 불구하고 channels.discord.eventQueue.listenerTimeout에서 대기 중인 Discord 실행이 타임아웃되는 문제를 해결하는 장기적인 해결책입니다.
현재 상태
섹션 제목: “현재 상태”이 계획은 부분적으로 구현된 상태예요.
이미 완료된 작업:
- Discord listener 타임아웃과 Discord run 타임아웃이 이제 별도의 설정으로 분리되었습니다.
- 수락된 인바운드 Discord 턴은
src/discord/monitor/inbound-worker.ts에 큐로 쌓입니다. - 이제 Carbon listener 대신 worker가 오래 걸리는 턴을 소유합니다.
- 큐 키(queue key)를 통해 기존의 경로별 순서 보장이 유지됩니다.
- Discord worker 경로에 대한 타임아웃 회귀 테스트가 존재합니다.
이것이 실제로는 어떤 의미일까요?
- 프로덕션 환경의 타임아웃 버그가 수정되었습니다.
- Discord listener의 제한 시간이 만료되어도 실행 중인 턴이 중단되지 않습니다.
- 다만, worker 아키텍처가 아직 완전히 완성된 것은 아닙니다.
아직 부족한 부분:
DiscordInboundJob이 여전히 부분적으로만 정규화되어 있고, 라이브 런타임 참조를 포함하고 있습니다.- 커맨드 시맨틱(
stop,new,reset및 향후 세션 제어)이 아직 worker 네이티브 방식으로 완전히 전환되지 않았습니다. - worker의 observability와 운영자 상태 확인 기능이 아직 최소 수준입니다.
- 재시작 시의 내구성(durability)이 아직 없습니다.
이 작업이 필요한 이유
섹션 제목: “이 작업이 필요한 이유”현재 동작은 전체 agent 턴을 listener 수명 주기에 묶어두고 있어요.
src/discord/monitor/listeners.ts가 타임아웃과 중단 경계를 적용합니다.src/discord/monitor/message-handler.ts가 큐에 쌓인 실행을 해당 경계 내에 유지합니다.src/discord/monitor/message-handler.process.ts가 미디어 로딩, 라우팅, 디스패치, 타이핑 상태 표시, 드래프트 스트리밍, 최종 답장 전달을 인라인으로 수행합니다.
이 아키텍처에는 두 가지 좋지 않은 특성이 있습니다.
- 실행 시간이 길지만 정상적인 턴이 listener watchdog에 의해 강제로 중단될 수 있습니다.
- 다운스트림 런타임이 응답을 생성할 수 있는 상황임에도 사용자는 응답을 받지 못할 수 있습니다.
타임아웃 수치를 높이는 것은 도움이 될 수 있지만, 실패 모드 자체를 바꾸지는 못합니다.
비목표
섹션 제목: “비목표”- 이번 단계에서 Discord가 아닌 채널을 재설계하지 않습니다.
- 첫 번째 구현에서 이를 모든 채널을 위한 범용 worker 프레임워크로 확장하지 않습니다.
- 아직 채널 간 공유되는 인바운드 worker 추상화를 추출하지 않습니다. 중복이 명확할 때만 저수준 프리미티브를 공유합니다.
- 안전한 배포를 위해 꼭 필요한 경우가 아니라면, 첫 번째 단계에서 영구적인 크래시 복구 기능을 추가하지 않습니다.
- 이 계획에서 경로 선택, 바인딩 시맨틱 또는 ACP 정책을 변경하지 않습니다.
현재 제약 사항
섹션 제목: “현재 제약 사항”현재의 Discord 처리 경로는 장기적인 job 페이로드에 포함되어서는 안 되는 일부 라이브 런타임 객체에 여전히 의존하고 있습니다.
- Carbon
Client - 가공되지 않은 Discord 이벤트 형태
- 인메모리 guild history 맵
- 스레드 바인딩 매니저 콜백
- 라이브 타이핑 및 드래프트 스트림 상태
이미 실행 로직을 worker 큐로 옮겼지만, 정규화 경계는 아직 불완전합니다. 현재 worker는 “동일한 프로세스 내에서 일부 라이브 객체를 가지고 나중에 실행되는” 방식이며, 완전히 데이터로만 이루어진 job 경계는 아닙니다.
목표 아키텍처
섹션 제목: “목표 아키텍처”1. Listener 단계
섹션 제목: “1. Listener 단계”DiscordMessageListener는 계속 진입점 역할을 하지만, 그 역할은 다음과 같이 바뀝니다.
- 프리플라이트(preflight) 및 정책 체크 실행
- 수락된 입력을 직렬화 가능한
DiscordInboundJob으로 정규화 - 해당 job을 세션별 또는 채널별 비동기 큐에 삽입
- 큐 삽입이 성공하면 즉시 Carbon에 리턴
listener는 더 이상 엔드 투 엔드 LLM 턴의 수명 주기를 소유하지 않습니다.
2. 정규화된 job 페이로드
섹션 제목: “2. 정규화된 job 페이로드”나중에 턴을 실행하는 데 필요한 데이터만 포함하는 직렬화 가능한 job 디스크립터를 도입합니다.
최소한의 형태:
- 경로 식별 정보 (route identity)
agentId,sessionKey,accountId,channel
- 전달 식별 정보 (delivery identity)
- 대상 채널 ID, 답장 대상 메시지 ID, 스레드 ID(있는 경우)
- 발신자 식별 정보 (sender identity)
- 발신자 ID, 라벨, 사용자 이름, 태그
- 채널 컨텍스트 (channel context)
- guild ID, 채널 이름 또는 슬러그, 스레드 메타데이터, 확인된 시스템 프롬프트 오버라이드
- 정규화된 메시지 본문 (normalized message body)
- 기본 텍스트, 유효 메시지 텍스트, 첨부 파일 디스크립터 또는 확인된 미디어 참조
- 게이팅 결정 (gating decisions)
- 멘션 요구 사항 결과, 커맨드 권한 결과, 바인딩된 세션 또는 agent 메타데이터(해당하는 경우)
job 페이로드는 라이브 Carbon 객체나 가변적인 클로저를 포함해서는 안 됩니다.
현재 구현 상태:
- 부분적으로 완료되었습니다.
src/discord/monitor/inbound-job.ts가 존재하며 worker 핸드오프를 정의합니다.- 페이로드에 여전히 라이브 Discord 런타임 컨텍스트가 포함되어 있어 더 줄여야 합니다.
3. Worker 단계
섹션 제목: “3. Worker 단계”다음을 담당하는 Discord 전용 worker 러너를 추가합니다.
DiscordInboundJob으로부터 턴 컨텍스트 재구성- 실행에 필요한 미디어 및 추가 채널 메타데이터 로드
- agent 턴 디스패치
- 최종 답장 페이로드 전달
- 상태 및 진단 정보 업데이트
권장 위치:
src/discord/monitor/inbound-worker.tssrc/discord/monitor/inbound-job.ts
4. 순서 보장 모델
섹션 제목: “4. 순서 보장 모델”특정 경로 경계에 대해 현재와 동일한 순서 보장이 유지되어야 합니다.
권장 키:
resolveDiscordRunQueueKey(...)와 동일한 큐 키 로직을 사용합니다.
이를 통해 기존 동작을 유지합니다.
- 바인딩된 하나의 agent 대화는 스스로 섞이지 않습니다.
- 서로 다른 Discord 채널은 여전히 독립적으로 진행될 수 있습니다.
5. 타임아웃 모델
섹션 제목: “5. 타임아웃 모델”전환 후에는 두 가지 별도의 타임아웃 클래스가 존재합니다.
- listener 타임아웃
- 정규화 및 큐 삽입만 다룹니다.
- 짧아야 합니다.
- run 타임아웃
- 선택 사항이며, worker가 소유하고 명시적이며 사용자에게 노출됩니다.
- Carbon listener 설정에서 실수로 상속받지 않아야 합니다.
이렇게 하면 “Discord gateway listener가 살아있음”과 “agent 실행이 건강함” 사이의 우발적인 결합이 제거됩니다.
권장 구현 단계
섹션 제목: “권장 구현 단계”1단계: 정규화 경계
섹션 제목: “1단계: 정규화 경계”- 상태: 부분적으로 구현됨
- 완료된 작업:
buildDiscordInboundJob(...)추출- worker 핸드오프 테스트 추가
- 남은 작업:
DiscordInboundJob을 순수 데이터로만 구성- 라이브 런타임 의존성을 job 페이로드 대신 worker 소유 서비스로 이동
- 라이브 listener 참조를 job에 다시 연결하여 프로세스 컨텍스트를 재구축하는 방식 중단
2단계: 인메모리 worker 큐
섹션 제목: “2단계: 인메모리 worker 큐”- 상태: 구현됨
- 완료된 작업:
- 확인된 실행 큐 키로 식별되는
DiscordInboundWorkerQueue추가 - listener가
processDiscordMessage(...)를 직접 기다리는 대신 job을 큐에 넣음 - worker가 프로세스 내 인메모리에서 job을 실행
- 확인된 실행 큐 키로 식별되는
이것이 첫 번째 기능적 전환점입니다.
3단계: 프로세스 분리
섹션 제목: “3단계: 프로세스 분리”- 상태: 시작 전
- 전달, 타이핑 상태 표시, 드래프트 스트리밍 소유권을 worker용 어댑터 뒤로 이동합니다.
- 라이브 프리플라이트 컨텍스트를 직접 사용하는 대신 worker 컨텍스트 재구성으로 교체합니다.
- 필요한 경우
processDiscordMessage(...)를 임시 파사드로 유지한 후 분리합니다.
4단계: 커맨드 시맨틱
섹션 제목: “4단계: 커맨드 시맨틱”-
상태: 시작 전 작업이 큐에 쌓여 있을 때도 네이티브 Discord 커맨드가 올바르게 작동하는지 확인해야 합니다.
-
stop,new,reset -
향후 추가될 세션 제어 커맨드
worker 큐는 커맨드가 활성 상태이거나 대기 중인 턴을 타겟팅할 수 있도록 충분한 실행 상태를 노출해야 합니다.
5단계: Observability 및 운영자 UX
섹션 제목: “5단계: Observability 및 운영자 UX”- 상태: 시작 전
- 모니터 상태에 큐 깊이와 활성 worker 수를 표시합니다.
- 큐 삽입 시간, 시작 시간, 종료 시간, 타임아웃 또는 취소 사유를 기록합니다.
- worker 소유의 타임아웃이나 전달 실패를 로그에 명확하게 노출합니다.
6단계: 선택적 내구성 후속 작업
섹션 제목: “6단계: 선택적 내구성 후속 작업”-
상태: 시작 전 인메모리 버전이 안정화된 후에만 진행합니다.
-
큐에 쌓인 Discord job이 gateway 재시작 시에도 유지되어야 하는지 결정합니다.
-
유지해야 한다면 job 디스크립터와 전달 체크포인트를 저장합니다.
-
유지하지 않는다면 명시적인 인메모리 경계를 문서화합니다.
재시작 복구가 필수적이지 않다면 별도의 후속 작업으로 진행해야 합니다.
파일 영향도
섹션 제목: “파일 영향도”현재 주요 파일:
src/discord/monitor/listeners.tssrc/discord/monitor/message-handler.tssrc/discord/monitor/message-handler.preflight.tssrc/discord/monitor/message-handler.process.tssrc/discord/monitor/status.ts
현재 worker 파일:
src/discord/monitor/inbound-job.tssrc/discord/monitor/inbound-worker.tssrc/discord/monitor/inbound-job.test.tssrc/discord/monitor/message-handler.queue.test.ts
다음에 수정할 가능성이 높은 파일:
src/auto-reply/dispatch.tssrc/discord/monitor/reply-delivery.tssrc/discord/monitor/thread-bindings.tssrc/discord/monitor/native-command.ts
지금 바로 진행할 다음 단계
섹션 제목: “지금 바로 진행할 다음 단계”다음 단계는 worker 경계를 부분적인 상태에서 완전한 상태로 만드는 것입니다.
다음을 먼저 진행하세요:
DiscordInboundJob에서 라이브 런타임 의존성 제거- 해당 의존성을 Discord worker 인스턴스가 대신 보유하도록 변경
- 대기 중인 job을 순수한 Discord 전용 데이터로 축소:
- 경로 식별 정보
- 전달 대상
- 발신자 정보
- 정규화된 메시지 스냅샷
- 게이팅 및 바인딩 결정
- worker 내부에서 해당 데이터를 바탕으로 실행 컨텍스트 재구성
실제로 이는 다음 객체들이 각 job에 포함되지 않고, worker 자체나 worker 소유의 어댑터 뒤에 존재해야 함을 의미합니다.
clientthreadBindingsguildHistoriesdiscordRestFetch- 기타 가변적인 런타임 전용 핸들
이 작업이 완료되면 stop, new, reset에 대한 커맨드 상태 정리를 후속 작업으로 진행해야 합니다.
테스트 계획
섹션 제목: “테스트 계획”다음 파일에 있는 기존 타임아웃 재현 테스트를 유지하세요:
src/discord/monitor/message-handler.queue.test.ts
다음에 대한 새로운 테스트를 추가하세요:
- listener가 전체 턴을 기다리지 않고 큐 삽입 후 즉시 리턴하는지 확인
- 경로별 순서 보장이 유지되는지 확인
- 서로 다른 채널이 여전히 동시에 실행되는지 확인
- 답장이 원래 메시지 목적지로 전달되는지 확인
stop이 worker 소유의 활성 실행을 취소하는지 확인- worker 실패가 이후 job을 차단하지 않고 가시적인 진단 정보를 생성하는지 확인
- ACP 바인딩된 Discord 채널이 worker 실행 하에서도 올바르게 라우팅되는지 확인
리스크 및 완화 방안
섹션 제목: “리스크 및 완화 방안”-
리스크: 커맨드 시맨틱이 현재의 동기식 동작과 달라질 수 있음
- 완화 방안: 커맨드 상태 연결 작업을 나중이 아닌 전환 시점에 함께 진행합니다.
-
리스크: 답장 전달 시 스레드 또는 답장 대상 컨텍스트를 잃어버릴 수 있음
- 완화 방안:
DiscordInboundJob에서 전달 식별 정보를 최우선 데이터로 다룹니다.
- 완화 방안:
-
리스크: 재시도 또는 큐 재시작 중 중복 전송 발생
- 완화 방안: 첫 번째 단계는 인메모리 방식으로만 유지하거나, 영구 저장 전에 명시적인 전달 멱등성을 추가합니다.
-
리스크: 마이그레이션 중에
message-handler.process.ts를 파악하기 어려워짐- 완화 방안: worker 전환 전이나 도중에 정규화, 실행, 전달 헬퍼로 분리합니다.
수락 기준
섹션 제목: “수락 기준”다음 조건이 충족되면 계획이 완료된 것으로 간주합니다.
- Discord listener 타임아웃이 정상적으로 실행 중인 긴 턴을 더 이상 중단시키지 않습니다.
- 코드상에서 listener 수명 주기와 agent 턴 수명 주기가 분리됩니다.
- 기존의 세션별 순서 보장이 유지됩니다.
- ACP 바인딩된 Discord 채널이 동일한 worker 경로를 통해 작동합니다.
stop이 이전의 listener 소유 콜 스택이 아닌 worker 소유의 실행을 타겟팅합니다.- 타임아웃 및 전달 실패가 listener의 소리 없는 드랍이 아닌 명시적인 worker 결과가 됩니다.
남은 배포 전략
섹션 제목: “남은 배포 전략”다음 후속 PR들을 통해 마무리할 예정이에요.
DiscordInboundJob을 순수 데이터로 만들고 라이브 런타임 참조를 worker로 이동stop,new,reset에 대한 커맨드 상태 소유권 정리- worker observability 및 운영자 상태 추가
- 내구성이 필요한지 결정하거나 인메모리 경계를 명시적으로 문서화
Discord 전용으로 범위를 한정하고 성급한 채널 간 worker 추상화를 피한다면, 이는 충분히 통제 가능한 후속 작업이 될 것입니다.
다음 단계
섹션 제목: “다음 단계”OpenClaw Expert
아직 막혀 있나요?
이 문서에서 답을 못 찾았다면 OpenClaw Expert에게 바로 물어보세요.