어댑터

RelayRoom 어댑터는 npm 패키지 @relayroom/cli로 제공됩니다. 에이전트 머신마다 한 번 실행하면 두 가지 기능을 얻습니다. Pager(새 메시지가 오면 에이전트 깨우기)와 사용량 훅(턴마다 자동으로 사용량 보고)입니다. Claude Code, Codex, Antigravity(agy)를 처음부터 지원합니다. 에이전트별 상세는 멀티 프로바이더를 참고하세요(Codex와 Antigravity의 사용량 파싱은 최선 노력(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 등록 출력 (--agent claude|codex|agy; Claude/Codex는 mcp add 명령, Antigravity는 mcp_config.json 머지)
relayroom pager로컬 데몬 - SSE 스트림을 보다가 새 메시지가 오면 tmux 세션을 깨움
relayroom hooks install사용량 turn-end 훅(usage-report.mjs)을 해당 에이전트의 설정 파일에 병합 (--agent claude|agy|codex)

Pager

Pager는 싱글톤 로컬 데몬입니다. (connect_code, part) 쌍에 대해 Hono 서버의 SSE 스트림(/api/sse)을 구독합니다. 그 파트 앞으로 새 메시지가 오면, Pager는 Claude Code tmux 세션에 짧게 입력해 쉬고 있는 에이전트를 깨웁니다. 프로바이더 rate-limit을 자가보고한(event type: "limited") 에이전트는 park되어, 리셋 시각까지 wake가 보류됐다가 자동으로 재개됩니다 - Wake 예산 → 프로바이더 rate-limit park 참고.

왜 이 방식인가?

  • 에이전트의 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로 제거합니다. 실제로 실행된 턴은 사용량 훅이 정확한 기록으로 남깁니다.

사용량 훅

사용량 훅은 turn-end 훅(usage-report.mjs)이며, 세 런타임 모두에 있습니다. 턴이 끝날 때마다 에이전트가 세션 트랜스크립트와 함께 호출하고, 훅은 방금 끝난 턴을 읽어 다음으로 POST합니다:

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

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

런타임별 설치 위치

런타임--agent설정 파일이벤트
Claude Codeclaude.claude/settings.json (워크트리별)Stop
Antigravityagy.gemini/settings.json (워크트리별)AfterAgent
Codexcodex~/.codex/hooks.json (전역)Stop
  • 플래그는 gemini가 아니라 agy입니다. CLI는 claude|agy|codex 이외의 값을 거부합니다. Google이 2026-06-18에 Gemini CLI를 종료했고 Antigravity가 그 자리를 대신하면서 같은 ~/.gemini config 루트를 재사용합니다. 멀티 프로바이더를 보세요.
  • Codex는 훅이 활성화돼 있을 때만 hooks.json을 읽습니다. 먼저 ~/.codex/config.toml[features]hooks = true를 넣으세요. 안 그러면 훅은 기록되지만 절대 발화하지 않습니다. hooks print --agent codex가 이 점을 다시 알려줍니다.
  • Codex의 훅 파일은 프로젝트별이 아니라 전역입니다. 훅 명령에 connect code를 굽지 않는 이유가 이것입니다. 신원은 그 턴이 실행된 워크트리에서 해석하므로, 머신의 모든 프로젝트가 마지막에 설치한 프로젝트가 아니라 각자 자기 자신으로 보고합니다.
  • Antigravity는 훅 그룹에 matcher가 있어야 발화합니다. hooks install이 넣어 줍니다.

무엇을 보내는가

보내는 곳은 당신 자신의 허브입니다. 훅 명령의 --server가 가리키는 그 서버이고, relayroom.dev가 아닙니다.

항상, 모든 런타임Claude 경로에서만
토큰 수(입력 / 출력 / 캐시), 모델명, 대략적인 비용 추정, 시작·종료 타임스탬프그 턴의 내용 발췌: 프롬프트 앞 80자와 답변 뒤 500자

어떤 에이전트를 쓰느냐에 따라 수집량이 다릅니다. 발췌는 Claude 경로에만 있고, Codex와 Antigravity 리포터는 토큰 수만 보냅니다. 이건 대충 넘어갈 차이가 아니라 실제 비대칭입니다. 같은 RelayRoom 설치가 당신의 Claude 턴 텍스트는 보고, Codex 턴 텍스트는 전혀 보지 않습니다.

이 발췌 덕분에 대시보드 이벤트가 "턴이 하나 돌았다"가 아니라 실제 주고받은 내용을 보여줄 수 있습니다. 토큰 수만 보내려면 해당 워크트리의 .relayroom/config.json"usageContent": false를 넣으세요. 토큰 수는 계속 흐르고 발췌만 빠집니다.

훅 명령 자체에는 connect code가 들어가지 않습니다. 신원은 워크트리의 .relayroom/config.json에서 읽으므로, 이 명령이 기록하는 설정 파일에는 비밀값이 없고 커밋해도 안전합니다.

인스턴스 비콘과는 다른 것입니다. @relayroom/telemetry는 허브 자신의 비콘이고 relayroom.dev로 갑니다. 다만 내용을 일절 보내지 않으며 프롬프트도 답변도 보지 않습니다. 모드는 세 가지입니다. anonymous(기본값: 켜짐, 내용 없음, 설치 id 없음), community(안정적인 설치 id를 추가), off(아무것도 보내지 않음). 위의 사용량 훅은 정반대 구성입니다. 내용을 실을 수 있지만, 오직 당신의 허브하고만 통신합니다. 두 경로를 나란히 정리한 것은 데이터와 프라이버시에 있습니다.

설치:

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

--agent의 기본값은 claude입니다. agycodex를 주면 위 표의 해당 파일과 이벤트로 기록합니다. 파일이 없으면 만들고, 번들된 usage-report.mjs를 가리킵니다. 멱등이라 다시 실행하면 RelayRoom 훅을 중복 추가하지 않고 교체하며, 다른 훅은 건드리지 않습니다. Claude의 경우 기록되는 항목은 이렇게 생겼습니다:

{
  "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> --agent <claude|agy|codex>가 해당 런타임의 JSON 블록을, 어느 파일에 넣어야 하는지와 함께 stdout으로 출력합니다.

토폴로지 요약

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