배경
AgentDesk 멀티노드는 "봇 토큰당 Discord 게이트웨이는 하나"를 전제로 설계됐다. 게이트웨이 리스(discord_gateway_lock_id, PG advisory lock)를 잡은 leader만 웹소켓을 열고, 다른 노드 소유 세션으로 가는 메시지는 leader가 intake_outbox에 넣어 forwarding한다(cluster.intake_routing.mode: enforce, 운영 켜짐). 이 forwarding 계층 위에 trusted origin 검증, outbox 재시도/claim 회수, 첨부 이식성 판정, 세션 소유자 해석이 쌓여 있다.
사용자 관점 결함(2026-09-05 확인):
제안: 채널 소유 기반 게이트웨이 인그레스
Discord 게이트웨이는 채널 단위 구독이 없다. 봇 토큰으로 접속한 모든 세션이 모든 이벤트를 받는다. 그러므로 각 노드가 게이트웨이를 직접 열고, 자기가 소유하지 않은 채널 이벤트는 인그레스에서 버린다. forwarding 계층은 제거 대상이 된다.
- 소유 정본: PG
채널 → 소유 노드 + 리스 만료 + epoch 테이블. 소유 노드가 heartbeat로 갱신.
- DM: 길드가 없으므로
사용자 → 담당 노드 매핑을 별도로 둔다.
- 슬래시 명령·컴포넌트 인터랙션: 3초 내 정확히 한 노드가 ack해야 한다. 소유 판정이 어긋나면 이중 응답 또는 무응답이므로 소유 테이블 조회를 인그레스 첫 단계에 둔다.
- 첨부: 소유 노드가 Discord CDN에서 직접 받으므로 이식성 문제가 사라진다. 전환 전 단기 수리가 필요하면 forwarding 시 로컬 경로 대신 CDN URL(서명 포함, 약 24시간 유효) 또는 바이트를 넘기는 것으로 한다. 이 단기 수리는 본 이슈 범위에 포함하되 별도 PR로 먼저 착지 가능.
- 싱글턴 잡(policy tick, github-sync)은 지금처럼 leader 리스를 유지한다. 게이트웨이 리스만 폐지 대상.
Failover (채널 소유 노드가 떨어졌을 때)
가능한 것: 받아서 답하기. 소유 노드 heartbeat가 끊기면 리스 만료 뒤 다른 노드가 채널을 가져간다. 지금의 봇 단위 failover(docs/multimachine-failover.md)를 채널 단위로 내리는 것과 같다.
불가능한 것: 하던 작업 이어가기. tmux, Claude/Codex TUI 프로세스, 워크트리, ~/.claude/projects/ 대화 파일이 전부 소유 노드 로컬이다. 인수한 노드는 새 세션을 열어 응답할 수 있을 뿐이다. 세션 연속성은 별개 이슈로 다룬다.
반드시 같이 설계할 것:
- 원 소유 노드 복귀 시 split-brain. 리스 epoch로 낮은 세대의 응답을 차단하고, 되찾기 정책(강제 회수 vs 인수 노드가 놓을 때까지 대기)을 정한다.
- 다운 구간 메시지. Discord는 새 웹소켓 세션에 지난 이벤트를 재전송하지 않으므로, 마지막 처리 메시지 id 이후를 REST로 catch-up 한다. leader의 기존 CATCH-UP 로직을 채널 단위로 이관.
트레이드오프
|
현재(leader forwarding) |
제안(채널 소유 인그레스) |
| 봇 토큰 |
leader만 |
모든 노드 |
| 게이트웨이 트래픽 |
1× |
노드 수 × (2대 규모에서 무시 가능) |
| 첨부 |
비이식, 차단 |
소유 노드가 직접 다운로드 |
| 실패 지점 |
forwarding 경로 전체 |
소유 테이블 정합성 |
| 인터랙션 ack |
leader 단일 |
소유 판정 정확성에 의존 |
| 코드 |
outbox/claim/trusted origin/portability 유지 |
그 계층 제거, 소유 리스 + catch-up 추가 |
착수 조건
완료 정의
관련
배경
AgentDesk 멀티노드는 "봇 토큰당 Discord 게이트웨이는 하나"를 전제로 설계됐다. 게이트웨이 리스(
discord_gateway_lock_id, PG advisory lock)를 잡은 leader만 웹소켓을 열고, 다른 노드 소유 세션으로 가는 메시지는 leader가intake_outbox에 넣어 forwarding한다(cluster.intake_routing.mode: enforce, 운영 켜짐). 이 forwarding 계층 위에 trusted origin 검증, outbox 재시도/claim 회수, 첨부 이식성 판정, 세션 소유자 해석이 쌓여 있다.사용자 관점 결함(2026-09-05 확인):
runtime/discord_uploads/)에 받은 뒤 다른 노드로 넘길 방법이 없어NonPortableAttachmentForeignOwner/NonPortableAttachmentRoutedTarget로 차단한다(intake_router_hook.rs:295,537,591).trusted_forward_origin_missing864건이 한 예).제안: 채널 소유 기반 게이트웨이 인그레스
Discord 게이트웨이는 채널 단위 구독이 없다. 봇 토큰으로 접속한 모든 세션이 모든 이벤트를 받는다. 그러므로 각 노드가 게이트웨이를 직접 열고, 자기가 소유하지 않은 채널 이벤트는 인그레스에서 버린다. forwarding 계층은 제거 대상이 된다.
채널 → 소유 노드 + 리스 만료 + epoch테이블. 소유 노드가 heartbeat로 갱신.사용자 → 담당 노드매핑을 별도로 둔다.Failover (채널 소유 노드가 떨어졌을 때)
가능한 것: 받아서 답하기. 소유 노드 heartbeat가 끊기면 리스 만료 뒤 다른 노드가 채널을 가져간다. 지금의 봇 단위 failover(
docs/multimachine-failover.md)를 채널 단위로 내리는 것과 같다.불가능한 것: 하던 작업 이어가기. tmux, Claude/Codex TUI 프로세스, 워크트리,
~/.claude/projects/대화 파일이 전부 소유 노드 로컬이다. 인수한 노드는 새 세션을 열어 응답할 수 있을 뿐이다. 세션 연속성은 별개 이슈로 다룬다.반드시 같이 설계할 것:
트레이드오프
착수 조건
docs/intake-node-routing.md대체 문서, 소유 테이블 스키마, 전환 단계(observe → 실채널 1개 → 전체), 롤백 경로.완료 정의
intake_outboxforwarding 경로 제거, 채널 소유 리스 인그레스로 대체, 2노드 스모크(docs/agent-maintenance/multinode-two-node-smoke.md갱신)관련
cluster.nodes미설정. 독립 수리src/services/cluster/intake_router_hook.rs,intake_routing_config.rs,docs/multimachine-failover.md,docs/agent-maintenance/multinode-transition.md