The first live subscription: kind 1059 p-tagged to us, on every DM relay, open
until the wallet changes or the process ends. That covers direct messages and
the Marmot Welcome events that make us a member of a group — so a DM now lands
whether or not anyone has the chat list open, and a group invite is noticed
without waiting for the next screen to schedule a sync.
Nothing is removed from the queue yet. ChatRoomListViewModel and
ChatMessageListViewModel still schedule what they always did; this runs beside
them and the duplicate events cost a no-op storeNostrEvent each. The screens
stop doing it in the last step, once groups are covered too.
**No `since`.** NIP-59 randomizes a wrap's created_at into the past, and our own
outbound path stamps them with TimeUtils.randomWithTwoDays() — so a wrap
published right now can carry a timestamp two days old, and a `since` anywhere
near the present would silently drop it. "Some messages just never arrive" is
the worst failure mode to debug, and re-receiving a wrap costs one no-op write.
**`limit` instead.** NIP-01 scopes limit to the initial query — the stored
events a relay sends before EOSE — and explicitly not to the stream that
follows, so limit=100 bounds what a reconnect costs without touching the live
tail. Filling in the rest of the history stays negentropy's job.
**What ends a subscription, and what doesn't.** CLOSED does, and is retried with
backoff from 5s to 5m, jumping straight to the maximum when the reason parses as
back-pressure — answering "too many subscriptions" by promptly opening another
is how one refusal becomes a flood. EOSE does not end anything: it is the
boundary between stored history and the live tail, and treating it as an end is
precisely what made every sync a poll. It does reset the backoff, since a
subscription that got that far was working and whatever ended it is a new
problem. A dropped socket needs nothing from this loop at all — the pool replays
the REQ on reconnect and the collector, attached to the socket client rather
than to a session, simply starts receiving again.
A NOTICE is logged and otherwise ignored. It carries no subscription id, so it
reaches every collector on the socket; acting on one here would let an unrelated
relay complaint tear this subscription down.
Events are saved inline rather than in a launched coroutine, so they land in the
order the relay sent them and the socket's buffer does the back-pressure,
instead of fanning a burst out into a coroutine per event.
Wiring: LiveSubscriptionManager is built by SynchronizationViewModel because it
has to share that class's RelaysSocketManager — a second one would mean a second
RelayPool and a duplicate socket per relay — and is launched inside the same
per-wallet supervisorScope as the three pumps, so a wallet switch tears it down.
It deliberately does not take from the pumps' Semaphore(4): a subscription that
never finishes would hold a permit forever and permanently halve backfill
throughput. The comment there now says so.
isBackPressure moves out of SynchronizationViewModel to
network/relays/RelayBackPressure.kt, unchanged, now that two callers need it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>