어댑터

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)

wake 전달 - 페이저 또는 채널

유휴 에이전트에게 wake가 닿는 경로가 둘이고, 기본은 페이저입니다.

wake가 도착하는 방식사용 가능 조건
페이저(기본)tmux send-keys로 세션에 짧은 nudge를 타이핑언제나. preview 기능도, 플래그도, 허용목록도 필요 없음
채널(옵트인)Claude Code가 이벤트로 큐에 넣고 턴 경계에서 처리Claude Code, 대화형 세션에서만

켜려면 ./rr.sh up --channel, 또는 relayroom channel on 후 재시작입니다. 플래그는 같은 저장된 설정에 대한 sugar이므로, 자체 업데이트나 respawn이 그 선택을 조용히 되돌리지 않습니다.

콘솔에서 보이는 모양이 다르고, 그게 정상입니다. 페이저에서는 wake가 세션에 타이핑되는 텍스트로 도착하고, 채널에서는 이벤트로 주입되어 타이핑 중인 것과 섞이지 않습니다. 에이전트의 동작은 어느 쪽이든 같습니다. 0.6.0에서 막 업그레이드했다면 지금 보이는 것이 이 변화이지 고장이 아닙니다.

옵트인 전에 알아둘 한계 둘:

  • 헤드리스(claude -p)에는 채널 경로가 아예 없습니다. 비대화형에서는 Claude가 development-channels 플래그를 무시하므로, 헤드리스 배포는 항상 페이저를 씁니다.
  • 채널은 research preview이고 --channels는 허용목록에 있는 플러그인만 받습니다. RelayRoom은 채널을 평범한 MCP 서버로 등록하는데 그 허용목록이 그 형태를 표현할 수 없어서, 대신 개발용 플래그로 로드합니다.

채널이 켜져 있는데 시작하지 못하면 전달이 페이저로 떨어지고, 그 사실을 말해줍니다. 조용히 실패하지 않고, 설정을 꺼버리지도 않습니다. 사람이 고른 것을 아무도 대신 끄지 않습니다. ./rr.sh status가 이유를 보고합니다.

channel: ON for this worktree, but NOT delivering - fell back to pager at 04:12 (...).
         claude accepted the launch and dropped notifications; wakes arrive by send-keys instead.
         The setting is left ON: nobody turns off what a person chose.

그동안 tmux 상태바에는 빨간 ○ !Channel이 뜹니다. 연속 실패는 횟수로 세고, ./rr.sh up --no-channel로 재시도를 멈춥니다.

herdr를 통한 wake 전달

이건 위의 Claude Channels와 별개입니다. 다른 질문이에요. Channels는 Claude가 어떤 종류의 nudge를 받느냐이고, 이건 Pager가 어느 멀티플렉서에 타이핑하느냐입니다. herdr 워크트리는 Pager를 쓰되 tmux send-keys 대신 herdr 소켓을 통합니다.

wake는 staging하고, 확인하고, 그 다음에만 제출됩니다. Pager가 텍스트를 보내고, 그것이 실제로 페인의 입력창에 도달했는지 확인한 뒤에야 Enter를 보냅니다.

이 순서는 측정 때문에 생겼습니다. herdr 자체의 agent.prompt가 사용자를 대신해 권한 다이얼로그에 답해버리는 것이 관측됐습니다. 끝에 붙은 Enter가 프롬프트가 아니라 다이얼로그로 갔던 겁니다.

그래서 권한 다이얼로그가 페인을 잡고 있으면 아무것도 제출되지 않고 아무것도 답해지지 않습니다. wake는 큐에 남고, 시도마다가 아니라 막힌 구간당 한 번 알림이 오며, 그동안 워크스페이스에 표시가 붙습니다. 페인이 풀리면 에이전트가 그 메시지를 받습니다.

herdr 소켓에 도달할 수 없으면 Pager는 tmux 전달로 폴백하고 그 사실을 로그에 남깁니다. wake는 계속 도착하므로 겉보기에 고장 난 것이 없습니다. 다만 그 파트는 요청받은 곳에서 돌고 있지 않습니다. 비대칭에 주의하세요. ./rr.sh up --use-herdr는 소켓에 도달할 수 없으면 하드 에러로 시작을 거부하지만, Pager는 조용해지는 대신 폴백합니다. 전달이 계속되는 것이 전달이 깔끔한 것보다 낫기 때문입니다.

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 서버에 대한 네트워크 접근만 있으면 됩니다.