Skip to content

fix(huddle): prevent phantom huddle from late-arriving relay events#344

Merged
tlongwell-block merged 1 commit intomainfrom
fix/phantom-huddle-after-end
Apr 16, 2026
Merged

fix(huddle): prevent phantom huddle from late-arriving relay events#344
tlongwell-block merged 1 commit intomainfrom
fix/phantom-huddle-after-end

Conversation

@tlongwell-block
Copy link
Copy Markdown
Collaborator

Problem

After ending a huddle, HuddleIndicator shows 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 emits 48103 (huddle ended) via end_huddle. When reconstruct() replays these events in created_at order, the sequence is:

48100 started → 48101 joined → ... → 48103 ended → 48102 left

The 48103 correctly sets huddle = null. But the 48102 that follows hits the "infer huddle exists" fallback — when huddle is null, the join/left handlers assume the start event fell out of the subscription window and re-create the huddle. This produces an ActiveHuddle with an empty participant set, which displays as 1 participant due to Math.max(1, 0).

Confirmed in staging

The huddle-test channel on Blox staging had exactly this event sequence in the DB:

48100 started  (21:41:46)
48101 joined   (21:41:47)
48101 joined   (21:53:17)  ← reconnect
48102 left     (21:53:44)
48103 ended    (21:59:56)  ← client-side end_huddle
48102 left     (21:59:57)  ← relay-emitted, 1s later — resurrects phantom

Fix

Track which ephemeral channel IDs have been ended in a local endedChannels Set during reconstruction. Join and left events for an ended channel are skipped instead of triggering the inference fallback.

Three changes to reconstruct():

  1. endedChannels Set — tracks ephemeral channel IDs that have received a 48103 event
  2. Guard on join/left handlersif (endedChannels.has(ephId)) break skips the inference fallback for ended channels
  3. Less restrictive ENDED handler — now records the ended state even when huddle is already null (covers the case where the ended event survives in the subscription window but the corresponding started event does not)
  4. STARTED clears ended stateendedChannels.delete(ephId) so a new huddle on the same ephemeral channel ID works correctly

The endedChannels Set is function-local (rebuilt on every reconstruct() call) and bounded by the subscription window (limit: 100 events).

Changes

  • desktop/src/features/huddle/components/HuddleIndicator.tsx — +16/-2 lines

Testing

  • biome check — clean
  • Codex CLI review: APPROVED 9/10
  • Verified against the exact event sequence from the staging DB

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 tlongwell-block merged commit 0c5904b into main Apr 16, 2026
10 checks passed
@tlongwell-block tlongwell-block deleted the fix/phantom-huddle-after-end branch April 16, 2026 22:19
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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant