Files
buzz/mobile/pubspec.yaml
ce56e34411 fix(mobile): recover stale relay sessions (#4372)
### Summary

Fixes [this
issue](buzz://message?channel=e62570dd-33ad-42c5-b92b-75f2689f9694&id=b726c366abfe62429ee3cdcd34d0c0fb98c33c3ea053480585bed71745412b56):
> I often don’t see my bot responses until after I post. they’re usually
time stamped correctly so I think it’s just a refresh issue?

### What changed?

Buzz Mobile now reconnects relay sessions after the app has remained
backgrounded beyond the existing 5-second grace period, even when the
session still reports a stale `connected` state. This makes resume
recovery independent of whether iOS runs the grace timer before or after
delivering `resumed`.

Reconnection is now based on elapsed background time rather than a
direct socket-health probe.
- If the app was backgrounded for at least the 5-second grace period,
the socket is presumed dead and the session reconnects regardless of
reported status.
- If it was backgrounded for less than that, a reported `connected`
status is still trusted.

In the sub-5-second window the socket is either genuinely alive, which
is the common case for a momentary background, or it is dead and the
client ping detects it within the two-interval worst case described
below. That is now a degraded-latency path, not a silent-forever path.

The mobile relay socket now uses `IOWebSocketChannel.connect` with a
30-second `pingInterval`. An unanswered ping closes the Dart socket
through the existing disconnect and reconnect path.

Detection takes up to two ping intervals, so about 60 seconds worst
case, not 30. One interval of idleness elapses and a ping is sent, then
a second interval elapses with no pong and the socket closes. Any
inbound pong restarts the first stage, so the clock measures idleness
rather than running on a fixed cadence.

### Why?

Buzz iOS can sometimes stop showing new bot or agent responses after a
phone has been locked for 5 to 10 minutes. When the user later posts a
message, the missing responses can appear all at once. iOS may suspend
Buzz before the short delayed cleanup that would normally close its
connection has a chance to run. Before this change, Buzz trusted the
resulting stale healthy status on resume and skipped reconnecting, so
the missing responses stayed hidden until a later post exposed the dead
connection.

A state-machine test with a stubbed connection reproduced this reported
pattern and showed that it matches this failure mode: the failed post
triggered a reconnect that fetched the missing messages. The same test
also checked the other candidate explanation, the bug tracked in
[#3053](https://github.com/block/buzz/pull/3053), where the relay has
closed the app's subscription. That state does not produce the pattern.
Posting succeeds and the user's own message appears, but nothing looks
for the missed messages, so they stay hidden. The test confirmed that
the missed messages were still available to fetch in that state, so the
missing step was a trigger to fetch them. This was not an end-to-end
reproduction on an iOS device or a live relay.

The new resume check covers the normal lock and unlock path. If the app
was backgrounded for less than the 5-second grace period, it still
trusts a connection marked as healthy. A dead connection in that window
is instead detected by the ping check, which can take up to about 60
seconds but prevents the app from remaining silently stuck. The ping
only runs while iOS is running the app, so it does not detect a
connection that died during suspension; the resume check owns the lock
and unlock path.

A pre-existing path also runs the same resume handling when network
connectivity returns while the app is already in the foreground. Because
the app was not backgrounded, this change does not alter that path,
which still trusts a connection marked as healthy and relies on the
slower ping check.

Recovery from a subscription that the relay explicitly closes remains in
[#3053](https://github.com/block/buzz/pull/3053), and the two changes
overlap in one file. Changes to how missed messages are backfilled or
replayed are out of scope.

### How is it tested?

Full mobile suite at base and head. Both runs have the same known
macOS-host-only failure in `ChannelDetailPage keeps follow mode off
while a tall newest message stays visible` at line 1053:

- Base: 1,021 passed, 1 skipped, 1 failed
- Head: 1,025 passed, 1 skipped, 1 failed

Added tests:

-
[`relay_session_test.dart`](https://github.com/block/buzz/tree/main/mobile/test/shared/relay/relay_session_test.dart):
long-background resume reconnect and within-grace control
-
[`relay_socket_liveness_test.dart`](https://github.com/block/buzz/tree/main/mobile/test/shared/relay/relay_socket_liveness_test.dart):
silent-peer disconnect and idle-but-healthy control

Mutation checks confirm that removing elapsed-background resume recovery
fails with one socket instead of two, and removing `pingInterval` leaves
the silent peer connected. Restored production code passes both
mutations' regression tests and the healthy idle control.

Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: npub1tquskdu6yc4h8l7xxtceculxw600grekeq0xg2ukqfrwl7vrzg3quz3gmp <58390b379a262b73ffc632f19c73e6769ef40f36c81e642b960246eff9831222@buzz.block.builderlab.xyz>
2026-08-03 12:28:50 -07:00

73 lines
1.8 KiB
YAML

name: buzz
description: Buzz mobile client
publish_to: 'none'
version: 0.0.0+1
environment:
sdk: ^3.11.4
dependencies:
flutter:
sdk: flutter
# Grapheme-cluster segmentation, for deciding whether a message body is
# nothing but emoji. Already present transitively via the Flutter SDK;
# declared here because we import it directly.
characters: ^1.4.0
hooks_riverpod: ^3.0.3
flutter_hooks: ^0.21.3
lucide_icons_flutter: ^3.1.0
http: ^1.4.0
flutter_secure_storage: ^10.0.0
flutter_svg: ^2.2.4
mobile_scanner: ^7.0.0
shared_preferences: ^2.5.5
web_socket_channel: ^3.0.1
connectivity_plus: ^7.0.0
nostr: ^2.0.0
pointycastle: ^4.0.0
url_launcher: ^6.3.2
gpt_markdown: ^1.1.6
highlight: ^0.7.0
intl: ^0.20.2
uuid: ^4.5.1
file_selector: ^1.1.0
camera: ^0.12.0+2
image_picker: ^1.1.2
photo_manager: ^3.11.0
video_player: ^2.10.1
package_info_plus: ^10.0.0
app_badge_plus: ^1.2.10
app_links: ^6.4.0
scrollable_positioned_list: ^0.3.8
open_filex: ^4.7.0
path_provider: ^2.1.6
share_plus: ^13.3.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
crypto: ^3.0.7
custom_lint: ^0.8.0
riverpod_lint: ^3.1.0
mocktail: ^1.0.4
flutter:
uses-material-design: true
assets:
- assets/images/buzz-icon.png
# Generated by `just mobile-emoji-data` from the same emoji-mart dataset
# desktop uses. Do not hand-edit.
- assets/emoji/emoji-data.json
fonts:
- family: Inter
fonts:
- asset: assets/fonts/InterVariable.ttf
- asset: assets/fonts/InterVariable-Italic.ttf
style: italic
- family: GeistMono
fonts:
- asset: assets/fonts/GeistMono-Variable.ttf
- asset: assets/fonts/GeistMono-Italic-Variable.ttf
style: italic