Repository navigation
fix(unread): repair unread from the recovery tick only every 15 minutes - #2252
Merged
Merged
Conversation
The 60 s live recovery tick asked unread to repair on every tick: a read-state refresh and one read per 128 joined channels, every minute, on a healthy connection. The live stream already carries every unread change, and each connection's first establishment repairs unread once. The tick's repair is still the only one for changes the live stream loses with no signal: a channel the relay silently pruned from a multi-channel batch, or a dropped fan-out frame. So it stays, as a backstop: the tick asks for a repair only when the last one (by the tick or by an establishment) is 15 minutes old. Those losses now take up to 15 minutes to repair instead of one. Signed-off-by: Logan Johnson <[email protected]> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
loganj
marked this pull request as ready for review
November 3, 2026 13:17
…ff the backstop clock The recovery tick repairs unread on the next tick while a repair failed or the stream dropped with none completed since. A covered resume reads no read-state snapshot, so it no longer restarts the 15-minute interval. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Logan Johnson <[email protected]>
…ness owed() read the evidence freshness, so one live message after a failed repair made it false while other channels stayed unrepaired, and a failed read-state snapshot left it false once the evidence read succeeded. The unread owner now tracks repair debt itself (a failed repair, an overflow or a drop, paid only by a completed repair), and the read-state owner reports a snapshot read still owed (a failed read or marker, or a drop, paid by a successful read or resume). Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Logan Johnson <[email protected]>
TheSentinel454
approved these changes
Nov 3, 2026
TheSentinel454
left a comment
Contributor
There was a problem hiding this comment.
🤖 LGTM at 827e56e. The backstop interval, owed-retry, and covered-resume handling look correct, and the new regressions cover each case Kalvin raised.
Adds regressions for an evidence overflow (the next tick repairs), a live marker that fails to apply and a drop (a snapshot read is owed until a read or covered resume), and a covered reconnect (the next tick reads nothing). The docs now state that evidence debt and marker debt each clear only their own way. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Logan Johnson <[email protected]>
tlongwell-block
added a commit
that referenced
this pull request
Nov 3, 2026
Resolves read-state.ts against #2252 (observationOwed): both sides kept; the snapshot capacity basis and the owed-observation flag are independent. Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Signed-off-by: Tyler Longwell <[email protected]>
dr0uleau
added a commit
that referenced
this pull request
Nov 3, 2026
…xecution-support-for-native-buzz-33ea2bea * origin/main: (74 commits) feat(host): harden byte lifecycle and add protected image upload (#2199) Updated onboarding (#2183) perf(relay): read each signed roster once in large channels (#2244) AIDA-2346: Keep spoilers hidden in reminder previews (#2261) fix: AIDA-2371 Keep sidebar selection pill aligned during resize (#2182) fix(relay): skip background rereads of profiles a read found absent (#2147) fix: prevent startup logo selection (#1823) fix(agents): send sign-in evidence with agent Git credentials (#2240) perf(mentions): stop scanning every member on each mention keystroke (#2246) fix(attachments): send image dimensions so previews don't shift (#2262) feat(shortcuts): add Slack-style hotkeys (#2170) perf(plugins): share host request clients across calls (#2243) fix(identity): keep federated sessions and agent grants in one keychain item (#2225) fix(profiles): evict the least recently requested profile (#2255) fix(unread): repair unread from the recovery tick only every 15 minutes (#2252) AIDA-2374: Use readable labels in chat titles (#2185) perf(profiles): keep verified names checked through a planned renewal (#2234) feat(plugins): let a plugin ask Buzz to review an install (#2059) fix(identity): sign FI builds in at first run through the native session (FOUNDATION: App.tsx, services.ts) (#2249) Place profile verification beside the verified name (#2224) ... Signed-off-by: Daniel Rouleau <[email protected]>
dr0uleau
added a commit
that referenced
this pull request
Nov 3, 2026
* commit 'c8e882165': (50 commits) Scope execution targets by harness session Read durable execution state before admission feat(host): harden byte lifecycle and add protected image upload (#2199) Updated onboarding (#2183) perf(relay): read each signed roster once in large channels (#2244) AIDA-2346: Keep spoilers hidden in reminder previews (#2261) fix: AIDA-2371 Keep sidebar selection pill aligned during resize (#2182) fix(relay): skip background rereads of profiles a read found absent (#2147) fix: prevent startup logo selection (#1823) fix(agents): send sign-in evidence with agent Git credentials (#2240) perf(mentions): stop scanning every member on each mention keystroke (#2246) fix(attachments): send image dimensions so previews don't shift (#2262) feat(shortcuts): add Slack-style hotkeys (#2170) perf(plugins): share host request clients across calls (#2243) fix(identity): keep federated sessions and agent grants in one keychain item (#2225) fix(profiles): evict the least recently requested profile (#2255) fix(unread): repair unread from the recovery tick only every 15 minutes (#2252) AIDA-2374: Use readable labels in chat titles (#2185) perf(profiles): keep verified names checked through a planned renewal (#2234) feat(plugins): let a plugin ask Buzz to review an install (#2059) ... Signed-off-by: Daniel Rouleau <[email protected]> Co-authored-by: Codex <[email protected]>
matt2e
added a commit
that referenced
this pull request
Nov 4, 2026
…re the minute tick Review of 3f417fa (PR #1697, wesbillman): the warm-relaunch journey timed out on hosted Chromium 7/9 waiting 75 s for the broker's minute tick, with no unread reads. Since the rebase onto main brought #2252, the session's `recover()` repairs unread only when unread is owed or UNREAD_BACKSTOP_INTERVAL (15 minutes) has passed since the last repair. After the healthy relaunch nothing is owed and the relaunch has just repaired, so the tick reads nothing. The tick itself comes from the broker's subscriber in Node on a real 60 s chain, so a page clock cannot speed it up. The test now installs the renderer's Playwright clock before the first launch, flowing in real time, and moves its time 15 minutes forward before waiting for the tick. It moves the time back to real time before the next reload. Installing the clock in a running page would replace timers the app already set and briefly read the time as 0, so it is installed up front. The one-delta assertion is unchanged; the production interval is unchanged. Checks: `bin/pnpm test:browser --project=chromium --project=webkit --no-deps tests/browser/unread-storage.spec.mjs`: 4 passed (both cases on each engine; the warm relaunch case 1.1 m on each), at load ~60. With the time jump removed (install kept), chromium fails at the same poll with 0 reads after 75 s, as on CI. Biome clean. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
matt2e
added a commit
that referenced
this pull request
Nov 4, 2026
…re the minute tick Review of 3f417fa (PR #1697, wesbillman): the warm-relaunch journey timed out on hosted Chromium 7/9 waiting 75 s for the broker's minute tick, with no unread reads. Since the rebase onto main brought #2252, the session's `recover()` repairs unread only when unread is owed or UNREAD_BACKSTOP_INTERVAL (15 minutes) has passed since the last repair. After the healthy relaunch nothing is owed and the relaunch has just repaired, so the tick reads nothing. The tick itself comes from the broker's subscriber in Node on a real 60 s chain, so a page clock cannot speed it up. The test now installs the renderer's Playwright clock before the first launch, flowing in real time, and moves its time 15 minutes forward before waiting for the tick. It moves the time back to real time before the next reload. Installing the clock in a running page would replace timers the app already set and briefly read the time as 0, so it is installed up front. The one-delta assertion is unchanged; the production interval is unchanged. Checks: `bin/pnpm test:browser --project=chromium --project=webkit --no-deps tests/browser/unread-storage.spec.mjs`: 4 passed (both cases on each engine; the warm relaunch case 1.1 m on each), at load ~60. With the time jump removed (install kept), chromium fails at the same poll with 0 reads after 75 s, as on CI. Biome clean. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
matt2e
added a commit
that referenced
this pull request
Nov 4, 2026
…unt the delta as a tick's read, and reuse the cached fold The rebase onto main cbc71c8 brings #1286 (a 30-day read floor) and #2366 (folded rows cached per channel), and leaves two #2252 tests counting reads the delta does not make. - Read floor. `createReadState` now reads everything older than 30 days before the device clock as read. The branch-only setups in unread-restore, unread-repair and unread-fold, and live-session's `caughtUp`, date their fixtures in 1970 and 2023, so 56 of their tests saw nothing unread. They take `floor: () => 0` (or `readStateFloor`), as main's setups do. - #2252's recovery tests. "a live message after a failed unread repair does not cancel its retry" and "a covered reconnect pays the drop's debt" counted a tick's evidence read as an `include_aux` filter with kind 40002. A catch-up reads the delta, which carries no `include_aux`, so neither saw the tick's read. Both failed on hosted CI at 3f417fa (run 41733593360, JavaScript 4/4 and both Vitest flake checks). `tickReads` now counts a channel read (`#h`, no `#e` or `ids`, kind 40002), full or delta, and the first test uses it. - Fold cache. With #2366 an owner reuses a channel's folded rows until the evidence index rebuilds, so the 2,000-event fold test's `activity("c7")` after `inbox()` folded nothing. It now asks c7's activity first, which folds c7's own 23 events, and `inbox()` then folds the other 79 channels and reuses c7's. The rebase resolved `afterFrontier` as the branch's `catchUp` term plus main's `reads.floor()` in the `Math.max`, and main's `folded()` cache to fold `foldable(channelId)`, the channel's own evidence and its overlays. Checks: bin/pnpm typecheck; biome check --error-on-warnings on the changed files; vitest src/features/relay, src/bundled/inbox and src/features/notifications: 157 files, 3,348 passed, 4 skipped. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
matt2e
added a commit
that referenced
this pull request
Nov 4, 2026
…next recovery tick reads it again Review thread on 5f4836a (PR #1697, kalvinnchau), P3. Since #2252 the session's minute recovery tick repairs unread only while a repair is owed or UNREAD_BACKSTOP_INTERVAL (15 minutes) has passed since the last full repair. A pass whose read was dropped after its retry (a delta chunk, a window, a thread read, a root fetch, the read by ID) ended with `partial = unchecked` but `owing = false`, so every later tick skipped unread. The reviewer's 129-channel probe, with the second delta chunk failing and the relay healthy after, kept the partial notice and unknown coverage through five minute ticks, with no reads, until the backstop. A pass in which nothing landed throws and stays owed, and was read again on the next tick. docs/inbox.md promised "the next check" in one place and "no automatic retry" in another. The pass's tail now sets `owing = unchecked`. A pass that drops a read stays owed, as one that throws does, and the next tick repairs. What each dropped read leaves behind is already the next pass's to read: a dropped delta keeps the mark, a dropped window or thread read leaves its channel outside the roster set, and a dropped read by ID resumes. A pass with every read landed pays the debt, and the tick after it waits for the backstop again. So a relay that keeps dropping one read costs one repair a minute, the same as a relay that keeps failing whole repairs, never more. `session.ts` (FOUNDATION) is unchanged: its gate reads `unread.owed()`, which reads `owing`. Docs: docs/inbox.md now says the repair stays owed after a dropped read, so the next periodic check, a minute later, repairs again, as after a repair that fails. The passages that already said "the next check" stand. docs/relay-queries.md counts a repair that dropped a read as evidence debt, cleared only by one with every read landed. The unread-repair comment on an owner that does not retry on its own says the session's tick does. Test (unread-startup, through the session's real recovery gate): "a pass that drops a delta chunk leaves unread owed: the next recovery tick reads it again, not the 15-minute backstop, and the tick after that reads nothing". 129 channels, so the delta is two chunks. A full reconnect pays the marker debt. A covered reconnect then resumes the markers, and its one-channel chunk fails twice and is dropped (`partial: true`). A minute later, with the relay answering, the tick reads that chunk again and `partial` clears. A minute after that, the tick reads no evidence; the barrier is `retrySync()`, which settles after any read-state refresh the tick began, then `ensure()`, which waits for the repair chained to it. - On the previous unread.ts the tick a minute later reads nothing: "expected 3 to be 4" at the chunk's count. - With `owing = true` at the tail instead (always owed), the last tick repairs: "expected 12 to be 10" at its evidence count. The live-session `caughtUp` harness was tried first and is not used: its read-state snapshot reads after the first fail, so marker debt never clears there and every tick repairs either way. Checks: bin/pnpm typecheck; biome check --error-on-warnings on the changed files; git diff --check; vitest src/features/relay, src/bundled/inbox and src/features/notifications: 157 files, 3,349 passed, 4 skipped. Not run: Playwright, the native host, a live relay. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
This was referenced Nov 4, 2026
Closed
arividar
pushed a commit
to minniai/minni-buzz-app
that referenced
this pull request
Nov 4, 2026
…es (block#2252) ## Summary - Every connected client ran an unread repair on the 60-second recovery tick, even with a healthy connection and no outage: a read-state refresh plus one relay read per 128 joined channels, every minute. - The live stream already carries every unread change, and each connection's first establishment still asks unread to repair once (or to resume, when the membership replay was continuous). - The tick's repair is still the only fix for unread changes the live stream loses with no signal to the client. So it stays as a backstop: the tick asks for a repair only when the last one, by the tick or by an establishment, is at least 15 minutes old (`UNREAD_BACKSTOP_INTERVAL`). - Effect: about 15 times fewer unread reads from every connected client. The cost: a silent loss now takes up to 15 minutes to repair instead of up to 1 minute. - `src/features/relay/session.ts` is marked FOUNDATION. Logan asked for this change directly. ## Details - `session.ts`: `repairUnread()` calls `unread.reconnect()` and, for a full repair, records when it was asked. The membership route's covered-read step (from block#2204) calls it with the replay's continuity, so each connection's full repair resets the clock; a covered resume reads no read-state snapshot and leaves the clock alone. `recover()` repairs at once while `unread.owed()`, and otherwise only when `UNREAD_BACKSTOP_INTERVAL` has passed. This matches the roster backstop from block#2215. - `unread.ts`: `owed()` reports a repair still owed. The unread owner tracks its own debt: a failed repair, an evidence overflow or a drop sets it, and only a completed repair clears it, so a live message that marks the evidence observed again does not clear it. - `read-state.ts`: `owed()` reports a snapshot read still owed: a failed snapshot read, a failed marker apply or a drop sets it, and a successful snapshot read or resume clears it. `unread.owed()` includes it, so a backstop whose snapshot read failed retries on the next tick even when its evidence read succeeded. - `docs/relay-queries.md`: the recovery-timer paragraph describes the unread backstop next to the roster backstop. ## Silent losses this backstop repairs A relay review (block/buzz `009dc103f`) found these ways a connected client can lose an unread-relevant event (kinds 9, 40002, 40008, and read state 30078) with no signal. For channels that are not open in a window, the tick repair is the only fix today. With this change each one takes up to 15 minutes to heal: 1. **Send buffer full.** Fan-out uses `try_send` and drops the frame. The relay cancels the connection only after 15 consecutive full sends, so short bursts of drops are silent. 2. **Cross-pod delivery through Redis pub/sub** is at most once: a lagging broadcast receiver, a Redis subscriber reconnect, or a failed publish from the writing pod drops the event for other pods. Read state from another device travels the same way. 3. **A failed fan-out access check** drops the event for that recipient. Only syncs are told, not live REQs. 4. **A channel removed from a batch subscription** with no CLOSED (revoke followed by a regrant whose notice was lost). 5. **Replica read budget** (`BUZZ_REPLICA_READ_MAX_AGE_MS`, if set in production): a replay or repair read can miss the newest events. A relay-side signal for these losses (a NOTICE or CLOSED to the affected live REQs, as syncs already get) would let the client repair at once. That is relay work and is not in this PR. ## Review - Auto reviewed the relay side and found no blockers (the list above). - Cerveau accepted the client-state review and did not report. This PR does not wait on it. ## Testing - `unread-startup.test.ts`: after a connection's own repair, 14 healthy ticks make no unread read; the tick at 15 minutes repairs. A failed repair is retried on the next tick, also after a live message marks the evidence observed. A failed backstop snapshot read with a successful evidence read is retried on the next tick. An evidence overflow is repaired on the next tick, and a covered reconnect after a drop leaves the next tick with no read. `read-state.test.ts`: a live marker that fails to apply, or a drop, owes a snapshot read until a snapshot read or a covered resume succeeds. A covered reconnect at 10 minutes does not postpone the full repair at 15. Each test fails without its rule. - Full vitest (873 files, 15,420 tests), tsc and biome pass on the PR head's tree (`19d0d4634`). The pre-push hook passed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Logan Johnson <[email protected]> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <[email protected]>
matt2e
added a commit
that referenced
this pull request
Nov 4, 2026
…next recovery tick reads it again Forward-port 2 of the split (#note:c2f2f5fd): the layer-1 part of dd306db on missing-messages-in-inbox (PR #1697 review, kalvinnchau, P3). Since #2252 the session's minute recovery tick repairs unread only while a repair is owed or UNREAD_BACKSTOP_INTERVAL (15 minutes) has passed since the last one. A pass that dropped a read after its retry (a window request, a thread read, a root fetch) ended with `partial` set but `owing` false, so every later tick skipped unread and the partial notice and unknown channels stayed until the backstop. A pass in which nothing landed throws and stays owed. The repair's tail now sets `owing = unchecked`. A pass that drops a read stays owed, as one that fails does, and the next tick repairs; a pass with every read landed pays the debt. A relay that keeps dropping one read costs one repair a minute, as one that keeps failing whole repairs does. session.ts (FOUNDATION) is unchanged: its gate reads `unread.owed()`. Docs: docs/inbox.md says the repair stays owed after a dropped read, so the next periodic check, a minute later, repairs again, where it said "no automatic retry". docs/relay-queries.md counts a repair that dropped a read as evidence debt, cleared only by one with every read landed. Its "an evidence overflow" goes from the same sentence: this series' evidence never resets on overflow, it evicts. Test (unread-startup, through the session's real recovery gate): "a pass that drops a window chunk leaves unread owed: ...". 129 channels, so the windows are two requests: the viewer's page with 127 windows, then two windows. A full reconnect pays the marker debt; a covered reconnect then drops the second window request after its retry (`partial: true`). A minute later, with the relay answering, the tick reads it again and `partial` clears; a minute after that, the tick reads no evidence. - With `owing = false` at the tail, as before: "expected 4 to be 5" at the tick a minute later. - With the tail always owed: "expected 21 to be 17" at the last tick. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
matt2e
added a commit
that referenced
this pull request
Nov 4, 2026
…t a repair's channel read, and fail only the timeline's page read Forward-port 6 of the layer-1 split: the test fixes main's later work needs, from 367b38c and 11f22a6. - Read floor (#1286). `createReadState` reads everything older than 30 days before the device clock as read, and the repair fixtures date in 1970, so 10 unread-repair tests saw nothing unread (and the two `keepRoot` tests passed without checking anything). Their setup takes `floor: () => 0`, as main's setups do. unread-fold's floor and #2366's fold-cache reorder came with forward-port 1, whose fold test needed them. - #2252's recovery tests counted a repair as one evidence read: a kind-9 request, or an `include_aux` filter with kind 40002 first. A layer-1 repair sends the authored page and windows, then the roster sample, so each counted two. `tickReads` counts a channel read (`#h`, no `#e` or `ids`, kind 40002) leading a request, which is the sample once a repair, and the four tests count with it. - entity-navigation's failed-message-read test failed any kind-9 read, so the Inbox's reads, which run first, used up its scripted failures. It fails only the timeline's `top_level` page read (11f22a6). The new "a pass that drops a window chunk leaves unread owed" test still fails with `owing = false` (4 reads, expected 5) and with unread always owed (16, expected 13) under the new count. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
matt2e
added a commit
that referenced
this pull request
Nov 5, 2026
…next recovery tick reads it again Forward-port 2 of the split (#note:c2f2f5fd): the layer-1 part of dd306db on missing-messages-in-inbox (PR #1697 review, kalvinnchau, P3). Since #2252 the session's minute recovery tick repairs unread only while a repair is owed or UNREAD_BACKSTOP_INTERVAL (15 minutes) has passed since the last one. A pass that dropped a read after its retry (a window request, a thread read, a root fetch) ended with `partial` set but `owing` false, so every later tick skipped unread and the partial notice and unknown channels stayed until the backstop. A pass in which nothing landed throws and stays owed. The repair's tail now sets `owing = unchecked`. A pass that drops a read stays owed, as one that fails does, and the next tick repairs; a pass with every read landed pays the debt. A relay that keeps dropping one read costs one repair a minute, as one that keeps failing whole repairs does. session.ts (FOUNDATION) is unchanged: its gate reads `unread.owed()`. Docs: docs/inbox.md says the repair stays owed after a dropped read, so the next periodic check, a minute later, repairs again, where it said "no automatic retry". docs/relay-queries.md counts a repair that dropped a read as evidence debt, cleared only by one with every read landed. Its "an evidence overflow" goes from the same sentence: this series' evidence never resets on overflow, it evicts. Test (unread-startup, through the session's real recovery gate): "a pass that drops a window chunk leaves unread owed: ...". 129 channels, so the windows are two requests: the viewer's page with 127 windows, then two windows. A full reconnect pays the marker debt; a covered reconnect then drops the second window request after its retry (`partial: true`). A minute later, with the relay answering, the tick reads it again and `partial` clears; a minute after that, the tick reads no evidence. - With `owing = false` at the tail, as before: "expected 4 to be 5" at the tick a minute later. - With the tail always owed: "expected 21 to be 17" at the last tick. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
matt2e
added a commit
that referenced
this pull request
Nov 5, 2026
…t a repair's channel read, and fail only the timeline's page read Forward-port 6 of the layer-1 split: the test fixes main's later work needs, from 367b38c and 11f22a6. - Read floor (#1286). `createReadState` reads everything older than 30 days before the device clock as read, and the repair fixtures date in 1970, so 10 unread-repair tests saw nothing unread (and the two `keepRoot` tests passed without checking anything). Their setup takes `floor: () => 0`, as main's setups do. unread-fold's floor and #2366's fold-cache reorder came with forward-port 1, whose fold test needed them. - #2252's recovery tests counted a repair as one evidence read: a kind-9 request, or an `include_aux` filter with kind 40002 first. A layer-1 repair sends the authored page and windows, then the roster sample, so each counted two. `tickReads` counts a channel read (`#h`, no `#e` or `ids`, kind 40002) leading a request, which is the sample once a repair, and the four tests count with it. - entity-navigation's failed-message-read test failed any kind-9 read, so the Inbox's reads, which run first, used up its scripted failures. It fails only the timeline's `top_level` page read (11f22a6). The new "a pass that drops a window chunk leaves unread owed" test still fails with `owing = false` (4 reads, expected 5) and with unread always owed (16, expected 13) under the new count. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
matt2e
added a commit
that referenced
this pull request
Nov 5, 2026
…next recovery tick reads it again Forward-port 2 of the split (#note:c2f2f5fd): the layer-1 part of dd306db on missing-messages-in-inbox (PR #1697 review, kalvinnchau, P3). Since #2252 the session's minute recovery tick repairs unread only while a repair is owed or UNREAD_BACKSTOP_INTERVAL (15 minutes) has passed since the last one. A pass that dropped a read after its retry (a window request, a thread read, a root fetch) ended with `partial` set but `owing` false, so every later tick skipped unread and the partial notice and unknown channels stayed until the backstop. A pass in which nothing landed throws and stays owed. The repair's tail now sets `owing = unchecked`. A pass that drops a read stays owed, as one that fails does, and the next tick repairs; a pass with every read landed pays the debt. A relay that keeps dropping one read costs one repair a minute, as one that keeps failing whole repairs does. session.ts (FOUNDATION) is unchanged: its gate reads `unread.owed()`. Docs: docs/inbox.md says the repair stays owed after a dropped read, so the next periodic check, a minute later, repairs again, where it said "no automatic retry". docs/relay-queries.md counts a repair that dropped a read as evidence debt, cleared only by one with every read landed. Its "an evidence overflow" goes from the same sentence: this series' evidence never resets on overflow, it evicts. Test (unread-startup, through the session's real recovery gate): "a pass that drops a window chunk leaves unread owed: ...". 129 channels, so the windows are two requests: the viewer's page with 127 windows, then two windows. A full reconnect pays the marker debt; a covered reconnect then drops the second window request after its retry (`partial: true`). A minute later, with the relay answering, the tick reads it again and `partial` clears; a minute after that, the tick reads no evidence. - With `owing = false` at the tail, as before: "expected 4 to be 5" at the tick a minute later. - With the tail always owed: "expected 21 to be 17" at the last tick. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
matt2e
added a commit
that referenced
this pull request
Nov 5, 2026
…t a repair's channel read, and fail only the timeline's page read Forward-port 6 of the layer-1 split: the test fixes main's later work needs, from 367b38c and 11f22a6. - Read floor (#1286). `createReadState` reads everything older than 30 days before the device clock as read, and the repair fixtures date in 1970, so 10 unread-repair tests saw nothing unread (and the two `keepRoot` tests passed without checking anything). Their setup takes `floor: () => 0`, as main's setups do. unread-fold's floor and #2366's fold-cache reorder came with forward-port 1, whose fold test needed them. - #2252's recovery tests counted a repair as one evidence read: a kind-9 request, or an `include_aux` filter with kind 40002 first. A layer-1 repair sends the authored page and windows, then the roster sample, so each counted two. `tickReads` counts a channel read (`#h`, no `#e` or `ids`, kind 40002) leading a request, which is the sample once a repair, and the four tests count with it. - entity-navigation's failed-message-read test failed any kind-9 read, so the Inbox's reads, which run first, used up its scripted failures. It fails only the timeline's `top_level` page read (11f22a6). The new "a pass that drops a window chunk leaves unread owed" test still fails with `owing = false` (4 reads, expected 5) and with unread always owed (16, expected 13) under the new count. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
matt2e
added a commit
that referenced
this pull request
Nov 5, 2026
…next recovery tick reads it again Forward-port 2 of the split (#note:c2f2f5fd): the layer-1 part of dd306db on missing-messages-in-inbox (PR #1697 review, kalvinnchau, P3). Since #2252 the session's minute recovery tick repairs unread only while a repair is owed or UNREAD_BACKSTOP_INTERVAL (15 minutes) has passed since the last one. A pass that dropped a read after its retry (a window request, a thread read, a root fetch) ended with `partial` set but `owing` false, so every later tick skipped unread and the partial notice and unknown channels stayed until the backstop. A pass in which nothing landed throws and stays owed. The repair's tail now sets `owing = unchecked`. A pass that drops a read stays owed, as one that fails does, and the next tick repairs; a pass with every read landed pays the debt. A relay that keeps dropping one read costs one repair a minute, as one that keeps failing whole repairs does. session.ts (FOUNDATION) is unchanged: its gate reads `unread.owed()`. Docs: docs/inbox.md says the repair stays owed after a dropped read, so the next periodic check, a minute later, repairs again, where it said "no automatic retry". docs/relay-queries.md counts a repair that dropped a read as evidence debt, cleared only by one with every read landed. Its "an evidence overflow" goes from the same sentence: this series' evidence never resets on overflow, it evicts. Test (unread-startup, through the session's real recovery gate): "a pass that drops a window chunk leaves unread owed: ...". 129 channels, so the windows are two requests: the viewer's page with 127 windows, then two windows. A full reconnect pays the marker debt; a covered reconnect then drops the second window request after its retry (`partial: true`). A minute later, with the relay answering, the tick reads it again and `partial` clears; a minute after that, the tick reads no evidence. - With `owing = false` at the tail, as before: "expected 4 to be 5" at the tick a minute later. - With the tail always owed: "expected 21 to be 17" at the last tick. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
matt2e
added a commit
that referenced
this pull request
Nov 5, 2026
…t a repair's channel read, and fail only the timeline's page read Forward-port 6 of the layer-1 split: the test fixes main's later work needs, from 367b38c and 11f22a6. - Read floor (#1286). `createReadState` reads everything older than 30 days before the device clock as read, and the repair fixtures date in 1970, so 10 unread-repair tests saw nothing unread (and the two `keepRoot` tests passed without checking anything). Their setup takes `floor: () => 0`, as main's setups do. unread-fold's floor and #2366's fold-cache reorder came with forward-port 1, whose fold test needed them. - #2252's recovery tests counted a repair as one evidence read: a kind-9 request, or an `include_aux` filter with kind 40002 first. A layer-1 repair sends the authored page and windows, then the roster sample, so each counted two. `tickReads` counts a channel read (`#h`, no `#e` or `ids`, kind 40002) leading a request, which is the sample once a repair, and the four tests count with it. - entity-navigation's failed-message-read test failed any kind-9 read, so the Inbox's reads, which run first, used up its scripted failures. It fails only the timeline's `top_level` page read (11f22a6). The new "a pass that drops a window chunk leaves unread owed" test still fails with `owing = false` (4 reads, expected 5) and with unread always owed (16, expected 13) under the new count. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
matt2e
added a commit
that referenced
this pull request
Nov 6, 2026
…next recovery tick reads it again Forward-port 2 of the split (#note:c2f2f5fd): the layer-1 part of dd306db on missing-messages-in-inbox (PR #1697 review, kalvinnchau, P3). Since #2252 the session's minute recovery tick repairs unread only while a repair is owed or UNREAD_BACKSTOP_INTERVAL (15 minutes) has passed since the last one. A pass that dropped a read after its retry (a window request, a thread read, a root fetch) ended with `partial` set but `owing` false, so every later tick skipped unread and the partial notice and unknown channels stayed until the backstop. A pass in which nothing landed throws and stays owed. The repair's tail now sets `owing = unchecked`. A pass that drops a read stays owed, as one that fails does, and the next tick repairs; a pass with every read landed pays the debt. A relay that keeps dropping one read costs one repair a minute, as one that keeps failing whole repairs does. session.ts (FOUNDATION) is unchanged: its gate reads `unread.owed()`. Docs: docs/inbox.md says the repair stays owed after a dropped read, so the next periodic check, a minute later, repairs again, where it said "no automatic retry". docs/relay-queries.md counts a repair that dropped a read as evidence debt, cleared only by one with every read landed. Its "an evidence overflow" goes from the same sentence: this series' evidence never resets on overflow, it evicts. Test (unread-startup, through the session's real recovery gate): "a pass that drops a window chunk leaves unread owed: ...". 129 channels, so the windows are two requests: the viewer's page with 127 windows, then two windows. A full reconnect pays the marker debt; a covered reconnect then drops the second window request after its retry (`partial: true`). A minute later, with the relay answering, the tick reads it again and `partial` clears; a minute after that, the tick reads no evidence. - With `owing = false` at the tail, as before: "expected 4 to be 5" at the tick a minute later. - With the tail always owed: "expected 21 to be 17" at the last tick. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
matt2e
added a commit
that referenced
this pull request
Nov 6, 2026
…t a repair's channel read, and fail only the timeline's page read Forward-port 6 of the layer-1 split: the test fixes main's later work needs, from 367b38c and 11f22a6. - Read floor (#1286). `createReadState` reads everything older than 30 days before the device clock as read, and the repair fixtures date in 1970, so 10 unread-repair tests saw nothing unread (and the two `keepRoot` tests passed without checking anything). Their setup takes `floor: () => 0`, as main's setups do. unread-fold's floor and #2366's fold-cache reorder came with forward-port 1, whose fold test needed them. - #2252's recovery tests counted a repair as one evidence read: a kind-9 request, or an `include_aux` filter with kind 40002 first. A layer-1 repair sends the authored page and windows, then the roster sample, so each counted two. `tickReads` counts a channel read (`#h`, no `#e` or `ids`, kind 40002) leading a request, which is the sample once a repair, and the four tests count with it. - entity-navigation's failed-message-read test failed any kind-9 read, so the Inbox's reads, which run first, used up its scripted failures. It fails only the timeline's `top_level` page read (11f22a6). The new "a pass that drops a window chunk leaves unread owed" test still fails with `owing = false` (4 reads, expected 5) and with unread always owed (16, expected 13) under the new count. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
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
UNREAD_BACKSTOP_INTERVAL).src/features/relay/session.tsis marked FOUNDATION. Logan asked for this change directly.Details
session.ts:repairUnread()callsunread.reconnect()and, for a full repair, records when it was asked. The membership route's covered-read step (from fix(relay): skip status, emoji, read-state and reminder re-reads a continuous replay covers #2204) calls it with the replay's continuity, so each connection's full repair resets the clock; a covered resume reads no read-state snapshot and leaves the clock alone.recover()repairs at once whileunread.owed(), and otherwise only whenUNREAD_BACKSTOP_INTERVALhas passed. This matches the roster backstop from fix(relay): read the full channel list from the recovery tick only every 15 minutes #2215.unread.ts:owed()reports a repair still owed. The unread owner tracks its own debt: a failed repair, an evidence overflow or a drop sets it, and only a completed repair clears it, so a live message that marks the evidence observed again does not clear it.read-state.ts:owed()reports a snapshot read still owed: a failed snapshot read, a failed marker apply or a drop sets it, and a successful snapshot read or resume clears it.unread.owed()includes it, so a backstop whose snapshot read failed retries on the next tick even when its evidence read succeeded.docs/relay-queries.md: the recovery-timer paragraph describes the unread backstop next to the roster backstop.Silent losses this backstop repairs
A relay review (block/buzz
009dc103f) found these ways a connected client can lose an unread-relevant event (kinds 9, 40002, 40008, and read state 30078) with no signal. For channels that are not open in a window, the tick repair is the only fix today. With this change each one takes up to 15 minutes to heal:try_sendand drops the frame. The relay cancels the connection only after 15 consecutive full sends, so short bursts of drops are silent.BUZZ_REPLICA_READ_MAX_AGE_MS, if set in production): a replay or repair read can miss the newest events.A relay-side signal for these losses (a NOTICE or CLOSED to the affected live REQs, as syncs already get) would let the client repair at once. That is relay work and is not in this PR.
Review
Testing
unread-startup.test.ts: after a connection's own repair, 14 healthy ticks make no unread read; the tick at 15 minutes repairs. A failed repair is retried on the next tick, also after a live message marks the evidence observed. A failed backstop snapshot read with a successful evidence read is retried on the next tick. An evidence overflow is repaired on the next tick, and a covered reconnect after a drop leaves the next tick with no read.read-state.test.ts: a live marker that fails to apply, or a drop, owes a snapshot read until a snapshot read or a covered resume succeeds. A covered reconnect at 10 minutes does not postpone the full repair at 15. Each test fails without its rule.19d0d4634). The pre-push hook passed.🤖 Generated with Claude Code