Four fixes addressing the full state management problem:
1. Passive ready handler: no longer auto-connects to LiveKit on
page refresh. Prevents identity conflicts when the same user
has multiple sessions fighting for one LiveKit identity slot.
The user must re-accept to join; state is shown but not acted on.
2. SoundController sync guard: incomingCallLoading/outgoingCallLoading
refs prevent multiple playSound calls during async audio load.
If call is cancelled while sound loads, stops it immediately on
completion. Eliminates the "5 ringtones at once" bug.
3. Host dm_call_accepted broadcasts now include federatedCallId so
all clients (including remote instances) can match the event.
4. Removed all diagnostic console.log statements.
Root cause: sendToFederatedCallUsers used sendToDmMembers when dmChannelId
was set, which broadcast to ALL DM members including the caller's replicated
stub. The caller's multi-instance WS received dm_call_accepted with the
REMOTE instance's dmChannelId, causing token request for a non-existent
channel (403) and preventing the caller from connecting.
Fix 1: sendToFederatedCallUsers always uses ringedUserIds (exact recipients)
instead of sendToDmMembers (all members including caller stub).
Fix 2: dm_call_accepted handler only sets activeDmCall if the client is the
caller (wasOutgoingCall) or already connected to LiveKit. Other instances of
the same user just clear ringing without entering stuck "Connecting..." state.
1. Accept/reject/end from remote instance now resolves federatedId
to local dmChannelId via DB lookup, so the host can find its
VoiceRoom when the event arrives with only a federatedCallId.
Previously silently failed with "No active call" error.
2. Batch all dm_call_incoming state updates into a single
useVoiceStore.setState() call. Prevents SoundController from
starting multiple ringtone instances (async playSound guard
race when 4 separate set() calls each triggered the subscription).
3. Always overwrite callOrigin/federatedCallId (with null if absent)
on dm_call_incoming. Prevents stale values from a previous
federated call routing local accepts to the wrong instance.
Three fixes for multi-instance call state consistency:
1. Client dm_call_accepted handler only auto-connects to LiveKit if
the user was the caller (outgoingCall was set). Other instances of
the same user just clear ringing state without connecting.
2. Server processDmCallAcceptEvent remote path skips duplicate
broadcast when FederatedCallEntry is already active (prevents
state conflicts from host fan-out arriving after local accept).
3. Ready payload handler clears stuck incomingCall when restoring
an already-active call after page refresh.
When findOrCreateDmChannel creates or finds a local channel for a
federatedId that has an active FederatedCallEntry with null dmChannelId,
update the entry. Prevents stale null references for Path B calls.
The outbox worker's event reconstruction whitelist was missing these two
fields, causing read_state_update and dm_close/dm_reopen relay events to
arrive at remote instances with empty payloads and get rejected.
Three bugs that combined to corrupt DM identities during initial sync:
1. Sync endpoint omitted federatedId for group DMs, causing the receiver
to treat them as 1-on-1 DMs and compute wrong pair hashes — creating
phantom channels that collide with real ones.
2. backfillHomeUserId unconditionally overwrote existing homeUserIds,
so a single wrong match would permanently corrupt a user's identity
and cascade to all subsequent lookups.
3. Migration duplicate-stub Criterion 1 ("shared 1-on-1 DM membership")
incorrectly merged different users from the same domain who were
simply having a conversation, destroying one user's identity.
The relay broadcast loop skipped members whose homeInstance matched the
source instance, assuming they already received the message on their home
server. This broke delivery for federated users (e.g. youruser@nova on
orbit) who are actively connected to the remote instance.
Client-side dedup in addRealtimeMessage already handles double delivery
via sourceMessageId cross-matching, making the server-side skip both
unnecessary and harmful.
Queue dm_close relay in DELETE /api/dm/:id after broadcasting locally,
and queue dm_reopen relay inside the myDm.closed === 1 branch of POST
/api/dm so peers are notified only on actual reopen, not new DM creation.
When a federated user first accesses DMs on a remote instance,
all channels appear unread because no read_states rows exist.
Seed missing read states to the latest message during the ready
payload build. The S2S relay keeps things in sync going forward.
Receive read_state_update events from peers, translate message
coordinates to local IDs via sourceInstance/sourceMessageId
mapping, update read_states with timestamp-only LWW.
Queue read_state_update events when users ack DM messages on
channels with a federatedId. Translates local message IDs to
federation coordinates using sourceInstance/sourceMessageId.
profileUpdatedAt ?? createdAt treated freshly registered users as having
"newer" profiles than users with real edit history, because createdAt is
always NOW at registration time. This broke federation profile sync:
the client correctly pushed home → remote, but the remote server's LWW
guard rejected the write (stored createdAt > incoming profileUpdatedAt).
A null profileUpdatedAt means "never edited" — that's timestamp 0, not
the user's creation time.
After deleting a federated identity, the server-side user_federation_registry
and users.replicated_instances were not cleaned up, causing "already connected"
errors when trying to re-federate. The deletion endpoint now authoritatively
removes both the registry row and the replicatedInstances entry, and bumps the
LWW timestamp to prevent stale client syncs from re-inserting them.
Also extends the endpoint to accept mode 'leave' (skip S2S, just clean up),
and enables the "Select instances..." scope option in DeleteIdentityDialog.
Switch from manual space-ID collection to collectDeletionBroadcastTargets and
add user_updated broadcast so clients patch their caches when a federated user
is deleted via S2S. Force-disconnect moved after broadcasts so other tabs
receive events before the connection is torn down.
- Orphaned DM channels (zero members) are unreachable garbage — clean
them up regardless of purgeContent mode, not just in full/nuke mode.
- Full/nuke mode now also deletes the user's space messages, their
attachments, and embeds. This is the meaningful distinction: "Delete
User" preserves all content as "Deleted User", "Nuke" removes it.
After a prior deletion + re-federation, multiple user records share
the same homeUserId (one deleted, one live). The unfiltered .get()
returned the older deleted record, causing the idempotency check to
short-circuit and miss the live record entirely.
When a federated user's identity has been tombstoned (isDeleted=1),
findFederatedUser filters them out, causing resolveOrCreateReplicatedUser
to silently create a new stub — a "zombie" resurrection. This guard checks
for a deleted row before creating a stub and returns null instead.
All 11 call sites across federation.ts and dm.ts have been updated with
appropriate null guards: federation relay handlers reject or skip the event
(participant_not_found / accepted no-op), while dm.ts routes convert null
to undefined so the existing 404 path handles it.
Aligns the ready payload query with the GET /api/dm approach:
get MAX(created_at) per channel first, then fetch the actual
message rows. Avoids issues with federated relay messages whose
local snowflake IDs don't match chronological order.