- `LIVE_RELOAD=false npm run dev` keeps the renderer alive across a machine suspend; the reload client otherwise destroys the session under test. - `dev-peer.sh` plus a separate userdata dir runs a second local peer. - `tools/voice-probe.js` samples peer state and RTP counters from a live window, persisting to localStorage so a renderer reload cannot erase it. - e2e helpers for voice pairs, peer-role election, and a TURN relay. - Electron single-instance and dev-client-load decisions move into rules files with colocated specs.
5.2 KiB
05 — Signaling & multi-signal-server connection
User theme: “I’m connected but nobody sees me / rooms flicker / different signal servers break everything.”
Severity: P0 for presence and as root cause of voice + chat visibility.
How signaling is supposed to work (user view)
- When you open a community, you appear in the member list for everyone in that community.
- Leaving / going offline shows you as offline (without duplicate leave spam for multi-device).
- Switching communities updates who you see without wiping presence for other saved servers.
- If the signal host restarts or Wi‑Fi blips, you come back automatically.
- Communities hosted on different signal hosts still work as long as your client is authorized on that host (see
04). - Users should be able to see/sync chats/files/profile images/server states info change/plugin states and events/talk without losing connectivity or only some users are seen and other aren't.
How signaling is supposed to work (system)
- One WebSocket per signal URL (
SignalingManager). - Identify first with that URL’s token + actor
oderId+clientInstanceId. - Then
join_server/view_serverfor rooms on that URL. - Server serializes handlers per connection so join cannot race mid-identify.
- Client
reIdentifyAndRejoinon reconnect; rooms effects resync as safety net. - Room traffic prefers room
sourceUrl; temporary fallback to other compatible endpoints on outage. - Non-federated: Signal A does not share peer registry with Signal B. Same human on two hosts = two actor ids (client glue).
Canonical catalog: agents-docs/features/signaling.md.
Client map: infrastructure/realtime/README.md + signaling/.
Failure modes
1 — Identify / join race or skipped identify
Symptom: Local UI shows room open; others don’t see you; you miss user_joined / chat_message; “alone”.
Causes
- Join sent before identify (fixed server-side serialization + client reIdentify — regressions still dangerous).
getIdentifyCredentialsForSignalUrlreturns null (no credential) → no identify.- Identify cache empty on fresh socket without store fallback (lesson: must fall back to credential store).
2 — Wrong socket / wrong affinity
Symptom: Presence or RTC relay never reaches peers in the room; works only when everyone shares the same endpoint URL alias.
Causes
- Room
sourceUrlstale vs directoryserverInstanceIdcanonicalization. - Fallback broadcast regressions (raw room messages must not spam every manager when route unknown — README describes current intended behavior).
- Cold start reconnect before health probes collapse aliases.
3 — Multi-signal leave tears down wrong peers
Symptom: Leaving a room on signal-sweden drops a peer still shared via signal.toju.app.
Intended: user_left subtracts only that cluster’s shared servers; preserve routes while P2P still live.
4 — Half-open / zombie sockets
Symptom: UI “connected”; no events after server process restart.
Mitigations in tree: keepalive ack timeout after first ack; /api/health probe; serverInstanceId change forces new WS. Regressions: skipping first heartbeat tick, treating 521/522 as incompatible.
5 — Auth failure cascade
Foreign auth_required / auth_error without silent re-provision → authorize UI or invisible membership (04).
6 — Multi-device eviction loop
Shared clientInstanceId in localStorage across tabs → server evicts sibling on identify. Must stay in sessionStorage.
Different signal servers — user-visible contract
| Situation | Expected |
|---|---|
| Two users, same room, same signal URL | Discover each other; RTC relay OK |
| Two users, “same” community mirrored on different signals | Not supported as one mesh — they are different rooms unless directory/affinity converges them |
| One user, rooms on A and B | Two sockets; two actor ids; local profile one |
| DM/call across people who met on foreign rooms | Must address connected actor id (see 09) |
Key files
signaling.manager.ts,signaling-transport-handler.ts,server-signaling-coordinator.tssignaling-message-handler.ts,server-membership-signaling-handler.tsroom-signaling-connection.ts,rooms.effects.tsserver-directoryhealth / canonical endpoint rules- Server:
server/src/websocket/handler.ts(out of default scope — ask)
Proof of done (signaling)
- Two clients join same room: both appear in member lists within seconds of identify.
- Kill signal process; both recover presence without manual leave/rejoin.
- User with rooms on two URLs: leave on URL A does not remove voice peer still shared only via URL B.
- Fresh connect: first outbound after open is identify; join never accepted unauthenticated (server log / test).
- Alias URLs with same
serverInstanceIdcollapse before room reconnect.
Fix order notes
Usually fix auth provision (04) before deep signaling surgery — many “connection” bugs are missing identify. Then harden identify credential resolution and affinity. Instrument connectionScope, actor id, and signal URL on every join in debug builds.