镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

fix(relay): skip full profile and roster re-reads after a continuous reconnect - #2203

Merged
loganj merged 3 commits into
mainfrom
larry/reconnect-no-full-rereads
Nov 2, 2026
Merged

loganj merged 3 commits into
mainfrom
larry/reconnect-no-full-rereads

Conversation

@loganj

@loganj loganj commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

🤖

Summary

  • After every reconnect, the app read every cached profile and the whole channel list from the relay again, even after a short network blip. It also read the whole channel list on every window focus. With many clients, this is a large, mostly redundant load on the relay.
  • The app already keeps two live subscriptions for this data: one for profiles and one for the viewer's channel membership. After a reconnect, each one replays what changed while the app was offline. When that replay starts where the previous connection stopped and is not cut short by the relay's 500-event limit, it has already sent every stored change, so the full read adds nothing.
  • Now the app skips the full profile read and the full channel-list read after such a reconnect. It still does both reads on the first connection, after a cut replay, when that subscription fails to start, and when the previous full read did not succeed.
  • The window-focus channel-list read is removed. The 60-second live recovery tick still reads the channel list; a stacked PR removes it.
  • src/features/relay/session.ts is marked FOUNDATION. Logan asked for this change directly.

Details

  • Continuity signal (live.ts). When the profiles or membership live route reaches end-of-replay, it reports continuous: every key had a cursor from an earlier connection, and the replay stayed under LIVE_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:

    • continuous: no full read, unless an earlier read is still owed;
    • cut: full read;
    • failed before end-of-replay, or not reported (older broker host): one full read for that connection, whatever order the routes arrive in.

    For profiles, a read stays owed until profiles.reconnect() resolves. For the channel list, it stays owed until the roster state is verified and no strong read is pending (store.ts rosterOwed()). 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.ts visible()) no longer calls refreshList(); 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 delete. The relay soft-deletes the channel before it posts its channel_deleted message, so the fan-out access check drops that message for every member. It sends no membership notice either.
  • Moderation kick. The relay evicts the member's subscription but sends no membership notice, and no CLOSED for a channel in a batched subscription.
  • Moderation DM. The relay opens the DM without a membership notice. Membership notices are also sent after commit on a best-effort basis.
  • Silent relay drops. When a client's send buffer is full, the relay can drop a live frame without telling the client. A replay does not reach a frame dropped more than about 16 minutes before the disconnect. For profiles, such a change now stays stale until a cut reconnect or a restart; before, every reconnect repaired it.

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.ts and relay-broker-live.test.mjs: the flag crosses the broker boundary; a malformed one reports nothing.

🤖 Generated with Claude Code

loganj and others added 3 commits November 2, 2026 16:45
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>

@kalvinnchau kalvinnchau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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.

@loganj
loganj merged commit d24a94f into main Nov 2, 2026
41 checks passed
@loganj
loganj deleted the larry/reconnect-no-full-rereads branch November 2, 2026 22:35
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants