fix(desktop): deduplicate relay outage notification - #3579
Conversation
Keep the reconnect card dismissed across retry-state churn and only re-arm it after a successful relay connection. Add coverage for cold-start failure, repeated retries, recovery, and a second outage. Signed-off-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
Treat community lifecycle changes and idle teardown as explicit outage boundaries so same-URL community switches cannot preserve a dismissed outage. Signed-off-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
Chessing234
left a comment
There was a problem hiding this comment.
Sensible outage-episode model. Keeping the reconnect toast dismissed for one contiguous unreachable stretch (and re-arming on recovery / community switch even when the URL is unchanged) matches how operators experience a flaky relay. Glad the dedicated auth/application-error path is preserved. The e2e coverage on the sidebar card helps. One watch-item: confirm outageActiveRef resets when the hook unmounts between community switches that remount AppShell children — the description says it does via lifecycle replacement; if that's only a URL-keyed effect, a same-URL remount edge case could leave it sticky.
wpfleger96
left a comment
There was a problem hiding this comment.
Reviewed exact remote head 2d5916517d0e6bd309bb53a3b58d16e22337c1f0.
Review
No blocking or non-blocking findings.
What's Solid
- The outage latch models the user-visible invariant directly: only the first degraded observation opens an episode, so
connecting/reconnecting/stalled/disconnectedretry churn cannot clear a dismissal. - Recovery is authoritative:
connectedcloses the episode and re-arms the next outage.idleteardown and the community ID +reinitKeylifecycle identity handle singleton replacement and same-URL community transitions without carrying stale dismissal state. - The existing auth/application-error split is preserved: terminal
disconnectedwith a non-unreachable error still stays on the dedicated error path. - The state remains hook-local and ephemeral; no persistence or new cross-module store was added.
- The tests cover cold-start failure, dismissal through retry churn, recovery, a second outage, and lifecycle teardown. Existing success-card, stale-error suppression, collapsed-sidebar, stalled-state, and auth-error behaviors remain covered.
- All GitHub checks are green. I independently ran
just desktop-test(3,769 passed, 0 failed) andpnpm build:e2e && pnpm exec playwright test tests/e2e/sidebar-relay-card.spec.ts --project=integration(11 passed, 0 failed) at exact head2d5916517d0e6bd309bb53a3b58d16e22337c1f0; the SHA was unchanged before and after both runs.
Alternatives
None preferable. A reducer or module-level outage store would add machinery without improving the hook-local lifecycle contract.
Quality Gate
- Minimalism: 9/10 — one lifecycle input, two refs, and one consolidated effect; each addition enforces an explicit outage boundary.
- Elegance: 9/10 — the episode transitions read in the same order as the product behavior: lifecycle reset, teardown, outage start, recovery.
- Correctness: 9/10 — cold start, retry churn, recovery/re-arm, lifecycle replacement, stale errors, and auth failures are handled and behaviorally covered.
Verdict: Approve.
…-style * origin/main: fix(desktop): deduplicate relay outage notification (block#3579) fix(desktop): reconcile thread arrivals at bottom (block#3585) Signed-off-by: Joah Gerstenberg <joah@squareup.com>
## Buzz Desktop release v0.5.2 ### Changes since v0.5.1: - feat(cli): mirror Desktop mention delivery ([#3330](#3330)) ([`7adc46268`](7adc462)) - fix(desktop): deduplicate relay outage notification ([#3579](#3579)) ([`66e705492`](66e7054)) - fix(desktop): reconcile thread arrivals at bottom ([#3585](#3585)) ([`b42a8d447`](b42a8d4)) - Improve emoji autocomplete matching ([#3571](#3571)) ([`259de6afb`](259de6a)) - Fix shared agent avatar import profiles ([#3578](#3578)) ([`324bd6b46`](324bd6b)) - Fix inline raster avatars in agent catalog ([#3581](#3581)) ([`7e9b77f72`](7e9b77f)) - feat(agent): make Gemini and MLflow-route models usable through databricks_v2 ([#3569](#3569)) ([`4a1ebf25c`](4a1ebf2)) **To release:** merge this PR. The tag and build will happen automatically. Signed-off-by: Wes <wesbillman@users.noreply.github.com>
* origin/main: (29 commits) feat(replica): portable heartbeat-token fence with snapshot-local reader routing (#3268) fix(git): channel binding tooling + author remediation for unbound repos (#3626) feat: configure S3 URL addressing style (#3400) feat: add first-class OpenRouter provider support (#1975) feat(agent,acp): wire provider total_tokens through NIP-AM publish chain (#3593) chore(release): release Buzz Desktop version 0.5.2 (#3624) docs: add Linux rendering troubleshooting guide (#3573) fix(desktop): discover bun-installed agent CLIs in ~/.bun/bin (#3343) feat(tracing): correlate trace IDs in relay logs (#3608) chore(ci): bump Linux AppImage build container to ubuntu:24.04 (#3602) feat(cli): mirror Desktop mention delivery (#3330) fix(desktop): deduplicate relay outage notification (#3579) fix(desktop): reconcile thread arrivals at bottom (#3585) fix(mobile): keep TLS on relays joined by invite (#3139) Improve emoji autocomplete matching (#3571) Fix shared agent avatar import profiles (#3578) Fix inline raster avatars in agent catalog (#3581) revert(acp): remove dead GOOSE_ACP_SCHEDULER_DISABLED env injection (#3576) feat(agent): make Gemini and MLflow-route models usable through databricks_v2 (#3569) fix(cli): mask credential env values in --help output (#3570) ... Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
* feat(storage): add Azure Blob conformance adapter Signed-off-by: Ross Hartmann <RossHartmann@users.noreply.github.com> * Improve emoji autocomplete matching (block#3571) ## Summary - Show all colon emoji autocomplete matches - Rank exact and prefix shortcodes before weaker matches - Add a regression test and screenshot ## Validation - `pnpm test` - `pnpm build` - `pnpm exec playwright test --project=smoke tests/e2e/custom-emoji.spec.ts --grep "exact standard shortcode"` - `just desktop-tauri-clippy` Native Tauri tests were attempted but could not link because the local disk filled during compilation. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> * fix(mobile): keep TLS on relays joined by invite (block#3139) ## Summary Communities joined via an invite link never connect: the app dials `ws://` on port 80 instead of `wss://` on 443 and sits on "Reconnecting…" indefinitely. `RelayConfig.baseUrl` is documented as an HTTP origin, but the two onboarding flows disagree on what they persist: - **Device pairing** validates and stores `https://` — `pairing_provider.dart:657` throws on anything else. - **Invite join** stores the relay URL straight off the invite link, and `deep_link.dart:165` always emits `ws://` or `wss://`. `wsUrl` only special-cased `https://`, so a `wss://` base fell through to the plaintext branch: ```dart final scheme = uri.scheme == 'https' ? 'wss' : 'ws'; // 'wss' is not 'https' ``` The claim request itself succeeds, because `_claimUrlFromRelay` (`invite_join_provider.dart:242`) maps `wss → https` explicitly. Only the socket path is missing that conversion — which is why the community appears, correctly named, and then never loads. The same `baseUrl` also feeds `/query` (`relay_session.dart:136`), media upload (`media_upload.dart:765`), Blossom auth (`media_auth.dart:128`) and `relayClientProvider` (`relay_provider.dart:113`), so those requests were malformed too. Where port 80 *does* answer, it is additionally a silent TLS downgrade after `validateInviteRelayUri` insisted on `wss://`. This folds the websocket schemes back to their HTTP equivalents in `baseUrl` itself, so every consumer is correct by construction rather than needing a second getter remembered at each call site, and communities **already persisted** with `wss://` are repaired on read without a migration. `community_icon_provider.dart:46` already performs this same conversion locally. One subtlety worth flagging for review: the normalization is derived in the getter rather than applied in the constructor, so the constructor stays `const`. The compile-time fallback at `relay_provider.dart:77` relies on const canonicalization for a stable identity across rebuilds, and Riverpod's `defaultUpdateShouldNotify` is `previous != next` (`element.dart:361`), which falls back to identity for this class. A `factory` constructor here yields a fresh instance per rebuild, which tears down and resubscribes every listener — `channels_provider_test.dart` catches it as an unexpected unsubscribe during reconnect. ### Related issue Fixes block#2662. ### Testing `flutter test` — **705 passed, 1 skipped, 0 failed** `flutter analyze` — No issues found `dart format --set-exit-if-changed .` — 249 files, 0 changed Run against the Hermit-pinned SDK (Flutter 3.41.7 / Dart 3.11.5), matching CI. 10 new unit tests in `mobile/test/shared/relay/relay_config_test.dart` covering both onboarding schemes, `http`/`https` passthrough, non-default ports, and agreement between the invite and pairing paths for the same relay. Verified end-to-end against a self-hosted relay behind `tailscale serve`, which terminates TLS on 443 and leaves port 80 closed. Relay logs show the invite claim succeeding over HTTPS at the moment of joining, while no WebSocket connection ever arrives — no `WebSocket connection established`, no NIP-42 auth, no `kind:0` profile, no push registration — across the relay's entire history, even though the member row is present and correct. Port-80 refusals are not logged by `tailscaled`'s netstack, which is why the retries leave no trace server-side. Reproduced on both iOS and Android. --------- Signed-off-by: Krishna C <github@kumb.uk> * fix(desktop): reconcile thread arrivals at bottom (block#3585) ## Summary - reconcile stale native-scroll anchors when a reply arrives at the physical floor - clear the thread new-message affordance instead of incrementing it from stale cached state - preserve the existing mid-history path and add direct lifecycle regression coverage ## Why PR block#3411 fixed geometry-driven reconciliation, but the reply-arrival branch still trusted a cached `message` anchor without checking the rendered position. Native anchoring could return a short thread to the floor without another scroll/resize callback, then the next reply incremented the pill anyway. ## Verification - Desktop checks passed - Desktop typecheck passed - focused lifecycle test passed (6/6) - push hook full Desktop unit suite passed (3,770/3,770) - `git diff --check` passed Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> * fix(desktop): deduplicate relay outage notification (block#3579) 🤖 ## Summary - Keep the relay reconnect notification dismissed across repeated connection retries during one continuous outage. - Re-arm the notification after recovery or relay lifecycle replacement, including switches between communities that use the same relay URL. - Preserve the existing dedicated path for authentication and other application-level errors. ### How an outage is tracked The AppShell-owned relay-card hook treats an outage as one contiguous runtime episode rather than assigning it a persisted ID. A hook-local `outageActiveRef` is armed by the first qualifying unreachable/degraded observation. While it is armed, intermediate retry states (`connecting`, `reconnecting`, `stalled`, and `disconnected`) belong to that same episode, so retry churn cannot clear dismissal or emit another notification. The hook receives the same lifecycle identity used by community initialization: community ID plus `reinitKey`. This distinguishes multiple communities even when they share a relay URL, and it also changes when the active community is explicitly reinitialized. The latch and dismissal are reset when that identity changes or when the relay singleton reports its authoritative `idle` teardown state. A successful `connected` state also closes the episode and re-arms the next outage. These boundaries deliberately bias toward re-notifying rather than suppressing a later outage: recovery, community switch/reinit, or relay teardown cannot leave the hook stuck believing an old outage is still active. No outage state is persisted beyond the mounted hook lifecycle. ### Related issue None found. ### Testing - `pnpm --dir desktop typecheck` - `pnpm --dir desktop test` — 3,769 passed - `pnpm --dir desktop check` - `pnpm --dir desktop build:e2e` - `pnpm --dir desktop exec playwright test tests/e2e/sidebar-relay-card.spec.ts --project=integration` — 11 passed --------- Signed-off-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz> Co-authored-by: npub13n66s06epmqf2kc3v373ez8hj65cuzyvxzjf93vwpervxqn2u7jq2qd9je <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz> * feat(cli): mirror Desktop mention delivery (block#3330) 🤖 ## Summary Agent-authored mentions currently depend on matching visible `@Name` text to channel profiles. That makes notification delivery ambiguous when names collide or profiles change, and it encourages an extra post-send lookup just to confirm that the intended `p` tags were emitted. This change makes `buzz messages send` mirror Desktop's existing model: the message keeps a readable name in its content while the recipient pubkey is supplied separately. ```bash buzz messages send \ --channel <UUID> \ --content '@alice could you review this?' \ --mention <alice-hex-or-npub> ``` `--mention` is repeatable. The CLI normalizes and deduplicates explicit pubkeys, merges them with any names it can resolve from the channel, and gives explicit identities priority under the existing 50-mention limit. Before uploading attachments, signing, or publishing, the command checks every resulting pubkey against the channel's current membership: - Members are mentioned normally. - Non-members stop the send and produce an actionable error. - `--allow-non-member-mentions` deliberately sends notifying `p` tags without adding anyone to the channel. Sending a message never changes membership. On success, `mention_pubkeys` is read from the exact signed event and returned with the relay response, so callers can verify the emitted recipients without another query. Managed-agent guidance teaches this single-command mention flow. Desktop mention behavior and the Nostr event schema are unchanged. Forum guidance is intentionally handled separately in block#3596. ### Related issue None found. This replaces the earlier guidance-only approach in this PR with the underlying CLI behavior it required. ### Testing - `cargo test -p buzz-sdk` - `cargo test -p buzz-cli` - `cargo test -p buzz-acp` - `cargo test --manifest-path desktop/src-tauri/Cargo.toml` --------- Signed-off-by: npub1fdupjvyregj3z2tx7gx5x6py04zw89jm5usef9lyea4f3vcgh8qq9zgkdz <4b78193083ca25112966f20d4368247d44e3965ba7219497e4cf6a98b308b9c0@buzz.block.builderlab.xyz> Signed-off-by: npub13n66s06epmqf2kc3v373ez8hj65cuzyvxzjf93vwpervxqn2u7jq2qd9je <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz> Co-authored-by: npub1fdupjvyregj3z2tx7gx5x6py04zw89jm5usef9lyea4f3vcgh8qq9zgkdz <4b78193083ca25112966f20d4368247d44e3965ba7219497e4cf6a98b308b9c0@buzz.block.builderlab.xyz> Co-authored-by: npub13n66s06epmqf2kc3v373ez8hj65cuzyvxzjf93vwpervxqn2u7jq2qd9je <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz> * chore(ci): bump Linux AppImage build container to ubuntu:24.04 (block#3602) ## What and why The Buzz AppImage is built on `ubuntu:22.04`, which links WebKitGTK against FreeType 2.11.1. Because `libfreetype.so.6` is on the linuxdeploy community excludelist, the bundled WebKit loads the **host's** FreeType at runtime instead of the bundled one. FreeType 2.13.0 (released 2023-02-09) added `FT_Bool read_variable` to `FT_ColorStopIterator`, growing the struct from 16 to 20 bytes. Any host running FreeType ≥ 2.13 (Fedora 42+, Ubuntu 24.04+) has a struct-layout mismatch with the 22.04-compiled WebKit. The mismatched offsets corrupt color-stop index arithmetic inside Skia's COLRv1 renderer, producing the assertion abort in issues block#2548 and block#2982: ``` stl_vector.h:1123: Assertion '__n < this->size()' failed. ... colrv1_configure_skpaint(FT_Face, ...) ... ``` ## Fix Bump the build container to `ubuntu:24.04` (noble), which ships FreeType **2.13.2**. Noble's struct layout matches every crash-affected host. The ABI mismatch disappears and the crash is eliminated at root. WebKitGTK also advances from **2.50.4** (jammy backport) to **2.52.3** (noble backport). ## Glibc floor change | Build base | glibc floor | Oldest supported AppImage distro | |---|---|---| | ubuntu:22.04 (before) | 2.35 | Ubuntu 22.04 LTS, Debian 12 | | ubuntu:24.04 (after) | 2.39 | Ubuntu 24.04 LTS, Fedora 40+ | Ubuntu 22.04 LTS and Debian 12 users lose AppImage support. Both distributions continue to receive first-class `.deb` / `.rpm` packages, which use the system WebKit and are unaffected. The crash-affected users (Fedora 42/44, Ubuntu 24.04+) all have glibc ≥ 2.39. ## Changes - `.github/workflows/linux-canary.yml:24` — container pin updated to `ubuntu:24.04@sha256:4fbb8e6a…` - `.github/workflows/release.yml:479` — same container pin updated - `.github/workflows/release.yml:501` — comment version string updated from 22.04 to 24.04 `fix-appimage.sh` and `desktop/src-tauri/**` are untouched. The block#3573 fontconfig stopgap remains active; retirement is a separate follow-on PR once this fix is verified on a shipped build. ## Sequencing `docs/linux-rendering-troubleshooting.md` (introduced in block#3573) will receive a glibc-floor callout section once block#3573 merges — adding it here would conflict with block#3573's open branch. Context: block#2548, block#2982. Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> * feat(tracing): correlate trace IDs in relay logs (block#3608) ## Summary Correlates trace + span IDs with logs, allowing traces and logs to be bridged seamlessly ### Related issue none found ### Testing Unit tests Signed-off-by: David Grochowski <dgrochowski@squareup.com> Co-authored-by: Amp <amp@ampcode.com> * feat(storage): productionize Azure Blob backend Signed-off-by: Ross Hartmann <1750494+RossHartmann@users.noreply.github.com> * feat(acp): bridge Buzz to exact-user Kiingo Compute Signed-off-by: Ross Hartmann <1750494+RossHartmann@users.noreply.github.com> * feat(deploy): add Azure production packaging Signed-off-by: Ross Hartmann <1750494+RossHartmann@users.noreply.github.com> * feat(acp): complete Buzz identity linking Signed-off-by: Ross Hartmann <1750494+RossHartmann@users.noreply.github.com> * feat(acp): execute scoped Buzz actions locally Signed-off-by: Ross Hartmann <1750494+RossHartmann@users.noreply.github.com> * feat(acp): preserve terminal stop reasons Signed-off-by: Ross Hartmann <1750494+RossHartmann@users.noreply.github.com> * feat(desktop): add Kiingo subscription enrollment release * test(storage): run conformance against private Azure * build(storage): add private Azure conformance job * test(storage): rehearse Azure version recovery * fix(ci): keep Buzz desktop and Rust checks green * fix(deploy): keep Azure secrets inside Key Vault * test(deploy): verify Key Vault CSI delivery * fix(ci): resolve mesh checkout from Cargo metadata * fix(ci): keep fork pull requests read-only * fix(ci): fetch the exact mesh source when Cargo is sparse --------- Signed-off-by: Ross Hartmann <RossHartmann@users.noreply.github.com> Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Krishna C <github@kumb.uk> Signed-off-by: Wes <wesbillman@users.noreply.github.com> Signed-off-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz> Signed-off-by: npub1fdupjvyregj3z2tx7gx5x6py04zw89jm5usef9lyea4f3vcgh8qq9zgkdz <4b78193083ca25112966f20d4368247d44e3965ba7219497e4cf6a98b308b9c0@buzz.block.builderlab.xyz> Signed-off-by: npub13n66s06epmqf2kc3v373ez8hj65cuzyvxzjf93vwpervxqn2u7jq2qd9je <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: David Grochowski <dgrochowski@squareup.com> Signed-off-by: Ross Hartmann <1750494+RossHartmann@users.noreply.github.com> Co-authored-by: Ross Hartmann <RossHartmann@users.noreply.github.com> Co-authored-by: klopez4212 <klopez4212@gmail.com> Co-authored-by: Krishna C <github@kumb.uk> Co-authored-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> Co-authored-by: Logan Johnson <loganj@squareup.com> Co-authored-by: npub13n66s06epmqf2kc3v373ez8hj65cuzyvxzjf93vwpervxqn2u7jq2qd9je <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz> Co-authored-by: npub1fdupjvyregj3z2tx7gx5x6py04zw89jm5usef9lyea4f3vcgh8qq9zgkdz <4b78193083ca25112966f20d4368247d44e3965ba7219497e4cf6a98b308b9c0@buzz.block.builderlab.xyz> Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Dave Grochowski <ThePumpingLemma@users.noreply.github.com> Co-authored-by: Amp <amp@ampcode.com>
🤖
Summary
How an outage is tracked
The AppShell-owned relay-card hook treats an outage as one contiguous runtime episode rather than assigning it a persisted ID. A hook-local
outageActiveRefis armed by the first qualifying unreachable/degraded observation. While it is armed, intermediate retry states (connecting,reconnecting,stalled, anddisconnected) belong to that same episode, so retry churn cannot clear dismissal or emit another notification.The hook receives the same lifecycle identity used by community initialization: community ID plus
reinitKey. This distinguishes multiple communities even when they share a relay URL, and it also changes when the active community is explicitly reinitialized. The latch and dismissal are reset when that identity changes or when the relay singleton reports its authoritativeidleteardown state. A successfulconnectedstate also closes the episode and re-arms the next outage.These boundaries deliberately bias toward re-notifying rather than suppressing a later outage: recovery, community switch/reinit, or relay teardown cannot leave the hook stuck believing an old outage is still active. No outage state is persisted beyond the mounted hook lifecycle.
Related issue
None found.
Testing
pnpm --dir desktop typecheckpnpm --dir desktop test— 3,769 passedpnpm --dir desktop checkpnpm --dir desktop build:e2epnpm --dir desktop exec playwright test tests/e2e/sidebar-relay-card.spec.ts --project=integration— 11 passed