block / block/buzz

Desktop 0.4.26: after a relay reconnect the channel view can go permanently blank — resubscribe reuses the subscription ID, relay silently drops it, UI shows an empty channel with no error

Open
#3,265 4 comments 0 reactions 0 assignees View on GitHub
Dominant language
Rust
Stars
32.7k
Forks
4.3k
Avg merge
1d 13h
Merged PRs (30d)
253

Description

## Summary

On Buzz Desktop 0.4.26 (macOS 15.5, hosted relay `wss://*.communities.buzz.xyz`), a
channel intermittently renders **empty or frozen at an old message** while the relay
demonstrably holds newer events. Outgoing messages from the same app still publish
successfully. The connection indicator stays healthy. The only reliable recovery is a
full app quit + relaunch — reopening the channel, switching channels, and waiting do
nothing.

Because Desktop keeps no local message cache, a dead subscription is visually
**indistinguishable from an empty channel**. That is the part that makes this expensive:
there is no error, no banner, no retry affordance. A user reasonably concludes their
counterpart never replied.

## Reproduction

Not deterministic on demand — it follows a network interruption (sleep/wake, Wi-Fi
change, relay blip). Observed several times over 2026-07-27/28. Sequence:

1. Have a channel open with live traffic.
2. Interrupt connectivity long enough for the relay socket to drop.
3. Let Desktop reconnect on its own.
4. Have another client publish a kind-9 into that channel.

**Expected:** the message appears.
**Actual:** nothing appears, indefinitely. Meanwhile:

```console
$ buzz messages get --channel | tail
# the relay has the message; it was stored and served on REQ
```

Sending from the blind Desktop window still works — the event publishes and other
clients receive it. So it is live fan-out to *this* client that is dead, not the
connection.

## Diagnosis

Desktop links **rust-nostr `nostr-sdk` 0.44.1 / `nostr-relay-pool` 0.44.1** (crate paths
present in the shipped binary). That library's reconnect path re-issues each stored
subscription with **the same `SubscriptionId`** — the strings `Impossible to
resubscribe.`, `Removing subscription.`, `Timeout reached for subscription,
auto-closing.` and `Auto-closing subscription.` are all present in the 0.4.26 binary,
alongside Buzz's own `[relay_reconnect_hook]` and `Heartbeat: reconnected to …`.

That behavior is **correct per NIP-01** — a `REQ` reusing an existing subscription ID is
specified to replace that subscription, and reusing an ID after `CLOSE` is legal.

The problem is that the relay does not honor it. #3019 documents exactly this,
reproduced with minimal WebSocket clients:

> A `REQ` reusing a sub ID that was CLOSEd earlier on the same connection is silently
> ignored. […] Replacing (same-ID re-REQ) or CLOSEing overlapping subs kills delivery
> for survivors.

Chain that together and the symptom follows directly:

```
socket drops
→ nostr-relay-pool auto-closes / re-REQs with the same SubscriptionId
→ relay silently discards the resubscribe (no NOTICE, no CLOSED)
→ transport is healthy, heartbeat logs "reconnected"
→ zero live events forever
→ UI renders an empty channel
```

A fresh process gets fresh subscription IDs, which is precisely why quit + relaunch is
the only thing that clears it.

I have not attached a packet trace to Desktop, so treat the causal chain as a strong
inference from (a) the linked library's documented resubscribe semantics, (b) the
strings in the shipped binary, and (c) the independently reproduced relay behavior in
#3019 — not as instrumented proof of this specific window.

## Asks

The root-cause fix is relay-side and belongs to **#3019** — the relay should honor a
same-ID `REQ` as a replacement per NIP-01. This issue is about the two things Desktop
can do regardless of when that lands:

**1. Never render a silently-dead subscription as an empty channel.** This is the
high-value one. If a subscription has been re-issued and no `EOSE`/events have arrived
within a timeout, surface it — a "reconnecting…" or "connection stale, click to
reload" state. Anything is better than a blank channel that looks like nobody has
spoken. Related but distinct: #3224 notes mobile lacks WebSocket stall detection;
desktop has transport-level detection but nothing at the *subscription* level, which
is where this fails.

**2. Mitigate by generating a fresh subscription ID on reconnect.** #3019's own
write-up names the client pattern that works reliably against the current relay: one
never-reused, never-replaced, never-CLOSEd subscription per channel. This is a
workaround for a relay bug rather than a correctness fix, so it is secondary to (1) —
but it would make the symptom disappear for every user on a hosted relay today,
without waiting on a relay deploy.

A cheap third option, if a cache is on the roadmap anyway: hydrating the channel view
from stored events on open would make a dead live-sub degrade to "stale" instead of
"empty."

## Environment

- Buzz Desktop 0.4.26 (latest release at time of filing), macOS 15.5 (Darwin 25.5.0), Apple silicon
- Hosted relay, `wss://.communities.buzz.xyz`
- `nostr-sdk` 0.44.1, `nostr-relay-pool` 0.44.1, `nostr` 0.44.4 per binary strings
- buzz-acp managed agents on the same machine keep responding normally through the
outage — the agent harness resubscribes successfully where the UI does not, which is
another signal the failure is in the Desktop subscription lifecycle rather than the
network.

## Related

- #3019 — relay live fan-out: closed sub IDs poisoned, replace/CLOSE tears down overlapping delivery (root cause)
- #3206 — multi-channel `#h` filters silently receive zero channel-scoped events
- #3215 — Desktop reconnect replay re-alerts old thread replies (same reconnect path, different symptom)
- #3224 — mobile: no WebSocket stall detection

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.