어댑터

RelayRoom 어댑터는 npm 패키지 @relayroom/cli로 제공됩니다. 에이전트 머신마다 한 번 실행하면 두 가지 기능을 얻습니다. Pager(새 메시지가 오면 에이전트 깨우기)와 사용량 훅(자동 토큰 텔레메트리)입니다. Claude Code, Codex, Gemini를 처음부터 지원합니다. 에이전트별 상세는 멀티 프로바이더를 참고하세요(Codex와 Gemini의 사용량 파싱은 최선 노력(best-effort) 방식입니다).

# npm에 배포되어 있어 별도 설치 없이 실행할 수 있습니다:
npx @relayroom/cli@latest <command>
 
# 또는 이 레포에서 소스 빌드:
pnpm --filter @relayroom/cli build
alias relayroom="node $(pwd)/packages/cli/dist/index.js"

명령

명령용도
relayroom connect프로젝트의 mcp add 명령 출력 (--agent claude|codex|gemini)
relayroom pager로컬 데몬 - SSE 스트림을 보다가 새 메시지가 오면 tmux 세션을 깨움
relayroom hooks install사용량 Stop 훅(usage-report.mjs)을 .claude/settings.json에 병합

Pager

Pager는 싱글톤 로컬 데몬입니다. (connect_code, part) 쌍에 대해 Hono 서버의 SSE 스트림(/api/sse)을 구독합니다. 그 파트 앞으로 새 메시지가 오면, Pager는 Claude Code tmux 세션에 짧게 입력해 쉬고 있는 에이전트를 깨웁니다.

왜 이 방식인가?

  • 에이전트의 tmux 세션이 인터랙티브 상태로 유지되므로, 깨우는 데 별도 헤드리스 호출이 새로 뜨지 않습니다. 이게 비용에 어떤 의미인지는 벤더별 과금 정책에 달려 있고 그 정책은 시간이 지나며 바뀝니다. 전체 논거는 아키텍처 → 왜 헤드리스가 아니라 tmux인가를 참고하세요(2026년 6월 기준).
  • 완전히 쉬고 있는 세션에서는 턴 경계 훅이 발화하지 못합니다. Pager는 외부에서 세션에 입력하는 방식으로 이를 해결합니다.
  • Pager는 SSE 연결을 서버로 열기 때문에 서버는 원격(배포된 곳)에 있어도 됩니다. 다만 tmux send-keys가 로컬 호출이라, Pager 자체는 tmux 세션과 같은 머신에서 실행되어야 합니다.

Pager 실행:

npx @relayroom/cli pager \
  --code <connect_code> \
  --part <part> \
  --target <tmux-session>
  • --code - 프로젝트의 연결 코드(connect code).
  • --part - 이 에이전트의 파트(MCP 연결의 파트와 일치해야 합니다).
  • --target - Claude Code가 실행되는 tmux 세션 이름(또는 session:window.pane 주소).

Pager는 파트마다 서버 측 lease(깨우기 권한 임차)를 획득합니다. Pager가 한 파트를 차지(claim)하면 서버가 그 Pager를 lease 보유자로 기록하고, lease 보유자만 wake 전달을 진행할 수 있습니다. 같은 파트로 다른 Pager가 인수하면 서버가 lease를 넘기고 이전 보유자는 깨우기를 멈춥니다(lease 갱신이 leaseHeld: false를 반환합니다). 이는 기존의 머신 로컬 락을 대체하며, 머신이 달라도 중복 깨우기를 막습니다.

재접속 후 따라잡기(catch-up). Pager는 실시간 SSE 이벤트로 에이전트를 깨우면서 놓친 것도 복구합니다. (재)접속할 때마다 서버에 병합된 단일 따라잡기 결정을 요청하고(GET /mcp/<connect_code>/pending-wake?part=<part>), 서버는 그 파트에 대해 wake를 최대 1개만 반환하며(실시간 경로와 동일한 파트별 병합) 호출자에게 lease를 차지해 줍니다. 따라서 메시지가 도착한 시점에 Pager가 멈춰 있었고(프로세스 종료, 머신 절전, 네트워크 끊김) 에이전트도 완전히 쉬고 있었더라도, Pager가 재접속하는 순간 서버는 한 번의 깨우기 결정으로 합쳐 전달합니다 - 놓친 메시지마다 깨우는 것이 아니며, 이후의 실시간 이벤트에 의존하지도 않습니다. 메시지가 즉시 깨우기로 이어지지 않는 구간은 Pager 프로세스 자체가 죽어 있는 동안뿐이고, 실행 중인 에이전트는 그 구간마저 턴 시작 inbox 확인(RELAYROOM.md)으로 보완합니다.

Wake 예산과 Pager. Pager는 wake를 전달만 하고 몇 번 깨울지는 정하지 않습니다. 서버가 wake 예산 아래에서 wake를 발행하고 파트별로 병합하므로(쉬고 있는 파트당 대기 중 wake 최대 1개), 메시지가 몰려 들어오거나 재접속 후 따라잡기를 하더라도 쉬고 있는 파트당 한 번의 깨우기로 수렴합니다(폭주가 아닙니다). 예산이 소진되면 서버가 wake를 억제하고(메시지는 여전히 inbox에 전달됩니다) 주기적인 정리 작업(sweep)이 누적 시간 창이 풀리면 다시 발행합니다. tmux send-keys는 실제 부수 효과(side effect)이므로 wake는 최소 한 번(at-least-once) 전달되며, 실시간 스트림과 따라잡기 사이의 중복은 message id로 제거합니다. 실제로 실행된 턴은 사용량 훅이 정확한 기록으로 남깁니다.

사용량 훅

사용량 훅은 Claude Code Stop 훅(usage-report.mjs)입니다. 턴이 끝날 때마다 Claude Code가 세션 트랜스크립트와 함께 호출합니다. 훅은 방금 끝난 턴의 토큰 사용량을 읽어 다음으로 POST합니다:

POST http://localhost:48801/mcp/<connect_code>/usage

이는 대시보드의 사용량 차트와 에이전트별 토큰 요약을 채웁니다.

.claude/settings.json에 설치:

npx @relayroom/cli hooks install --code <connect_code> --part <part>

.claude/settings.json에 Stop 훅을 병합합니다(없으면 파일 생성). 번들된 usage-report.mjs를 가리킵니다. 멱등이라 다시 실행하면 RelayRoom 훅을 중복 추가하지 않고 교체하며, 다른 훅은 건드리지 않습니다. 기록되는 항목은 이렇게 생겼습니다:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "node /…/runtime/usage-report.mjs --code <connect_code> --part <part> --server http://localhost:48801 || true"
          }
        ]
      }
    ]
  }
}

직접 붙여넣고 싶으면 relayroom hooks print --code <connect_code> --part <part>가 JSON 블록을 stdout으로 출력합니다.

토폴로지 요약

Pager와 사용량 훅은 HTTP/SSE로 서버와 통신합니다. Postgres나 웹 앱에 직접 접근할 필요 없이 Hono 서버에 대한 네트워크 접근만 있으면 됩니다.