diff --git a/packages/server/src/ws/handler.ts b/packages/server/src/ws/handler.ts index 0cf2b328..6955a939 100644 --- a/packages/server/src/ws/handler.ts +++ b/packages/server/src/ws/handler.ts @@ -754,16 +754,15 @@ class ConnectionManager { } } - /** Send event to users involved in a federated call. Works for both Path A (DM exists) and Path B (no local DM). */ + /** Send event to users who were ringed for a federated call. + * ALWAYS uses ringedUserIds, never sendToDmMembers — sendToDmMembers would + * also reach the caller's replicated stub, causing cross-instance event contamination + * (the caller's multi-instance WS gets dm_call_accepted with the wrong dmChannelId). */ sendToFederatedCallUsers(federatedId: string, event: ServerEvent): void { const call = this.federatedCalls.get(federatedId); if (!call) return; - if (call.dmChannelId) { - this.sendToDmMembers(call.dmChannelId, event); - } else { - for (const uid of call.ringedUserIds) { - this.sendToUser(uid, event); - } + for (const uid of call.ringedUserIds) { + this.sendToUser(uid, event); } } diff --git a/packages/web/src/hooks/useWebSocket.ts b/packages/web/src/hooks/useWebSocket.ts index ddb9aba4..31e55b5e 100644 --- a/packages/web/src/hooks/useWebSocket.ts +++ b/packages/web/src/hooks/useWebSocket.ts @@ -802,15 +802,19 @@ function handleEvent(origin: string, event: ServerEvent): void { case 'dm_call_accepted': { const { setIncomingCall, setOutgoingCall, outgoingCall, setActiveDmCall, connectFn, isLiveKitConnected } = useVoiceStore.getState(); - // Track whether WE are the caller before clearing state const wasOutgoingCall = !!outgoingCall; setIncomingCall(null); setOutgoingCall(null); + + // Only enter active call state if: + // - We're the caller (wasOutgoingCall) → will connect via connectFn below + // - We already connected to LiveKit (clicked accept in handleAccept) + // Other instances of the same user must NOT enter call state — they'd show + // "Connecting..." forever with no actual LiveKit connection. const callDmId = event.dmChannelId || event.federatedCallId || ''; - setActiveDmCall({ dmChannelId: callDmId }); - // Only auto-connect if we're the CALLER waiting for acceptance. - // The callee who accepted connects in the click handler (handleAccept). - // Other instances of the same user should NOT auto-connect. + if (wasOutgoingCall || isLiveKitConnected) { + setActiveDmCall({ dmChannelId: callDmId }); + } if (connectFn && !isLiveKitConnected && wasOutgoingCall && callDmId) { connectFn(callDmId, true).catch((err: unknown) => { console.error('[WS] DM call connect failed:', err);