# CLAUDE.md Tips for future agents working in this repo — a Node CLI that joins a [commonroom](https://github.com/concept-collection/commonroom) call as a muted, visible participant and records every other participant's audio (one WAV per participant) plus the room chat. Read commonroom's CLAUDE.md first: this tool speaks its protocol verbatim, and the protocol is documented there. ## Architecture ``` src/ identity.ts ported from commonroom; EPHEMERAL keypair (no localStorage) nostr.ts ported near-verbatim (Node >= 22 global WebSocket); close() added peer.ts ported, adapted to @roamhq/wrtc; receive-only media (see below) wav.ts incremental WAV writer (buffers ~1 s, re-patches header sizes) recorder.ts the heart: presence, mesh, control channel, audio sinks, files cli.ts arg parsing, signal handling, log lines test/ speaker.ts synthetic participant: sine tone + one chat message loopback.js test: recorder + speaker in a random room, verify tone + chat ``` ## Key design decisions - **Protocol-identical participant.** Same announcements, per-peer signaling topics, deterministic initiator (smaller peer ID), control-channel messages (hello/mute/chat/bye), stalled-connection retry, room-full handling, and the soft cap of 8 — the recorder counts toward it. Don't invent protocol; if the browser client changes, port the change. - **Receive-only media, but symmetric-looking.** Outgoing tracks are wrtc `RTCAudioSource`/`RTCVideoSource` placeholders that never produce data (= a fully muted participant). The VIDEO m-line is negotiated `sendonly` from our side (as initiator via `addTransceiver(track, {direction:'sendonly'})`; as answerer by flipping the transceiver direction after setRemoteDescription) so browsers never send us video — saves bandwidth and decode CPU. Audio stays sendrecv. - **Recording gates on first non-zero frame.** Before the first RTP packet, `RTCAudioSink` delivers all-zero frames at a PROVISIONAL sample rate (16 kHz observed), then switches to the real one (48 kHz) — opening the file eagerly yields junk stub segments. A never-unmuting participant produces no file. A mid-stream format change (rare) closes the segment and starts a new one. - **Wall-clock silence padding.** If the sink stalls > 1 s (network gap, DTX), silence is inserted so sample position keeps tracking elapsed time — the manifest's segment `startedAt` plus the file offset IS the meeting timeline. - **connectionState flaps.** wrtc can pass through 'connected' several times while ICE settles; the connect handler must be idempotent or hello/notice get re-sent per flap. - **Bye cooldown (3 s).** An announcement published just before a peer's bye can arrive just after it and would trigger an instant reconnect (and a stub recording); after a bye we ignore that peer's announcements briefly. - **Every exit path MUST end in `process.exit()`.** @roamhq/wrtc segfaults in its static destructors on a natural process exit whenever nonstandard media sources exist. The CLI, the speaker, and any future script that touches wrtc must exit explicitly. - **Crash-safe outputs.** events.jsonl and chat.txt are appended per event; WAVs flush (with header re-patch) about once a second; manifest.json is written atomically (tmp + rename) at segment boundaries and every 30 s. ## Testing `npm run build && npm run test:loopback` — full end-to-end over the real public relays (needs network): asserts the recorded WAV contains the 440 Hz tone (RMS + zero-crossing rate) and the chat message landed exactly once. Segfault-at-exit in a child process = some path bypassed `process.exit()`. For manual testing against real browsers, record a room and join it at https://concept-collection.github.io/commonroom/ — let the user do multi-person tests; don't try to automate browser media.