Skip to content

Huddles overhaul: drain-safe rooms, bounded send latency, typed relay errors, live route switching - #6585

Draft
tlongwell-block wants to merge 6 commits into
mainfrom
huddles/overhaul
Draft

Huddles overhaul: drain-safe rooms, bounded send latency, typed relay errors, live route switching#6585
tlongwell-block wants to merge 6 commits into
mainfrom
huddles/overhaul

Conversation

@tlongwell-block

@tlongwell-block tlongwell-block commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

Huddles overhaul

One PR, bug-first, each commit independently green and revertible. Lead: Meli; implementation Max / Wren / Perci / Meli; independent review Mari (every commit cleared at ≥ 9/10 on minimalness / elegance / correctness before integration).

Plan: PLANS/HUDDLES_OVERHAUL_HANDOFF_2026_08_21.md + PLANS/HUDDLES_OVERHAUL_PLAN_DELTA_2026_08_22.md (in the Buzz workspace). Thread: #buzz-conversational-agents.

Integrated

# Commit Lane What
5 fix(desktop): bound huddle audio send latency Max Mic encoding no longer waits behind a stalled WebSocket; depth-4 drop-oldest send queue with seq/ts assigned before enqueue; sink errors propagate to the disconnect owner.
1 fix(relay): never auto-end a huddle on drain; hold empty rooms 20s Meli T0.1. Pod drain / owner-drain no longer archives + emits 48103 for every owned huddle; last-leaver departures get a 20 s generation-fenced grace window before auto-end (rejoin fences the stale end; the room stays pinned in the manager while a window is pending so a rejoiner always lands on the same Room; owner-loss or mid-window drain aborts).
3′ feat(desktop): seed huddle thread replies Wren Huddle-only placeholderData from the channel window so a new transcript root renders instantly while its authoritative fetch starts immediately. Projects path unchanged.
6 feat(desktop): polish huddle audio controls Max Typed AudioRelayConnectError { code, message } across the Rust boundary (reconnect can act on huddle_relay_draining); per-code friendly error copy; live output-device switching reaches peer playout, active TTS (mid-utterance), and STT coupling; one-time PTT hint.
2 feat(huddles): move audio reconnect into Rust with a 60 s window Meli T0.2. The audio pipeline task owns reconnect: ≥ 60 s window, 250 ms → 5 s jittered backoff, huddle_relay_draining → 250 ms redial, huddle identity re-checked after every dial and sleep; exhaustion → AudioLink::Lost (still joined, visible), never a silent leave. `HuddleState.audio_link: Live
AV feat(huddles): elect one agent voice publisher Perci Relay derives the agent-voice publisher role from verified NIP-OA auth + active bot membership (the client never declares it) and elects one publisher seat per agent identity, same-pod and cross-pod; rosters and ordered deltas carry the role; other desktops suppress only the authoritative publisher's local fallback and drop the utterance on a typed duplicate_identity election loss. relay_api.rs tests moved to relay_api_tests.rs (desktop 1000-line ratchet).

Pending (own branches, not yet cleared)

  • Commit 3b (Wren) — T0.6 remote-human STT attribution / one-transcriber-or-remove.
  • Commit 4 (Wren) — roster snapshot in indicator/strip, drop both polls (T0.3).

Dropped with receipts

  • T0.5 "transcript subtree vanishes" — root cause falsified at parent 4baccd539 (mounted-hook [A]→[B,A], add-root, invalidate/refetch all retain A). Not shipping a fix for a bug we cannot reproduce; symptom stays on the live-pass watch list.
  • Opus FEC / loss hint (5b) — this NetEq wrapper has no deadline-aware FEC seam; hand-rolled recovery displaced real reordered packets. Codec constants not changed.

Known flake (pre-existing, not touched here)

api::mesh_demo::tests::demo_join_forwarded_arm_round_trips_echo fails with 504-vs-200 under CPU contention (observed on parent 4baccd539 and on this branch while other builds ran; passes when the box is quiet). mesh_demo.rs / mesh_boot.rs reference no audio-room code.

Verification

Per-commit receipts are in the thread. At PR head: pre-push lanes (rust-tests, desktop-check, desktop-typecheck, desktop-test, desktop-tauri-checks, file-size-check, branch-skew) all green. Live two-client pass (GUIDES/HUDDLES_LOCAL_LIVE_PASS_2026_08_22.md) pending two desktop operators; staging has BUZZ_HUDDLE_AUDIO_AVAILABLE=false.

Co-authored-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Signed-off-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Meli and others added 3 commits August 22, 2026 20:14
A relay drain (SIGTERM or owner-drain) cancelled every local owner client
at once, so the last teardown emptied the room and took the archive +
48103 path: every pod restart ended every huddle it owned. The same path
also ended a huddle the instant its last client blipped, so a reconnect
found the channel archived.

The leave path now distinguishes a drain-driven teardown (the owner
draining token fired, or `shutting_down` / `owners.is_draining()` is set,
covering single-pod mode where there is no mesh token) and only releases
the lease and drops the empty room, leaving the channel alive for
rejoiners to re-acquire through Redis.

Ordinary last-leaver departures no longer end the room atomically.
`remove_peer_and_check_idle` captures an `IdleGeneration` (the admission
count) under the guard lock; a spawned task waits `ROOM_EMPTY_GRACE` and
calls `end_if_idle`, which sets `ended` only if the room is still empty
and no admission happened in between. A rejoin during the window fences
the stale observation even if the rejoiner has already left again, and
two leavers who both observed empty cannot both archive. Owner-loss or a
drain that begins mid-window aborts the end; owner-loss wins a tie with
the timer.

The fence only holds if the rejoiner lands on the same `Room`. A
pre-last leaver's `cleanup_if_empty` (or a failed pre-admission join)
could run after the last leaver's idle observation and evict the empty
room from the manager; the rejoiner would then get a fresh `Room` and the
stale grace task would archive underneath it. The idle observation now
sets `idle_hold` on the room and `cleanup_if_empty` refuses to evict
while it is set. The hold survives `end_if_idle` so the room stays
registered while `archive_channel` is in flight — a joiner in that gap
meets `ended` on the same room instead of admitting into a fresh one
whose pre-join DB check raced the archive. The hold clears on admission
or via the fenced `release_idle_hold`, called after the archive resolves
(success or rollback) and on the drain/abort paths that abandon the
window.

Tests: room idle generation (last-leaver-only, end-once, rejoin fences
stale), manager eviction (concurrent-leaver ordering pins the room and
the rejoiner fences the stale end; released hold permits eviction, stale
release does not; ended room stays pinned and refuses joiners until the
archive resolves; failed archive reopens and releases), and paused-time
grace outcomes (holds through window,
rejoin, no-rejoin ends, owner-loss aborts, mid-window drain aborts).

Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Render reply events already present in the bounded channel window while the
complete thread query loads. Keep the window data as placeholder-only so it
does not delay the authoritative request.

Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Signed-off-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Preserve typed relay admission codes through the audio connection boundary so
UI copy and reconnect policy can act on production handshake failures. Move
all active huddle playback consumers when the output route changes: remote
peers, agent TTS, and STT acoustic-coupling policy.

Co-authored-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Signed-off-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Perci and others added 2 commits August 22, 2026 20:55
Squash of 4baccd5..b92854104 (Perci's agent-voice lane). Relay derives
the publisher role from verified NIP-OA auth + active bot membership and
elects one publisher seat per agent identity, same-pod and cross-pod;
rosters and ordered deltas carry the role; desktop suppresses only the
authoritative publisher's local fallback and drops the utterance on a
typed election loss.

Integration note: the lane's standalone `AudioConnectError` enum was
folded into commit 6's typed `AudioRelayConnectError` as
`is_lost_election()` (code == "duplicate_identity") so the audio
handshake has one error type. `relay_api.rs` crossed the desktop
1000-line ratchet with the new tests, so its `tests` module moved to
`relay_api_tests.rs` via `#[path]`, matching `stt.rs`/`models.rs`.

