fix(huddle): prevent phantom huddle from late-arriving relay events#344
Merged
tlongwell-block merged 1 commit intomainfrom Apr 16, 2026
Merged
fix(huddle): prevent phantom huddle from late-arriving relay events#344tlongwell-block merged 1 commit intomainfrom
tlongwell-block merged 1 commit intomainfrom
Conversation
When a relay-emitted 48102 (participant left) arrives after a client- emitted 48103 (huddle ended) — common because the WS disconnect happens ~1s after the client sends end_huddle — the reconstruct() function in HuddleIndicator would resurrect a phantom huddle. The 'infer huddle exists' fallback in the join/left handlers re-created the huddle when huddle was null, producing an empty participant set that displayed as 1 participant due to Math.max(1, 0). The fix tracks which ephemeral channel IDs have been ended. Join and left events for an ended channel are skipped instead of triggering the inference fallback. A new 48100 (started) event clears the ended state so the same ephemeral channel ID can be reused. The KIND_HUDDLE_ENDED handler is also made less restrictive — it now records the ended state even when huddle is already null, covering the case where the ended event survives in the subscription window but the corresponding started event does not. Confirmed against staging DB: the huddle-test channel had exactly this event sequence that triggered the bug: 48100 → 48101 → 48101 → 48102 → 48103 → 48102 The final 48102 after the 48103 was resurrecting the phantom huddle.
tlongwell-block
added a commit
that referenced
this pull request
Apr 17, 2026
* origin/main: nip-ab: clarify transcript_hash role and fix protocol diagram (#346) chore: fix deprecation warnings and decompose AgentsView (#347) chore: improve thread panel inline replies and nesting behavior (#339) feat: NIP-AB device pairing — Phase 2 (desktop + mobile UI) (#343) fix(huddle): prevent phantom huddle from late-arriving relay events (#344) perf(tts): reduce Kokoro time-to-first-audio with session warmup and threading (#342)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
After ending a huddle,
HuddleIndicatorshows a phantom huddle with 1 participant — the green headphone icon stays lit indefinitely even though no one is in the huddle.Root cause
The relay emits a
48102(participant left) when the audio WebSocket disconnects, which happens ~1 second after the client emits48103(huddle ended) viaend_huddle. Whenreconstruct()replays these events increated_atorder, the sequence is:The
48103correctly setshuddle = null. But the48102that follows hits the "infer huddle exists" fallback — whenhuddleis null, the join/left handlers assume the start event fell out of the subscription window and re-create the huddle. This produces anActiveHuddlewith an empty participant set, which displays as 1 participant due toMath.max(1, 0).Confirmed in staging
The
huddle-testchannel on Blox staging had exactly this event sequence in the DB:Fix
Track which ephemeral channel IDs have been ended in a local
endedChannelsSet during reconstruction. Join and left events for an ended channel are skipped instead of triggering the inference fallback.Three changes to
reconstruct():endedChannelsSet — tracks ephemeral channel IDs that have received a48103eventif (endedChannels.has(ephId)) breakskips the inference fallback for ended channelsENDEDhandler — now records the ended state even whenhuddleis already null (covers the case where the ended event survives in the subscription window but the corresponding started event does not)STARTEDclears ended state —endedChannels.delete(ephId)so a new huddle on the same ephemeral channel ID works correctlyThe
endedChannelsSet is function-local (rebuilt on everyreconstruct()call) and bounded by the subscription window (limit: 100events).Changes
desktop/src/features/huddle/components/HuddleIndicator.tsx— +16/-2 linesTesting
biome check— clean