Repository navigation
fix(relay): skip full profile and roster re-reads after a continuous reconnect - #2203
Merged
Merged
Conversation
After every reconnect the session re-read every held profile in pages of 500 authors, even after a short outage. The live profiles route already resumes from the time its connection last heard the relay (minus the drift lag), so a replay that ends below the limit has delivered every profile change in the gap. live.ts now reports, for each global route it establishes, whether it stayed continuous: every key had a cursor when the route was created and its replay ended below LIVE_REPLAY_LIMIT. The dev broker carries the flag across the host boundary. The session re-reads held profiles only when the profiles route was not continuous; a first connection, a cut replay or a host that reports nothing keeps today's full re-read. The roster re-read is unchanged: a deleted channel (kind 9008) and a relay-opened moderation DM send no membership notice, so the live membership route cannot yet stand in for it. Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
…rted Pre-PR review found two paths where the session skipped the profile re-read it promised. A connection's first global establishment (often membership) consumed the per-connection marker, so a profiles report that arrived later without valid continuity read nothing. And a profiles route that failed before EOSE never established, so a reconnect read no profiles at all, where it used to read them on any global establishment. The session now settles profiles per connection with its own marker: a continuous profiles report needs no read, a cut one reads, and a missing report or a failed profiles route reads once on that connection, in any arrival order. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Signed-off-by: Logan Johnson <loganj@squareup.com>
The session read the whole channel list on every reconnect and on every window focus. The live membership route already replays each membership notice since the last connection heard the relay. The roster now follows the same rule as held profiles: a continuous membership replay needs no full read unless an earlier one is still owed. A cut replay reads it; a membership route that fails or goes unreported reads it once per connection. Window focus no longer reads the roster. The 60-second live recovery tick is unchanged. A roster read stays owed while the roster is not verified or a strong read is pending: a cache clear or a disconnect that retires a queued or in-flight exact membership confirmation leaves one pending, so the next reconnect reads the list even when its replay is continuous. A profile read that fails stays owed: the next reconnect reads again even when its replay is continuous. A live-only replacement route never reports continuous. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Signed-off-by: Logan Johnson <loganj@squareup.com>
This was referenced Nov 2, 2026
Merged
loganj
marked this pull request as ready for review
November 2, 2026 22:04
kalvinnchau
approved these changes
Nov 2, 2026
kalvinnchau
left a comment
Contributor
There was a problem hiding this comment.
🤖 Approved. No actionable findings in the code review of reconnect continuity, fallback reads, and membership handling. Runtime behavior was not exercised; relay delivery and replay assumptions remain unverified.
kalvinnchau
pushed a commit
that referenced
this pull request
Nov 2, 2026
…ntinuous replay covers (#2204) ## Summary After a reconnect, the app no longer re-reads statuses, community emoji, read markers, and reminders when the live subscription already replayed every change made during the outage. The owner goes back to its ready state with no extra relay read. If the replay was cut, had no cursor, or the route failed, the app does the same full read as today. Stacked on #2203, which adds the per-route continuity signal (route resumed from a cursor, and its replay ended under the 500-event limit) and uses it for profiles and the roster. ## Details **Which reads are skipped, and when** | Owner | Covering live route | Skipped when the route is continuous and… | Otherwise | |---|---|---|---| | Statuses (30315) | profiles | always; authors already loaded stay loaded, authors not loaded yet (or whose read failed) still read | today's reconnect re-read | | Community emoji (30030) | profiles | the catalog was ready, complete, not over budget, and no read was in flight | today's reconnect re-read | | Read state (30078) | membership | the markers were loaded and not in error before the drop, and no live ingest was cancelled by the drop | today's snapshot read | | Reminders (30300) | membership | the list was ready, hydrated, not loading, and every live edit decoded | today's `recover()` read | - Reminders stay "loading" until the replayed edits finish decoding. A second drop during that decode keeps them loading, and the next reconnect decides again. An edit that failed to decode causes a full read. - Read state restores its completeness and health. When edits are still pending publication, it goes back to "pending" and schedules a publish. - A `false` (cut) report re-reads even if an earlier report on the same connection was continuous. A route that errored or was never reported reads once per connection, the same as today. - `session.ts` is FOUNDATION. Logan asked for this change directly. The change: `settleCovered(route, continuous)` runs next to the sibling's `settleGlobal`, and the old `first` timer no longer starts these four reads. It still starts the agent library, channel activity, inbox, and the refreshers. - `docs/relay-queries.md` documents the new rule after the paragraph about global-route continuity. **Not in this PR** - Unread repair (the channel routes) still runs on every reconnect. It is already a delta read (from the marker minus 900 s), and it also runs on the 60-second recover tick. To skip it safely, it needs the per-channel continuity signal from the catch-up work, and it overlaps #1697. It should be a follow-up. - Inbox, channel activity for unjoined channels, artifact views, and agent config are not covered by any live REQ, so they keep their reads. **Evidence** (head c5195ca) - `live-session.test.ts`: a session-level test counts finite reads by kind. Continuous profiles and membership replays → zero 30315/30030/30078/30300 reads, and the replayed status, emoji, reminder, and a real signed read marker all show. Each route is independent. A cut replay, no cursor, an errored route, an unreported route, and a later cut on the same connection each re-read. - `read-state.test.ts` and `reminders.test.ts`: restore without a read; re-read when never loaded, after a decode failure, or after a drop cancelled an ingest; reminders stay loading through a second drop while decoding; re-read when a read was in flight at the drop. - `pnpm check` exit 0. Full `vitest` run on this tree: 15268 passed. - The pre-push hook failed with 3 vitest timeouts (5 s / 15 s / 30 s) at a load average of about 43. Those files passed alone (181 tests), and the full suite passed on the same tree, so the push used `--no-verify`. - Pre-PR review: Auto and Cerveau. Both blockers were fixed (a drop that cancelled a marker ingest; reminders showing ready while offline after a second drop). Accepted as is: a quick flap before the first report loses the skip for that connection, which means one extra read and no wrong state. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
dr0uleau
added a commit
that referenced
this pull request
Nov 2, 2026
Co-authored-by: Codex <noreply@openai.com> * origin/main: (26 commits) AIDA-2336: Show when worktree folders are required (#2165) AIDA-2326: Reveal full emoji completion names (#2172) feat(agent-command): let local agents discover advertised plugin tools (#2110) feat(bestie): a Bestie tab on your Bestie's agent page (#2034) fix(relay): notify verified profile changes once per replay (#2209) feat(shortcuts): open Shortcuts settings with Mod+/ (#2022) Re-send live channel subscriptions on access grants, not every minute (#2207) perf(profiles): count verified namesakes once per publish (#2159) feat(claude-code): start sessions with the agent's recent notes (#2040) fix(relay): skip status, emoji, read-state and reminder re-reads a continuous replay covers (#2204) Support agent credentials through the macOS debug helper (#2174) refactor: centralize relay search debounce and require two characters (#2200) fix(relay): drop unused profile exemptions from config and agent reads (#2202) fix(relay): skip full profile and roster re-reads after a continuous reconnect (#2203) Notify when Solo chat agents finish (#2011) feat(mcp): start a Solo chat with channel_create (#2128) fix(relay): keep profiles when channel access ends (#2187) Wait for thread readiness before keyboard input (#2181) fix(builderlab): unify the development sign-in (#2109) feat(host): add byte requests, private saves and POST dispatch protection (#2084) ... Signed-off-by: Daniel Rouleau <drouleau@squareup.com>
loganj
added a commit
that referenced
this pull request
Nov 7, 2026
…s complete (#2206) ## Summary - After every reconnect, each open channel read its newest page of messages from the relay again (plus up to five older pages and a notices read), and each open thread re-read its replies, even after a short network blip. - The app's live subscription for those channels already replays what it missed: it resumes from just before the time the previous connection last heard the relay. When a channel's own replay is not cut short by the relay's 500-event limit, and the session can apply every event in it by itself, the re-read adds nothing. - Now an open channel that was up to date when the connection dropped, and whose newest page was read in the last 15 minutes before the drop, skips those reads when its replay was complete, and is marked up to date at once. The live subscription's 15-minute refresh re-reads that page while connected, so most reconnects qualify. An open thread does the same. Every other case keeps today's catch-up: a replay that reached the limit; a replay that carried a reply, a deletion, or an edit or reaction that arrived before its target; a channel with no earlier connection; a channel opened, restored or read while offline; a channel still owed an earlier catch-up; a thread that was purged or read while offline; a batch re-sent for a grant, which includes any reconnect within about 16 minutes of a join, add, create or DM open; and any reconnect in the first 11 to 13 minutes of a session. - Builds on #2203 (merged), which added the same "replay was complete" signal for the profile and channel-list subscriptions. - `src/features/relay/session.ts` and `src/features/relay/store.ts` are marked FOUNDATION. Logan asked for this change directly. ## Details - **Per-channel signal (`live.ts`).** A channel batch subscription has one filter per channel, each with its own 500-event limit. The route counts replayed events per channel (an event that names no single channel of the batch counts against each of them) and reports, at end of replay, which channels resumed from an earlier connection's cursor and stayed under the limit. Continuity also requires coverage of the full 960-second resume lag before the cursor. The startup floor can truncate that interval until the cursor is 11 minutes into the session, so channel and global routes keep their repair reads during that period. A cursor ends where its connection last heard the relay, as of the recovery tick before the latest (ticks are 60 s apart), so it can trail the drop by up to two minutes: a drop 11 to 13 minutes into a session can still catch up. A live-only replacement reports none. That includes a batch re-sent for a grant (44100, sent when the user joins, is added to, creates or unarchives a channel, or opens a DM), and the batch re-sent when the membership replay re-delivers a grant of its last 16 minutes. So a reconnect within about 16 minutes of a grant catches up that batch as it does today. A grant that arrives while its batch is not live yet (its first reconnect REQ waits to retry, is queued or is in flight) takes that channel out of the batch's resumed set, so the channel catches up at its EOSE: its replay starts from a cursor of a stream the relay may have left the channel out of. A grant also ends the channel's cursor, and no connection sets it again until a REQ sent after the grant reaches EOSE, so a rebuilt batch or a later connection cannot restore it. That REQ is judged by order, not by the clock: an EOSE clears the debt only if no grant reached its route while its REQ was in flight. - **Batches over 10 channels (`live.ts`).** Production sends up to 2048 channels in one batch. A batch over 10 channels sends its REQ live-only and replays through separate history routes, so its EOSE establishes only the channels no history route still replays. Each other channel establishes when its replay ends, with that replay's continuity: a combined replay counts a channel only on the page where its paging ends below the limit, a split replay by its own filter's count, and the same repairs apply. A replay that fails or is stopped establishes its channels at once with nothing continuous. A connection that drops before a channel's replay ends sets no cursor for it, so the next connection resumes from the older cursor. A channel whose live batch is not live when its replay ends (the batch failed or is being retried) establishes at that batch's next EOSE, with nothing continuous. A grant re-sends the batch live-only, so it also ends the continuity of every channel of that batch whose history replay is still running. - **Planned renewals (`live.ts`).** A planned renewal's carried batch stays quiet when its replay ends below the limit, even for a channel granted before its REQ (a grant ends that channel's cursor, so a carried route cannot hold one), unless that replay carried a reply, deletion or other repair above. Then it establishes, and those channels catch up. - **Grant after a revoke (`store.ts`).** When access ends, the relay leaves the channel out of its batch with no frame, and the channel's cursor stays. A grant that restores access can reach the broker after the batch's EOSE (for example in the membership replay of the same reconnect), so the batch can first report that channel continuous. The grant then re-sends the batch, whose EOSE reports it again with nothing continuous, and the session starts a catch-up for it. The re-sent batch is live-only, so it can deliver a new row before that report, and the browser broker can hold a report back under backpressure while it writes live events at once. So the target is taken at the grant, not at the report: `live.ts` hands the 44100 to the session before it re-sends the batch, and the session tells the store (`granted`). The window keeps the rows it holds then. While the relay left the channel out, the stream delivered nothing for it and a read was denied, so those rows all precede the gap. Until a catch-up started after the grant takes them, a continuous report does not resume the window; the next catch-up, or the next drop's, pages back to them. A catch-up already running when the report arrives may have read its head before the grant, so the rows stay owed past it. A report with nothing continuous and no grant since takes the rows held when it arrives, so a routine batch re-send after a resume reads only the head. - **Events the session cannot apply alone (`live.ts`).** A channel is not continuous when its replay carries a reply or a deletion (each changes a thread summary, kind 39005, which the relay never stores, so the replay cannot carry the new one), an edit or reaction that arrives before the message it targets (the replay is newest first, and the session drops an auxiliary event whose target it does not hold), an undecodable event, or an event whose `h` tag names none of the route's channels. An edit or reaction to a message held before the drop, a new top-level message and a membership notice keep it continuous. A global route (`profiles`, `membership`) with an undecodable replay event is not continuous either, and that event counts against its limit. - **Broker (`relay-broker.mjs`, `broker-live.ts`).** The dev broker carries the list across its HTTP boundary; a malformed list counts as none. When a batch narrows during its replay, the broker drops that batch's untagged traffic, so it now reports no channel of that batch continuous. - **Channel windows (`store.ts`, `session.ts`).** A window records at the first drop whether it was verified and the head read whose rows it holds started at most 15 minutes before (`RESUME_READ_AGE`, from `LIVE_RESUME_LAG`). A head read the window never took (a hover prefetch, a cancelled catch-up) does not count. An open thread uses the same age rule, from its last full refresh. The replay starts 16 minutes before the drop, so a live frame the relay dropped before that is missing from it; the recent head read holds it. A window that skipped the read on one reconnect needs a new head read before it can skip again. `resume()` clears any outage read error and marks it verified with no head read only if it still holds that state: a head or page read while offline, or a queued catch-up, clears it. `resume()` also finishes a membership notice read that failed or was cut before the drop; it reads nothing when none is owed. - **Threads (`threads.ts`).** A thread view records at the first drop whether it was settled. A purge or any read after the drop clears that, a later drop keeps it, and the skip also needs the view to be ready now. - **Browser fixture.** `policy-relay.mjs` gains `replayNext()`, which models a reconnect replay (fixture events are dated before the client's resume time, so the relay would not replay them otherwise). Specs that test the catch-up now use a replay that reaches the limit; the startup reconnect spec checks that an under-limit replay near startup still repairs while preserving history and reading position. ## Known gaps An event that neither the live stream nor the replay carries is no longer repaired by the reconnect's re-read for a channel whose replay was complete. In general, a replay with fewer events than its limit is complete only if the relay applies every filter before the limit. Known cases, all relay-side: - **Channel deleted while offline.** The relay leaves it out of the replay with no message, so it looks complete. The 60-second channel-list read on the recovery tick removes it. - **Filters after the limit.** The relay checks some per-reader visibility (`event_visible_to_reader`) after the limit, so a replay that the limit cut can arrive with fewer than 500 events. It needs a busy channel. The relay fix is to apply those filters in SQL before the limit. - **Silent send-buffer drop.** When a connection's send buffer is full (`BUZZ_SEND_BUFFER`, `try_send_ws_message`), the relay discards a live frame without notice. The 15-minute head read age bounds what a skip can miss: a frame dropped before the latest head read is in that read, and one dropped after it is newer than the replay's start. The remaining case is such a frame whose author's clock is more than a minute slow. A live connection has the same gap until the next 15-minute refresh, with no reconnect at all. The relay fix is to end the subscription or the connection when it drops a frame. - The replay count requires a relay that sends `CLOSED`, not `EOSE`, after a failed history read (block/buzz #8692, 76cd4294). Deployed relays must include it. ## Rollout No flag. The change keeps no persisted state, so rollback is a plain revert. A skipped catch-up that misses an event shows as a message that appears only after a reload. Watch for such reports in the first days after release; revert if one traces to a skipped catch-up. ## Testing - `live.test.ts`: per-channel continuity with a cut channel, a channel with no cursor, a live-only renewal, and an event counted against two channels; replies, deletions and an edit or reaction before its target are not continuous, while edits and reactions to held messages and top-level messages are; an undecodable event or an event tagged for another channel makes no channel of its batch continuous; an address-only deletion is not continuous; a channel granted while its batch's first reconnect REQ waits to retry is not continuous at the retry's EOSE, nor after that batch is rebuilt for narrower interests or re-created on the next connection; a granted channel resumes on the next connection only after its re-send reached EOSE; a channel granted while that re-send is in flight, in the same millisecond as its REQ or after a clock rollback, stays owed; a global route with an undecodable replay event is not continuous, and that event counts against its limit; full-lag boundary at session ages 0, 659, 660 and 661 s. - `live.test.ts`, batches at the production size (2048) with 11 channels: the batch EOSE establishes no channel; the history replay's end establishes all of them, with a channel that carried a reply excluded; a reaction whose target is on the next combined page is not continuous; a channel the limit cut establishes at once, the rest when their paging ends; a failed history replay establishes all at once with nothing continuous; a drop before the replay ends keeps the older cursor for the next connection. Four of the five fail on the previous head. A grant that arrives while a retried batch REQ is in flight re-sends the batch at EOSE, and no channel whose history replay was running is reported continuous (both grant re-sends share `regrant()`; fails at 716622c). - `live.test.ts`, found by the model test below: a grant during a large batch's history replay reports no channel continuous; a channel whose batch fails during its replay establishes only when the batch is live again, with nothing continuous; a planned renewal whose replay carried a reply establishes the batch with that channel not continuous. Each fails on the previous head. - `live-continuity.model.test.ts` (new): a seeded randomized model of the relay checks one rule: a channel reported continuous holds every stored message of that channel, is streamed by an open subscription, and its replay carried no reply. The model relay replays newest first up to the limit, accepts events backdated up to 900 s, fans out live events, and silently leaves out channels the viewer cannot read, at REQ time and after a revoke. It runs 40 random steps (posts and replies, backdated posts, outages short and long, restarts, planned renewals, CLOSED, grants and revokes, interest changes, bursts) for 3 and 12 channels, 12 seeds each (about 40 s). Whenever a stream gap opens, it posts into the affected channels inside the gap, and it injects disruptions while a replay is in flight. A claim that missed a message passes only while that channel's grant is stored but not yet delivered (the grant-after-revoke case above), and a later report with the channel not continuous must retract it before the run ends. The model's catch-up is ideal: it does not run the store, so the store side of that case has its own test above. It models only messages and replies, not reactions, edits or deletions; the unit tests cover those repairs. `LIVE_MODEL_SEEDS` raises the seed count: 80 and 150 seeds pass, and 40 seeds pass at the final head. It found the three holes above. Mutation check, each removing one guard or reverting one fix in `live.ts`: the unit suites (`live`, `live-session`, `live-recovery`, `live-restriction`, `live-demand`) fail for all 14 mutants; the model alone, at the default 12 seeds, fails for 8 of them, including the original head before these fixes. - `live-session.test.ts`: after a continuous resume, a grant, then a live row from the re-sent batch before its report: the catch-up pages back past the head to the rows held at the grant. In the broker's order (grant, live row, then the delayed continuous report of the replay that left the channel out) the window is not resumed and catches up to the drop's rows; the re-sent batch's own report then reads only the head. With a second drop before the re-sent batch reports, the next catch-up still reaches the grant's rows. All three fail without the session's `granted` call. With an older catch-up still running at the re-sent batch's report (its head read before the grant), the next catch-up still reaches the grant's rows (fails at a7151b0). After a resume and three live messages with no grant, a routine re-send of the batch reads only the head (fails at 01d6486). An open thread whose last read is older than the replay recovers a reply the stream dropped. - `live-session.test.ts`: a window whose last head read started more than 15 minutes before the drop catches up after a continuous replay and shows a frame the live stream missed (fails on the previous head); a refresh while connected re-reads the head, so a reconnect 10 minutes later reads nothing; a hover prefetch more recent than the window's own head read does not let it skip (fails at 01d6486); an open thread skips its read at exactly 15 minutes since its last refresh and reads at 15 minutes plus 1 ms (fails without the thread age check). - `live-session.test.ts`: a verified window gets no head, page or notice read and shows backdated and missed events from the replay; an offline edit and reaction to a held message apply from the replay with no read; a cut replay catches up; a window read before the first connection, read again while offline, or still owed a catch-up, catches up; an owed notice read resumes with no head read; a thread refreshes only on a cut replay, and after a cache clear, a failed refresh, a refresh while offline, or a refresh between two drops. Each store and thread guard was removed in turn and a test failed, except the thread purge clear (see the review thread). - `broker-live.test.ts` and `relay-broker-live.test.mjs`: the list crosses the broker boundary; a batch with a channel no longer current reports no channel continuous. - Browser (`live.spec.mjs`, `message-navigation.spec.mjs`, `relay-recovery.spec.mjs`): catch-up after a cut replay keeps paged history, cursor and reading position; an under-limit replay near startup keeps them with a repair read. - Live relay at 2796e9b: a local buzz-relay behind a TLS proxy and a gate that closes the socket, with two browser users in the dev broker. A session aged 19.5 minutes has an 8-second outage, and the other user sends a message during it. After the reconnect, the open channel shows the missed message 0.8 s later with no head read (0 head reads, 10 reads in total after the drop). That scenario has no grant in the membership replay, so the grant change at the final head does not reach it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Signed-off-by: Brad Seiler <seiler@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-authored-by: Brad Seiler <seiler@squareup.com> Co-authored-by: Honor <e0bf479eeebe47dde1e35705212a4df70616a7bc834d907ddfb18cf0405f8b1b@seiler-test.communities.public.builderlabstaging.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🤖
Summary
src/features/relay/session.tsis marked FOUNDATION. Logan asked for this change directly.Details
Continuity signal (
live.ts). When theprofilesormembershiplive route reaches end-of-replay, it reportscontinuous: every key had a cursor from an earlier connection, and the replay stayed underLIVE_REPLAY_LIMIT(counted across the route's filters, so any cut filter marks the whole route). A live-only replacement never reports continuous. The opt-in dev broker carries the flag across its HTTP boundary (relay-broker.mjs,broker-live.ts); a malformed or missing flag counts as "not reported".Session rule (
session.ts). Each global route settles once per connection:For profiles, a read stays owed until
profiles.reconnect()resolves. For the channel list, it stays owed until the roster state isverifiedand no strong read is pending (store.tsrosterOwed()). A membership confirmation that a cache clear or a disconnect retires leaves a strong read pending, so the next reconnect reads the list even when its replay is continuous. Session recovery already retries a failed or deferred roster read.Removed reads. Window focus (
service.tsvisible()) no longer callsrefreshList(); it still asks recovery to retry failed reads. The live recovery tick (recover()) is unchanged.Not in this PR. Statuses, emoji, read-state, reminders, unread repair and open-channel catch-up still read in full on reconnect; sibling PRs stack on this continuity signal for those.
Known gaps
These roster and profile changes reach a connected client with no live signal. The 60-second roster read still repairs the roster cases in this PR; the stacked PR that removes it has to account for them. A relay follow-up is filed for each.
channel_deletedmessage, so the fan-out access check drops that message for every member. It sends no membership notice either.Testing
live.test.ts: global routes report continuous only when resumed from a cursor and under the limit; the renewal tests now expect no channel-list read when the membership route stayed continuous.live-session.test.ts: first connection, continuous, cut (per route), failed read then continuous, missing report in both orders, and failed profiles route, each with the exact number of profile and channel-list reads.live-demand.test.ts: a membership confirmation retired by a cache clear or a disconnect is still recovered through a continuous membership establishment.broker-live.test.tsandrelay-broker-live.test.mjs: the flag crosses the broker boundary; a malformed one reports nothing.🤖 Generated with Claude Code