Co-authored-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz>
Signed-off-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz>
An unexpected audio-relay disconnect used to emit
`huddle-audio-disconnected`, and HuddleContext.tsx answered with a
7-attempt loop (0/100/250/500/1000/2000/2000 ms, ~5.85 s total) that ended
in a silent `leaveHuddle()`. A relay deploy or a Wi-Fi roam that lasts
longer than that dropped the user out of the huddle with no explanation.

The audio pipeline task now awaits `reconnect::after_unexpected_disconnect`
in place of the event. `reconnect::run` redials for at least
`RECONNECT_WINDOW` (60 s) from the disconnect, with 250 ms → 5 s exponential
backoff jittered into [0.5, 1.0]; a `huddle_relay_draining` refusal redials
at the 250 ms floor because a replacement pod is coming. Huddle identity
(`is_current_huddle`) is re-checked after every dial and every sleep, so a
leave or a replacement huddle always wins and a socket won for the old
huddle is cancelled, never installed. Exhausting the window sets
`AudioLink::Lost` — still joined, visibly without audio — instead of leaving.

Progress is a new `HuddleState::audio_link` (`Live | Reconnecting{attempt,
draining} | Lost`), serde-tagged on `status`. It is a field, not a
`HuddlePhase` variant: `Connected | Active` is the liveness gate for
STT/TTS/agent voice, and a reconnecting phase would have stopped them
mid-blip. `claim_reconnect` flips `Live → Reconnecting` under the lock and
is the single guard against duplicate disconnect signals.

The `reconnect_huddle_audio` Tauri command and the React loop are deleted;
HuddleBar shows a muted pulsing "Reconnecting…" (or the relay-restart
variant) and a destructive "Couldn't reconnect audio" banner from the new
`audioLinkNotice`. Type mirrors in HuddleBar and the e2e bridge carry the
field.

`after_unexpected_disconnect` returns a boxed `Send` future because the call
graph is recursive (pipeline task → reconnect → `connect_audio_relay` →
pipeline task).

`connect_audio_relay` spawns the replacement pipeline before its handles
reach the reconnect loop. If that pipeline dies in the gap, its own
disconnect callback is refused by `claim_reconnect` (the link is already
`Reconnecting`), so the loop treats an already-cancelled token as a failed
dial and redials — `Live` never holds a dead sender.

Tests (paused tokio clock, injectable dial, each proven to fail when its
guard is removed): recovers after a 35 s outage without leaving, draining
redials promptly and is visible in state, leave during backoff exits the
loop, leave during an in-flight dial cancels the fresh socket, replacement
huddle is left untouched, a replacement pipeline that dies before install
is redialed not installed, duplicate signals do not start a second loop,
exhaustion marks Lost and terminates, not-live huddle is ignored, backoff
caps and jitters, `AudioLink` serialization. Node tests cover the banner
copy.

Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
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