Repository navigation
Conversation
72cb30b to
4d69bfb
Compare
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Star Lord’s automated source review via Wes’s account.
Three P2 correctness fixes and one P3 keyboard-focus fix are detailed inline. Rocket, Mantis, Groot and integration review are complete.
Head 4d69bfbf85fdf6a1e2ee94c70c6679a21f3b40dc; base b6e1941e6e29f2f688ba8e4920c5aaa175397221. Source only: no tests or app execution. CI was still running in the snapshot; native behavior, real IndexedDB lifecycle and failure-path interactions remain unverified.
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 Two product regressions and one browser-test ordering defect are detailed inline. The duplicate participation-budget defect is already covered by comment 4387616679 and is not repeated.
[P2] The required profile-preview journey also fails in Chromium and WebKit at this head: tests/fixtures/profiles.tsx:247 returns room evidence for the new per-channel queries, adding an unread badge so the exact button name "One" no longer matches. The failure was reproduced; this fixture/locator explanation is inferred from the fixture and failure snapshot. Update the fixture query handling or locator while retaining the profile-preview assertions.
… split oversized reads Review of 4d69bfb (PR #1697, wesbillman and kalvinnchau): seven inline threads and one failing required browser journey. Participation budget: a full-page thread read's split rereads replies the page already admitted, and they were charged again, so four remembered roots of 200 replies could exhaust the 1,024-reply budget on duplicates and drop a thread. Admitted replies are now tracked by ID across the pass and a full page is judged against the filter's own limit. Overlays at capacity: a peer edit or deletion ranked as unpinned because its `e` reference has no reply marker, so once pinned evidence filled the budget it was silently dropped and the superseded body stayed counted. An edit or deletion now ranks with its newest-ranked content target and is admitted while the target is; when its own target goes first, the target is retired with it. Remembered threads after restart: a remembered thread whose viewer reply is older than both the authored page and the 200-reply thread page lost its membership once its replies were read, since `want()` suppressed the lookup for read replies. Read replies in a remembered thread now run the membership lookup, which recovers the viewer's reply as a verified witness or finds it deleted and lets the thread leave; mention-only threads still gain no membership. Capacity across channels: eviction was oldest-first across every channel, so a busy channel's live traffic could evict a quiet channel's only unread message, which was then rejected as too old on refresh and read as a confident zero. Ordinary evidence is now evicted from the channel holding the most of the exhausted budget (events or bytes), oldest first; pinned evidence still goes last. A channel left with nothing retained loses observed coverage instead of counting zero. Oversized responses: a roster's windows can exceed the reader's 8 MiB response cap (40 channels of 76 messages with 3,000-byte bodies), and the identical retry failed the repair with no Inbox items. A response over the cap now splits the request deterministically — filters in halves, a lone filter's `ids` in halves, a lone filter's limit halved, dropping a filter at limit 1 — and the parts are read next; `READ_BUDGET_EXCEEDED` names the reader's error. Inbox partial notice: keyboard-activating its Refresh removed the focused button with no destination. It now joins the `retryButton` ownership handoff; focus moves to the alert's Retry when the refresh fails and to a stable Inbox control when it succeeds, and stays wherever the person moved it meanwhile. Ownership is only claimed by the ref release, since a control's ref is also released when its props change. Tests: the sidebar-unread journey waits until Alpha's window is held rather than asserting on whichever concurrent batch arrived first; the profiles fixture answers the repair's own filter shapes with nothing, as its former single evidence query did, so "One" carries no badge. Regressions cover each fix, including an owner-through-real-reader run of the oversized-response split. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
|
🤖 Re: the failing profile-preview journey (kalvinnchau's review body) — fixed in 29a55a7 by the fixture: |
… split oversized reads Review of 4d69bfb (PR #1697, wesbillman and kalvinnchau): seven inline threads and one failing required browser journey. Participation budget: a full-page thread read's split rereads replies the page already admitted, and they were charged again, so four remembered roots of 200 replies could exhaust the 1,024-reply budget on duplicates and drop a thread. Admitted replies are now tracked by ID across the pass and a full page is judged against the filter's own limit. Overlays at capacity: a peer edit or deletion ranked as unpinned because its `e` reference has no reply marker, so once pinned evidence filled the budget it was silently dropped and the superseded body stayed counted. An edit or deletion now ranks with its newest-ranked content target and is admitted while the target is; when its own target goes first, the target is retired with it. Remembered threads after restart: a remembered thread whose viewer reply is older than both the authored page and the 200-reply thread page lost its membership once its replies were read, since `want()` suppressed the lookup for read replies. Read replies in a remembered thread now run the membership lookup, which recovers the viewer's reply as a verified witness or finds it deleted and lets the thread leave; mention-only threads still gain no membership. Capacity across channels: eviction was oldest-first across every channel, so a busy channel's live traffic could evict a quiet channel's only unread message, which was then rejected as too old on refresh and read as a confident zero. Ordinary evidence is now evicted from the channel holding the most of the exhausted budget (events or bytes), oldest first; pinned evidence still goes last. A channel left with nothing retained loses observed coverage instead of counting zero. Oversized responses: a roster's windows can exceed the reader's 8 MiB response cap (40 channels of 76 messages with 3,000-byte bodies), and the identical retry failed the repair with no Inbox items. A response over the cap now splits the request deterministically — filters in halves, a lone filter's `ids` in halves, a lone filter's limit halved, dropping a filter at limit 1 — and the parts are read next; `READ_BUDGET_EXCEEDED` names the reader's error. Inbox partial notice: keyboard-activating its Refresh removed the focused button with no destination. It now joins the `retryButton` ownership handoff; focus moves to the alert's Retry when the refresh fails and to a stable Inbox control when it succeeds, and stays wherever the person moved it meanwhile. Ownership is only claimed by the ref release, since a control's ref is also released when its props change. Tests: the sidebar-unread journey waits until Alpha's window is held rather than asserting on whichever concurrent batch arrived first; the profiles fixture answers the repair's own filter shapes with nothing, as its former single evidence query did, so "One" carries no badge. Regressions cover each fix, including an owner-through-real-reader run of the oversized-response split. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
29a55a7 to
ffdf357
Compare
…loader, count only channel cursor pages CI on ffdf357 (PR #1697): the JavaScript shard and two browser journeys. The broker's Vite config reaches `unread-window-batch.ts` through `relay-broker.mjs`, and its extension-less `./events` import drew Vite's `configLoader: 'native'` warning that `vite-config.test.mjs` forbids. The import now names `./events.ts`, as `channel-details-protocol.ts` does. The message-navigation and scroll-paging journeys counted every query with `until` as a channel cursor page. Their fixture history is written by the viewer, so the authored page is full and the walk continues with `until` + `before_id` pages of its own (one and two respectively). Both now count cursor pages by `#h`. Checks: `vite-config.test.mjs` passes; biome and tsc clean; Playwright chromium message-navigation (13) and chromium-measurements scroll cursor paging pass. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 Two recovery defects remain at this head; details inline.
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Star Lord’s automated source review via Wes’s account. One additional P2—missing retry feedback after dropped thread reads—is detailed inline.
Source review also corroborates the existing cold-thread disappearance and native size-rejection mismatch; those findings are not duplicated here. The earlier admission, overlay, retention and focus fixes are present.
P3 — Sanitize public review material. The PR description and performance narratives in several commit messages include live workspace channel names and channel/message ID prefixes. Replace those identifiers with generic labels while retaining measurements and legitimate attribution.
Rocket, Mantis, Groot and integration review complete. Head 1cd1136aab64d8b22cb7e8c73df05cd2d9e9f9aa; base 1db9a6ca8294c356b8d845b941af3339e22f1578. Source only: no tests or app execution; native, IndexedDB and failure-path behavior unverified. CI snapshot successful except Windows validation skipped.
… thread read is dropped, state the cold-tier row contract Review of 1cd1136 (PR #1697, kalvinnchau and wesbillman): three P2 threads. Native size cap: the native host reads at most 16 MiB (`read_response`) and rejects before the reader can measure, with the plain string `Relay response is too large` its IPC carries; the repair's `oversized()` knew only the reader's 8 MiB `ReadError`, so a roster whose windows came back over the cap was retried once identically, dropped, and left the Inbox stale with unknown counts. `READ_BUDGET_EXCEEDED` moves to errors.ts beside the other native messages, and `isOversizedResponse()` names both rejections; the repair splits on either. Regression (unread-startup): a 40-channel roster whose 41-filter request the native host rejects splits to 21 + 20 and every channel is observed (pre-fix: `[41, 41]`, stale, as the reviewer's probe found). Dropped thread reads: a dropped `#e`/`ids` read reached `pass.landed` with no result and bypassed `unchecked`, so the repair ended `observed` with no notice while a participated thread's unread reply was missing. A dropped thread read or root fetch now sets `partial` too; its channels' counts stay observed, the Inbox shows the soft notice with Refresh, and a dropped walk page still leaves only the walk's mark for the next repair. The notice reads "Some recent activity could not be checked." since what is missing need not be a channel. The dropped-cold-read regression now asserts `partial: true`, no error, the channel observed, and `partial` cleared by a Refresh that lands the reply. Cold-tier rows after relaunch: a remembered thread outside the 50 hot ones whose replies the viewer has all read is not a row on later launches, since its boundary reply is passed over by ID and the walk resumes from its mark. This is the contract 520f885 chose on measurement (137 warm rows where 52 belong, two extra root-fetch requests), consistent with channel evidence older than its window; docs/inbox.md now says so explicitly — cold threads are rows while they hold a reply the viewer has not read — rather than "stay in the Inbox", and the walk test names it where it asserts the quiet thread absent. Checks (this checkout, load 25–33): `tsc --noEmit` clean; biome clean on the changed files; vitest unread-repair, unread-startup, unread-lookups, unread, reader, thread-participation and InboxPage suites 7 files, 378 tests pass. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
… split oversized reads Review of 4d69bfb (PR #1697, wesbillman and kalvinnchau): seven inline threads and one failing required browser journey. Participation budget: a full-page thread read's split rereads replies the page already admitted, and they were charged again, so four remembered roots of 200 replies could exhaust the 1,024-reply budget on duplicates and drop a thread. Admitted replies are now tracked by ID across the pass and a full page is judged against the filter's own limit. Overlays at capacity: a peer edit or deletion ranked as unpinned because its `e` reference has no reply marker, so once pinned evidence filled the budget it was silently dropped and the superseded body stayed counted. An edit or deletion now ranks with its newest-ranked content target and is admitted while the target is; when its own target goes first, the target is retired with it. Remembered threads after restart: a remembered thread whose viewer reply is older than both the authored page and the 200-reply thread page lost its membership once its replies were read, since `want()` suppressed the lookup for read replies. Read replies in a remembered thread now run the membership lookup, which recovers the viewer's reply as a verified witness or finds it deleted and lets the thread leave; mention-only threads still gain no membership. Capacity across channels: eviction was oldest-first across every channel, so a busy channel's live traffic could evict a quiet channel's only unread message, which was then rejected as too old on refresh and read as a confident zero. Ordinary evidence is now evicted from the channel holding the most of the exhausted budget (events or bytes), oldest first; pinned evidence still goes last. A channel left with nothing retained loses observed coverage instead of counting zero. Oversized responses: a roster's windows can exceed the reader's 8 MiB response cap (40 channels of 76 messages with 3,000-byte bodies), and the identical retry failed the repair with no Inbox items. A response over the cap now splits the request deterministically — filters in halves, a lone filter's `ids` in halves, a lone filter's limit halved, dropping a filter at limit 1 — and the parts are read next; `READ_BUDGET_EXCEEDED` names the reader's error. Inbox partial notice: keyboard-activating its Refresh removed the focused button with no destination. It now joins the `retryButton` ownership handoff; focus moves to the alert's Retry when the refresh fails and to a stable Inbox control when it succeeds, and stays wherever the person moved it meanwhile. Ownership is only claimed by the ref release, since a control's ref is also released when its props change. Tests: the sidebar-unread journey waits until Alpha's window is held rather than asserting on whichever concurrent batch arrived first; the profiles fixture answers the repair's own filter shapes with nothing, as its former single evidence query did, so "One" carries no badge. Regressions cover each fix, including an owner-through-real-reader run of the oversized-response split. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
…loader, count only channel cursor pages CI on ffdf357 (PR #1697): the JavaScript shard and two browser journeys. The broker's Vite config reaches `unread-window-batch.ts` through `relay-broker.mjs`, and its extension-less `./events` import drew Vite's `configLoader: 'native'` warning that `vite-config.test.mjs` forbids. The import now names `./events.ts`, as `channel-details-protocol.ts` does. The message-navigation and scroll-paging journeys counted every query with `until` as a channel cursor page. Their fixture history is written by the viewer, so the authored page is full and the walk continues with `until` + `before_id` pages of its own (one and two respectively). Both now count cursor pages by `#h`. Checks: `vite-config.test.mjs` passes; biome and tsc clean; Playwright chromium message-navigation (13) and chromium-measurements scroll cursor paging pass. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
dfb0339 to
1c42832
Compare
… thread read is dropped, state the cold-tier row contract Review of 1cd1136 (PR #1697, kalvinnchau and wesbillman): three P2 threads. Native size cap: the native host reads at most 16 MiB (`read_response`) and rejects before the reader can measure, with the plain string `Relay response is too large` its IPC carries; the repair's `oversized()` knew only the reader's 8 MiB `ReadError`, so a roster whose windows came back over the cap was retried once identically, dropped, and left the Inbox stale with unknown counts. `READ_BUDGET_EXCEEDED` moves to errors.ts beside the other native messages, and `isOversizedResponse()` names both rejections; the repair splits on either. Regression (unread-startup): a 40-channel roster whose 41-filter request the native host rejects splits to 21 + 20 and every channel is observed (pre-fix: `[41, 41]`, stale, as the reviewer's probe found). Dropped thread reads: a dropped `#e`/`ids` read reached `pass.landed` with no result and bypassed `unchecked`, so the repair ended `observed` with no notice while a participated thread's unread reply was missing. A dropped thread read or root fetch now sets `partial` too; its channels' counts stay observed, the Inbox shows the soft notice with Refresh, and a dropped walk page still leaves only the walk's mark for the next repair. The notice reads "Some recent activity could not be checked." since what is missing need not be a channel. The dropped-cold-read regression now asserts `partial: true`, no error, the channel observed, and `partial` cleared by a Refresh that lands the reply. Cold-tier rows after relaunch: a remembered thread outside the 50 hot ones whose replies the viewer has all read is not a row on later launches, since its boundary reply is passed over by ID and the walk resumes from its mark. This is the contract 520f885 chose on measurement (137 warm rows where 52 belong, two extra root-fetch requests), consistent with channel evidence older than its window; docs/inbox.md now says so explicitly — cold threads are rows while they hold a reply the viewer has not read — rather than "stay in the Inbox", and the walk test names it where it asserts the quiet thread absent. Checks (this checkout, load 25–33): `tsc --noEmit` clean; biome clean on the changed files; vitest unread-repair, unread-startup, unread-lookups, unread, reader, thread-participation and InboxPage suites 7 files, 378 tests pass. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
… split oversized reads Review of 4d69bfb (PR #1697, wesbillman and kalvinnchau): seven inline threads and one failing required browser journey. Participation budget: a full-page thread read's split rereads replies the page already admitted, and they were charged again, so four remembered roots of 200 replies could exhaust the 1,024-reply budget on duplicates and drop a thread. Admitted replies are now tracked by ID across the pass and a full page is judged against the filter's own limit. Overlays at capacity: a peer edit or deletion ranked as unpinned because its `e` reference has no reply marker, so once pinned evidence filled the budget it was silently dropped and the superseded body stayed counted. An edit or deletion now ranks with its newest-ranked content target and is admitted while the target is; when its own target goes first, the target is retired with it. Remembered threads after restart: a remembered thread whose viewer reply is older than both the authored page and the 200-reply thread page lost its membership once its replies were read, since `want()` suppressed the lookup for read replies. Read replies in a remembered thread now run the membership lookup, which recovers the viewer's reply as a verified witness or finds it deleted and lets the thread leave; mention-only threads still gain no membership. Capacity across channels: eviction was oldest-first across every channel, so a busy channel's live traffic could evict a quiet channel's only unread message, which was then rejected as too old on refresh and read as a confident zero. Ordinary evidence is now evicted from the channel holding the most of the exhausted budget (events or bytes), oldest first; pinned evidence still goes last. A channel left with nothing retained loses observed coverage instead of counting zero. Oversized responses: a roster's windows can exceed the reader's 8 MiB response cap (40 channels of 76 messages with 3,000-byte bodies), and the identical retry failed the repair with no Inbox items. A response over the cap now splits the request deterministically — filters in halves, a lone filter's `ids` in halves, a lone filter's limit halved, dropping a filter at limit 1 — and the parts are read next; `READ_BUDGET_EXCEEDED` names the reader's error. Inbox partial notice: keyboard-activating its Refresh removed the focused button with no destination. It now joins the `retryButton` ownership handoff; focus moves to the alert's Retry when the refresh fails and to a stable Inbox control when it succeeds, and stays wherever the person moved it meanwhile. Ownership is only claimed by the ref release, since a control's ref is also released when its props change. Tests: the sidebar-unread journey waits until Alpha's window is held rather than asserting on whichever concurrent batch arrived first; the profiles fixture answers the repair's own filter shapes with nothing, as its former single evidence query did, so "One" carries no badge. Regressions cover each fix, including an owner-through-real-reader run of the oversized-response split. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
1c42832 to
ea5d85d
Compare
…loader, count only channel cursor pages CI on ffdf357 (PR #1697): the JavaScript shard and two browser journeys. The broker's Vite config reaches `unread-window-batch.ts` through `relay-broker.mjs`, and its extension-less `./events` import drew Vite's `configLoader: 'native'` warning that `vite-config.test.mjs` forbids. The import now names `./events.ts`, as `channel-details-protocol.ts` does. The message-navigation and scroll-paging journeys counted every query with `until` as a channel cursor page. Their fixture history is written by the viewer, so the authored page is full and the walk continues with `until` + `before_id` pages of its own (one and two respectively). Both now count cursor pages by `#h`. Checks: `vite-config.test.mjs` passes; biome and tsc clean; Playwright chromium message-navigation (13) and chromium-measurements scroll cursor paging pass. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
… thread read is dropped, state the cold-tier row contract Review of 1cd1136 (PR #1697, kalvinnchau and wesbillman): three P2 threads. Native size cap: the native host reads at most 16 MiB (`read_response`) and rejects before the reader can measure, with the plain string `Relay response is too large` its IPC carries; the repair's `oversized()` knew only the reader's 8 MiB `ReadError`, so a roster whose windows came back over the cap was retried once identically, dropped, and left the Inbox stale with unknown counts. `READ_BUDGET_EXCEEDED` moves to errors.ts beside the other native messages, and `isOversizedResponse()` names both rejections; the repair splits on either. Regression (unread-startup): a 40-channel roster whose 41-filter request the native host rejects splits to 21 + 20 and every channel is observed (pre-fix: `[41, 41]`, stale, as the reviewer's probe found). Dropped thread reads: a dropped `#e`/`ids` read reached `pass.landed` with no result and bypassed `unchecked`, so the repair ended `observed` with no notice while a participated thread's unread reply was missing. A dropped thread read or root fetch now sets `partial` too; its channels' counts stay observed, the Inbox shows the soft notice with Refresh, and a dropped walk page still leaves only the walk's mark for the next repair. The notice reads "Some recent activity could not be checked." since what is missing need not be a channel. The dropped-cold-read regression now asserts `partial: true`, no error, the channel observed, and `partial` cleared by a Refresh that lands the reply. Cold-tier rows after relaunch: a remembered thread outside the 50 hot ones whose replies the viewer has all read is not a row on later launches, since its boundary reply is passed over by ID and the walk resumes from its mark. This is the contract 520f885 chose on measurement (137 warm rows where 52 belong, two extra root-fetch requests), consistent with channel evidence older than its window; docs/inbox.md now says so explicitly — cold threads are rows while they hold a reply the viewer has not read — rather than "stay in the Inbox", and the walk test names it where it asserts the quiet thread absent. Checks (this checkout, load 25–33): `tsc --noEmit` clean; biome clean on the changed files; vitest unread-repair, unread-startup, unread-lookups, unread, reader, thread-participation and InboxPage suites 7 files, 378 tests pass. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
… split oversized reads Review of 4d69bfb (PR #1697, wesbillman and kalvinnchau): seven inline threads and one failing required browser journey. Participation budget: a full-page thread read's split rereads replies the page already admitted, and they were charged again, so four remembered roots of 200 replies could exhaust the 1,024-reply budget on duplicates and drop a thread. Admitted replies are now tracked by ID across the pass and a full page is judged against the filter's own limit. Overlays at capacity: a peer edit or deletion ranked as unpinned because its `e` reference has no reply marker, so once pinned evidence filled the budget it was silently dropped and the superseded body stayed counted. An edit or deletion now ranks with its newest-ranked content target and is admitted while the target is; when its own target goes first, the target is retired with it. Remembered threads after restart: a remembered thread whose viewer reply is older than both the authored page and the 200-reply thread page lost its membership once its replies were read, since `want()` suppressed the lookup for read replies. Read replies in a remembered thread now run the membership lookup, which recovers the viewer's reply as a verified witness or finds it deleted and lets the thread leave; mention-only threads still gain no membership. Capacity across channels: eviction was oldest-first across every channel, so a busy channel's live traffic could evict a quiet channel's only unread message, which was then rejected as too old on refresh and read as a confident zero. Ordinary evidence is now evicted from the channel holding the most of the exhausted budget (events or bytes), oldest first; pinned evidence still goes last. A channel left with nothing retained loses observed coverage instead of counting zero. Oversized responses: a roster's windows can exceed the reader's 8 MiB response cap (40 channels of 76 messages with 3,000-byte bodies), and the identical retry failed the repair with no Inbox items. A response over the cap now splits the request deterministically — filters in halves, a lone filter's `ids` in halves, a lone filter's limit halved, dropping a filter at limit 1 — and the parts are read next; `READ_BUDGET_EXCEEDED` names the reader's error. Inbox partial notice: keyboard-activating its Refresh removed the focused button with no destination. It now joins the `retryButton` ownership handoff; focus moves to the alert's Retry when the refresh fails and to a stable Inbox control when it succeeds, and stays wherever the person moved it meanwhile. Ownership is only claimed by the ref release, since a control's ref is also released when its props change. Tests: the sidebar-unread journey waits until Alpha's window is held rather than asserting on whichever concurrent batch arrived first; the profiles fixture answers the repair's own filter shapes with nothing, as its former single evidence query did, so "One" carries no badge. Regressions cover each fix, including an owner-through-real-reader run of the oversized-response split. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
6cf1014 to
1280aa1
Compare
…loader, count only channel cursor pages CI on ffdf357 (PR #1697): the JavaScript shard and two browser journeys. The broker's Vite config reaches `unread-window-batch.ts` through `relay-broker.mjs`, and its extension-less `./events` import drew Vite's `configLoader: 'native'` warning that `vite-config.test.mjs` forbids. The import now names `./events.ts`, as `channel-details-protocol.ts` does. The message-navigation and scroll-paging journeys counted every query with `until` as a channel cursor page. Their fixture history is written by the viewer, so the authored page is full and the walk continues with `until` + `before_id` pages of its own (one and two respectively). Both now count cursor pages by `#h`. Checks: `vite-config.test.mjs` passes; biome and tsc clean; Playwright chromium message-navigation (13) and chromium-measurements scroll cursor paging pass. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
…over-cap session's catch-up, and rewrap two Inbox runs Answers review 7082f452 of a11a083: its three optional notes. No behaviour changes: comments and docs only, no constant or test moves. 1. unread.ts, ID_BATCH and HOME_BATCH. The two comments read as a contradiction: ID_BATCH said the relay plans 100 IDs as a scan (5 → 66 ms), while HOME_BATCH said the live relay answers 100 in about the time of 50 (44 against 40 ms). They measured different relays. - ID_BATCH now says the 66 ms came from a local relay on PostgreSQL 18.6 (the study in note 28de0d7e), and that the live relay did not reproduce it. - HOME_BATCH says its figures are the live relay's relay time, replaying real findHomes ID sets with no cliff at 100 (note 7adcdfc7, "(a) ID lookups"). Should the live relay's planner show that cliff, HOME_BATCH is the constant to lower back to ID_BATCH. - Neither constant changes. No EXPLAIN was run, since the live relay is read-only to us. The review accepts a statement of where each figure came from in its place. 2. docs/unread.md. "While that record is complete ... catches up" read as the saved record alone. Since 63c7ffa a session whose selection is too large to save writes an incomplete record but keeps catching up from what it holds, as docs/inbox.md says. A clause now says so. The rest of the bullet is rewrapped, with the link kept on one line. 3. docs/inbox.md. Two runs of the ownership-and-limits paragraph are rewrapped to its width: the 84-character line, the "while the device" break and the 91-character line after them, then "replies dropped (below)," and the short lines around it. The words are unchanged. Checks: bin/pnpm typecheck; biome check --error-on-warnings on unread.ts; git diff --check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
…ssage-read test The fixture failed any read carrying kind 9, so Inbox unread reads that run first consumed the scripted failures and the page opened without ever failing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
…set pages, and skip the palette search in unreadReads CI on 3bfb504 (PR #1697): the same four journeys failed on every push since the delta reads arrived, while main at the merged base passed. The fixture and the policy relay held any `/query` whose filter has `until` and `#h`, as a timeline or notice page reading older rows. The unread owner's catch-up pages have that shape too: the fixtures' message times all fall within the delta's 1,865 s lookback, so page one is full and page two is asked with `until` and `before_id`. history-loading:93, live:217 and scroll:114 then found it in `app.pending` (live.spec found it twice: the held read passed the deadline and was retried). A history page never carries `since`; the catch-up's pages always do, so a filter with `since` is no longer held. unread-storage:236's `unreadReads` counted the message search the Search Buzz palette sends while `openPage` types "Inbox". On CI it lost all 20 flake-check repeats; locally it passed. It now excludes `search`. Checks: history-loading:93, live:217 and the warm relaunch pass on chromium and webkit; scroll:114 passes under chromium-measurements; membership.spec and unread-storage.spec pass whole on both engines (12); biome clean on the changed files. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
…, not only of what it did not Review thread on 9cce132 (PR #1697, wesbillman), P2. When a full repair's read of the edits and deletions since a stale mark was still full at its last page, `vouch` read by ID only the held content that no read of the pass returned. Windows, the roster sample, the authored page and thread reads ask for content kinds alone and carry no edits, so a message they returned whose newer edit sat below the two 1,000-event overlay pages was skipped. The read by ID came back `read`, the roster sample moved the mark past the edit, and the Inbox kept the original preview with no later delta reaching the edit. The read by ID now covers every held content event in the chunks not read to their end except those received live while the pass ran, whose edits arrive live too. Being returned by a read still proves an event is not deleted, but not that its edits were read. The extra IDs ride the same path: still bounded by the evidence budget, 100 a filter and nine filters a request, halved and resumed as before. This path runs only when the overlay read is full. docs/inbox.md says the read by ID covers the events held in those channels, other than what arrived live, since the other reads return no edits. Tests (unread-restore): - New: a peer's answer in the viewer's thread, returned by team's window and the thread read, with its edit older than 1,000 newer deletions since a stale mark. The read by ID names the root and the answer, the row shows "answer, edited", there is no partial notice and the mark moves. Before this change no read by ID was made ("expected [] to deeply equal ArrayContaining{…}"). - Changed, since the read by ID now also names what the reads returned: - the stale-mark by-ID test asserts its two requests name the held messages, team's window and the roster sample, and not the live one; - the full-window cut test expects every held team and busy event; - the shared-request test asserts the first filter is the 100 held and the second starts with the 101st; - the halving it.each drops `newer` and busy, so the read by ID is still the two messages and its trace is unchanged; the counts are 0/1 and the sample confirms the mark at its own time; - the resumed-read test compares every top-level filter of both passes; - the >128-channel and dropped-window tests add the sample's events. Checks: `bin/pnpm typecheck`; biome check --error-on-warnings on the changed files; `git diff --check`; vitest src/features/relay/unread*, inbox-feed, reader and src/bundled/inbox: 17 files, 635 passed (load ~30). Not run: Playwright (no journey reaches a read by ID), the native host, a live relay. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
…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 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
…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 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
…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 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
5f4836a to
dd306db
Compare
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Star Lord’s automated source follow-up via Wes’s account, integrating independent Rocket, Mantis and Groot coverage.
One P2 recovery finding is detailed inline: retaining a boolean debt does not ensure the next delta retries the failed cold-thread scope. The preceding test-counter finding is addressed in source: tickReads now recognizes both full and delta evidence reads. The rebase’s read-floor and per-channel fold-cache integrations introduce no additional actionable finding in the inspected paths.
Reviewed head dd306db07686b1e82adc01c8065005dac070f3ac, base 8668dbd46a12d01fb5583c1b520093ff96930a6b (merge-base cbc71c85afc89abbb5120a3a584649231c26e91d), against previously reviewed head 5f4836ac903c30d7b8bf2b7feaf06f6a52caa1de.
Source only: no local tests, builds or app execution. CI was still running in the single snapshot; JavaScript 4/4 had passed and Windows validation was skipped. Full-suite counts/timings, native storage, automatic-recovery focus and performance remain unverified. The public description and 12 sampled frames from its synthetic demo were inspected; the video was not reviewed frame by frame. This COMMENT review is not approval or merge authorization.
…in it drops, covered before or not Review thread on dd306db (PR #1697, wesbillman), P2. A full Refresh over a channel the mark covers reads each remembered thread not read through on its own, since the newest reply the viewer wrote or read in it. When that read was dropped, `short` only kept the channel from joining the roster set; one already in it stayed. The record saved complete with the mark held, so the tick it is now owed (dd306db) routed to the delta. A delta reads from the mark less 1,865 s and reads no cold thread of a covered channel, so the older reply was never fetched, while `partial` and `owing` cleared. The pass's tail now takes every `short` channel out of `covered`, after adding the channels whose window and sample landed. The next catch-up then reads that channel once, as it does a joined one: - its window, with the viewer's authored page, beside the delta; - the roster sample; - its cold threads' own reads (`pass.uncovered`); - whole reads of its hot threads, which are no longer settled. The record saves the roster without it, so a relaunch does the same. Only after those reads land is the channel covered again and the debt paid. A dropped thread read asking only for replies from the delta's start on is left out of `short`. That is the read past the mark, whose `since` is the delta's own: a full repair's drop holds the mark, so the next delta reads every content event in those channels from the same point. Retiring coverage there would re-read up to 128 channels for nothing. Passes with no mark keep the old rule. No delta follows them, and the first pass to confirm a mark sets the roster set afresh. The other reads dropped in a full repair over a covered channel: - Hot whole reads: a hot thread not read whole is promoted by the next catch-up while it stays among the 50; one that falls out of the 50 first became a cold thread no catch-up read. A whole read has no `since`, so it now takes its channel out too. - The read past the mark: no gap, as above. The existing tests "a dropped read past the mark ..." and "a tick after a dropped read past the mark ..." fail if it is not left out. - Root fetches: `ids` reads name no channel. A dropped fetch leaves a remembered thread's reply ungrouped, so the record saves incomplete and the next pass runs the full repair and fetches again (e677c00's launch and tick tests). - A catch-up's own drops (a promotion or a lookup-fed cold read in a covered channel): the delta moves the mark there, so a fed cold read from before its start had the same gap. The same rule closes it. docs/inbox.md: a thread read that fails takes its channel out of what the point covers, covered before or not, unless a catch-up reads all it asked for, as for the read of the threads read through to the point. The sentence saying a catch-up only adds channels now says so. Test (unread-restore, it.each tick/launch): "a cold thread's own read dropped in a Refresh over a channel the mark covers takes the channel out of the roster set: ...". - Setup: a complete record caught up a minute ago covering every channel, with 50 hotter threads read whole and through. The viewer's root in team is remembered but not read through, and has a peer's unread answer that only its own read returns: newer posts fill team's window and, with busy's traffic, the roster sample. - The launch reads one delta. Refresh drops the thread's own read after its retry, with `partial` and `owed()` set and the record saved complete. - The next tick (or a relaunch) reads team's window and the thread's own read since T. When the tick first publishes the notice clear, the answer's row (unread 1) is already there, and `owed()` is false. - The tick after that reads the delta alone. Fail-before, on dd306db's unread.ts: both variants fail at team's window ("expected [] to have a length of 1 but got +0"). With the read-shape assertions removed, the tick variant fails where the notice clears without the reply, and the launch variant where the row is missing. Checks: bin/pnpm typecheck; biome check --error-on-warnings on the changed files; git diff --check; vitest src/features/relay/unread*: 9 files, 377 passed. Not run: Playwright, the native host, a live relay. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Star Lord’s automated source follow-up via Wes’s account, with independent Rocket, Mantis and Groot coverage.
No new actionable findings in the fix since dd306db. The prior cold-thread recovery issue is addressed in source: failed reads whose range is not covered by the delta now remove even an already-covered channel from the saved roster, so the next catch-up or relaunch requests its cold threads before clearing the incomplete-activity notice. Reads fully covered by the delta retain coverage.
Reviewed head: c4ab76b. Target base: 8668dbd (merge base cbc71c8). Follow-up scope: the three-file correction, recovery/persistence callers and error/retry focus behavior.
Source-only: regression tests were inspected, not run; no builds, app launches, live relay exercise, CI verification or native accessibility/performance validation. The public description and 12 sampled frames of its unchanged fixture-demo video were inspected; the remaining footage was not reviewed. This comment is not approval or runtime acceptance.
…ants Cut the tests this branch adds to about half, keeping a behavioural test for each user-visible contract and every regression test for a defect found in review or live measurement. No production code and no test that exists on main changes; the fixture and policy-relay changes production request shapes need stay. Added lines against the merge-base with main (cbc71c8), before/after: unread-restore.test.ts 4,692 -> 1,883 unread-repair.test.ts 2,216 -> 1,083 unread-storage.test.ts 658 -> 465 unread-startup.test.ts 418 -> 384 unread-storage.spec.mjs 383 -> 334 unread-fold.test.ts 327 -> 195 unread-lookups.test.ts 239 -> 112 live-session.test.ts 217 -> 214 unread.test.ts 135 -> 125 in scope 9,582 -> 5,092 all test files 10,051 -> 5,561 unread-startup's deletions against the merge-base read 514 -> 522 only because the diff realigns; every test main has is still there. Removed from unread-restore.test.ts: - Mark and delta variants, covered by the kept delta paging, clamp, nothing-newer, six-pages, day-old-mark and late-commit tests and by unread-startup's dropped-chunk regression (dd306db): "an event dated 900 s ahead raises the mark…", "a delta with a chunk that fails…", "a delta that drops keeps the mark though the roster sample lands…", "a pass that could not read the viewer's authored page…", "a pass whose roster sample fails…", "a resume launch whose cached roster is confirmed unchanged…", "a delta page is the relay's whole response…", "a full window's cut is its own limit-th match…", "a delta over a partly confirmed roster…" (near-duplicate of the unconfirmed membership test), "a delta-only pass advances the walk mark…", "a walk beside the delta that drops…" and "a catch-up that ends while the channel list is still being read…" (the walk-cut-short test covers the walk's record). - Thread reads beside the delta, whose hot/cold and walk contracts the kept unread-repair tests and the join test cover: "a channel the mark does not cover has its remembered threads past the 50 hot ones…", "a channel joined while this device was closed…", "a reply of the viewer's in an old thread…", "following a thread…", "unfollowing a thread…", "a remembered thread not read through since the mark…", "a full repair from a mark confirmed more than a day ago…", "a warm Refresh reads the threads read through…", "a full read past the mark is read again in halves…" (unread-startup's two split tests cover oversized responses), both variants of "a read past the mark … still full after eight splits…" (the dropped-read-past-mark test covers the outcome), "a dropped whole read of a thread…" and "a delta-only pass fetches by ID the root…" (the lookup-root and 1,000-roots tests). - Dropped reads and the roster set, beside the kept c4ab76b regression: "a channel the mark does not cover whose cold thread read drops…" (same fix), "a channel whose window dropped stays out of the roster set…", "a channel the delta observed is not observed once restore discards…", "unread messages in a channel whose window dropped beside a busy one's…", and the regression's launch variant (the tick variant stays). - Read-by-ID variants, beside the kept a0614a4 regression and the 16-filter bound: "… not received live read by ID…" (the regression extends its setup), "a held message … has that read halved…", "a held message deleted since the mark is read gone…", "a held unread mention edited 3,000 times…" (the edited-more-than-evidence test), "a read by ID asks 50 IDs a filter at most…", "a read by ID dropped at 200 halves is resumed…", "a read by ID that would need more than 200 halves…", "of the reads by ID sharing a request…" and "on a roster of more than 128 channels holding evidence…". - Restored-evidence discards, beside the kept tampered-event test and the cold-thread forget test (tick variant): "… discarded after a full repair from the mark…", "… discarded after the delta's first page…", "… discarded after the pass routed to the delta…", both variants of "restored evidence forgotten by …, whose roster sample drops…", the forget test's launch variant, and "restored evidence whose read of deletions is dropped…". - Record and root variants, beside the kept walk-cut-short, over-cap, root-fetch-drop, deleted-root and full-repair-roots tests: "a remembered thread's reply saved without its root…", "a remembered thread's root the relay returns nothing for…", "a root that full repair's fetch returns with no channel…", "a remembered thread's root outlasts 1,024 other structure fetches…", "a cold thread's read root, which only a fresh launch's walk lists…", "a repair in which nothing lands…", "a full repair whose reads land before restore is in…", the root-fetch-drop test's launch variant, the over-cap test's bytes variant, the mark clamp's delta variant (its fresh-device variant stays), and "the viewer's root a thread read fetches…". - Lifecycle and save edges beside the kept Refresh, cache-clear and dropped-read tests: "Refresh pressed while a pass asked to catch up…", "a cache clear settles once the record is wiped…", "a pass an access change retires…", "an access change while the launch reads storage…", "a save in flight is not written again…", "a save that is refused is written again…", "leaving a busy channel reads the roster sample once more…", "a tick after a dropped read past the mark…" and "each pass reports whether it read the full repair…" (a metrics pin; client-metrics.test.ts covers the export). - Inside kept tests: the CPU verify-count and overlay-read trace pins, the relaunch loop in the lookup-root test, and helpers left unused. Removed from unread-repair.test.ts: - Read-order pins, covered by the lost-thread and walk tests: "reads the viewer's page with 127 windows first…", "a thread read's full-page split goes ahead…", "a lookup that finds the viewer's reply while the windows read…" and "the Inbox leaves loading once the viewer's page…" (the dropped-first-request test covers loading). - Budget variants, beside the kept busiest-eviction, per-thread slot and quarter-reserve tests: "each thread in a landing is given its newest unread reply…", "a dropped read releases its thread's reserved slot…", "a full page's split rereads count each reply once…" and "an edit or deletion of a pinned message is admitted…". - Cold-thread variants, beside the kept return-until-read, boundary passed-over and boundary-marked-unread tests: "a cold thread whose page comes back full…", "a cold thread's boundary reply that is still unread returns to a warm repair…", "a cold thread's read is inclusive of its newest time…", "a cold thread marked unread by its …", "a cold thread's reply marked unread below its mark…" and "a dropped cold read leaves its thread to the next repair…" (the c4ab76b regression). - Dropped reads, beside dd306db and c4ab76b: "a window dropped after its retry…", "a window that times out once…", "a dropped walk page…" (the walk-cut-short test) and "clearing the owner while a walk page reads…". - Participation edges, beside the kept remembered-participation and walk tests: "remembers threads the viewer took part in, not every message…", "a followed thread is remembered as of the follow…", "a remembered thread in a channel the roster lacks…", "a thread the walk teaches in a channel the roster lacks…", "a remembered thread the viewer's deleted reply joined…", "a read reply outside the viewer's conversations…" and "a lookup feeding a thread whose mark was just lowered…". - "the roster sample keeps a read mention older than its channel's mark…", beside the catch-up-mark and large-roster tests. Removed elsewhere: - unread-storage.test.ts: "a field added after the participation…" (merged into the malformed-record test), "a purge made during a storage's session…" and "a storage whose first read failed saves over a purge…" (the purge-fence test and journey cover it), "a record saved with the mark in its meta…" and "a save of the same meta writes it whole over a meta its storage did not write…" (beside the kept mark-apart and same-meta tests). - unread-lookups.test.ts: three follow-up scheduling edges ("pre-empted by a new repair", "a follow-up read that fails", "lands as the repair's last read"); the kept lookup tests cover the thread read. - unread-fold.test.ts: the per-channel equivalence test; the 2,000-event scale test stays, its spy trimmed to the calls it asserts. - unread-startup.test.ts: "a relay that accepts reads but answers none costs one read per request…" (a count pin) and "a cache clear reports an Inbox record it could not wipe" (restore's cache-clear test). - unread.test.ts and live-session.test.ts: the deleted-edit and tick it.each blocks keep one case each. - unread-storage.spec.mjs: the same-meta-after-refusal block, the second fenced save and the rejoin (the vitest storage tests cover them), and the warm relaunch's minute tick and clock install (live-session's tick test covers what a tick reads). Checked on this tree: bin/pnpm typecheck; biome check on the changed files; git diff --check; vitest for src/features/relay, src/bundled/inbox and src/features/notifications (156 files, 3,245 passed, 4 skipped); Playwright unread-storage, sidebar-unread and notifications on chromium with --no-deps (18 passed). No test timed out on this loaded machine, so none was rerun. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
…nels A thread the viewer replied in, was mentioned in or follows vanished from the Inbox once busier channels' traffic aged it out of the single global evidence read (`#h` over up to 128 channels, limit 500, about 3.6 h of a busy roster). Nothing could recover it: membership lookups were suppressed for read replies, and a read reply whose root had left the sample fell back to a row keyed by its own ID. This commit is the production tree of 4d36397 on missing-messages-in- inbox (PR #1697, frozen at 34fc406), the reviewed and live-probed tip of the first fifteen commits there (34ffd57..4d36397), taken file for file onto main. The commits after it in this series forward-port later fixes; their bodies say what each changes. Repair now reads: - one window per joined channel, since the older of its read mark and catch-up mark (the last week when never read), sized to the roster so a first run fits the budget; - the viewer's authored page, and on a device's first repair a keyset walk of their authored history (up to 6 pages of 500), with the newest authored time kept as the walk mark; - the viewer's remembered threads: the 50 most recently read or written in whole, the rest (up to 1,000 remembered) one filter per root since the newest reply the viewer wrote or read there, passing over that boundary reply by ID; - main's roster sample (newest 500 a 128-channel chunk) behind the windows, so the evidence is a superset of main's. Windows, the authored page and thread reads ride up to 128 filters a request (`isUnreadWindowBatch`), two requests in flight. A request that fails is read once more; a response over the reader's 8 MiB budget or the native host's 16 MiB cap is split. A repair in which nothing lands fails as before; one that drops some reads ends `observed` with `partial: true`, and the Inbox shows "Some recent activity could not be checked." with Refresh. Retained evidence no longer resets when the budget fills: the busiest channel's oldest ordinary evidence is evicted first, the viewer's threads last, and a channel left with nothing is unknown rather than zero. Each asked thread holds one slot of the 1,024-reply participation budget, filled with its newest unread reply first. Lookup witnesses feed the running repair, or a background follow-up after it. Participation is remembered per relay and viewer in IndexedDB (`buzz-thread-participation-v1` here; the next commits rename it). Known at this commit on main: main's fourteen #1860 tests in unread-startup.test.ts fail, and the branch's fixtures predate main's 30-day read floor. Later commits in this series retire and fix them. session.ts (FOUNDATION): two lines wire the participation store into createUnread, approved by Matt Toohey for this project. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
…ants Layer 1's share of 34fc406 on missing-messages-in-inbox, the trim of PR #1697's tests: the same tests are removed from the snapshot's test files by name, and nothing else changes. Kept tests keep the snapshot's bodies, which assert layer 1's behaviour; 34fc406's bodies of the same tests also assert the device record and catch-up the later PRs add. Removed (each covered by a kept test, as 34fc406's body maps them): unread-repair.test.ts: - the roster sample keeps a read mention older than its channel's mark and a mention older than a never-read channel's week - an edit or deletion of a pinned message is admitted once pinned evidence fills the budget - a window that times out once is read again in its slot and the repair completes - a window dropped after its retry leaves its channel unknown and the Inbox partial, with the viewer's thread still read - reads the viewer's page with 127 windows first, remembered and newly taught threads next, then the remaining windows and the roster sample - a thread read's full-page split goes ahead of the remaining windows - a full page's split rereads count each reply once against the participation budget - each thread in a landing is given its newest unread reply before the busiest one takes the rest - a dropped read releases its thread's reserved slot to the reads still to land - remembers threads the viewer took part in, not every message they wrote - a followed thread is remembered as of the follow and refetched - the Inbox leaves loading once the viewer's page and remembered threads land, observing until the windows do - a read reply outside the viewer's conversations fetches only its root, never a membership lookup - a remembered thread the viewer's deleted reply joined leaves the Inbox after restart - a dropped walk page leaves the walk to the next repair without marking channels unchecked - a cold thread's read is inclusive of its newest time, so a peer reply in the same second as the viewer's is recovered - a cold thread's boundary reply that is still unread returns to a warm repair and shows unread, and a new reply in the same second joins it - a cold thread's reply marked unread below its mark moves the mark back to it, and the mark stays there across launches until the mark is cleared - for (const [name, target] of [ - a lookup feeding a thread whose mark was just lowered cannot re-advance it before another thread's mark is saved - a cold thread whose page comes back full is read whole, losing none of its replies - a dropped cold read leaves its thread to the next repair and the Inbox partial, with its channel still observed - a remembered thread in a channel the roster lacks is kept through a save and read again when the channel returns - a thread the walk teaches in a channel the roster lacks is remembered, not read, so the walk mark does not pass it - clearing the owner while a walk page reads retires the walk: no further page, thread read or save - a lookup that finds the viewer's reply while the windows read puts the thread's read ahead of the remaining windows unread-lookups.test.ts: - a lookup that lands as the repair's last read does still gets its thread read, in a follow-up - a follow-up read that fails is dropped without a retry, and the thread is still remembered - a follow-up pre-empted by a new repair leaves the thread it fed to that repair unread-startup.test.ts: - a relay that accepts reads but answers none costs one read per request after the first drop unread-lookups.test.ts also drops its `Priority` and `ReadOptions` import, which only a removed test used. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
…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 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
…nels A thread the viewer replied in, was mentioned in or follows vanished from the Inbox once busier channels' traffic aged it out of the single global evidence read (`#h` over up to 128 channels, limit 500, about 3.6 h of a busy roster). Nothing could recover it: membership lookups were suppressed for read replies, and a read reply whose root had left the sample fell back to a row keyed by its own ID. This commit is the production tree of 4d36397 on missing-messages-in- inbox (PR #1697, frozen at 34fc406), the reviewed and live-probed tip of the first fifteen commits there (34ffd57..4d36397), taken file for file onto main. The commits after it in this series forward-port later fixes; their bodies say what each changes. Repair now reads: - one window per joined channel, since the older of its read mark and catch-up mark (the last week when never read), sized to the roster so a first run fits the budget; - the viewer's authored page, and on a device's first repair a keyset walk of their authored history (up to 6 pages of 500), with the newest authored time kept as the walk mark; - the viewer's remembered threads: the 50 most recently read or written in whole, the rest (up to 1,000 remembered) one filter per root since the newest reply the viewer wrote or read there, passing over that boundary reply by ID; - main's roster sample (newest 500 a 128-channel chunk) behind the windows, so the evidence is a superset of main's. Windows, the authored page and thread reads ride up to 128 filters a request (`isUnreadWindowBatch`), two requests in flight. A request that fails is read once more; a response over the reader's 8 MiB budget or the native host's 16 MiB cap is split. A repair in which nothing lands fails as before; one that drops some reads ends `observed` with `partial: true`, and the Inbox shows "Some recent activity could not be checked." with Refresh. Retained evidence no longer resets when the budget fills: the busiest channel's oldest ordinary evidence is evicted first, the viewer's threads last, and a channel left with nothing is unknown rather than zero. Each asked thread holds one slot of the 1,024-reply participation budget, filled with its newest unread reply first. Lookup witnesses feed the running repair, or a background follow-up after it. Participation is remembered per relay and viewer in IndexedDB (`buzz-thread-participation-v1` here; the next commits rename it). Known at this commit on main: main's fourteen #1860 tests in unread-startup.test.ts fail, and the branch's fixtures predate main's 30-day read floor. Later commits in this series retire and fix them. session.ts (FOUNDATION): two lines wire the participation store into createUnread, approved by Matt Toohey for this project. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
…ants Layer 1's share of 34fc406 on missing-messages-in-inbox, the trim of PR #1697's tests: the same tests are removed from the snapshot's test files by name, and nothing else changes. Kept tests keep the snapshot's bodies, which assert layer 1's behaviour; 34fc406's bodies of the same tests also assert the device record and catch-up the later PRs add. Removed (each covered by a kept test, as 34fc406's body maps them): unread-repair.test.ts: - the roster sample keeps a read mention older than its channel's mark and a mention older than a never-read channel's week - an edit or deletion of a pinned message is admitted once pinned evidence fills the budget - a window that times out once is read again in its slot and the repair completes - a window dropped after its retry leaves its channel unknown and the Inbox partial, with the viewer's thread still read - reads the viewer's page with 127 windows first, remembered and newly taught threads next, then the remaining windows and the roster sample - a thread read's full-page split goes ahead of the remaining windows - a full page's split rereads count each reply once against the participation budget - each thread in a landing is given its newest unread reply before the busiest one takes the rest - a dropped read releases its thread's reserved slot to the reads still to land - remembers threads the viewer took part in, not every message they wrote - a followed thread is remembered as of the follow and refetched - the Inbox leaves loading once the viewer's page and remembered threads land, observing until the windows do - a read reply outside the viewer's conversations fetches only its root, never a membership lookup - a remembered thread the viewer's deleted reply joined leaves the Inbox after restart - a dropped walk page leaves the walk to the next repair without marking channels unchecked - a cold thread's read is inclusive of its newest time, so a peer reply in the same second as the viewer's is recovered - a cold thread's boundary reply that is still unread returns to a warm repair and shows unread, and a new reply in the same second joins it - a cold thread's reply marked unread below its mark moves the mark back to it, and the mark stays there across launches until the mark is cleared - for (const [name, target] of [ - a lookup feeding a thread whose mark was just lowered cannot re-advance it before another thread's mark is saved - a cold thread whose page comes back full is read whole, losing none of its replies - a dropped cold read leaves its thread to the next repair and the Inbox partial, with its channel still observed - a remembered thread in a channel the roster lacks is kept through a save and read again when the channel returns - a thread the walk teaches in a channel the roster lacks is remembered, not read, so the walk mark does not pass it - clearing the owner while a walk page reads retires the walk: no further page, thread read or save - a lookup that finds the viewer's reply while the windows read puts the thread's read ahead of the remaining windows unread-lookups.test.ts: - a lookup that lands as the repair's last read does still gets its thread read, in a follow-up - a follow-up read that fails is dropped without a retry, and the thread is still remembered - a follow-up pre-empted by a new repair leaves the thread it fed to that repair unread-startup.test.ts: - a relay that accepts reads but answers none costs one read per request after the first drop unread-lookups.test.ts also drops its `Priority` and `ReadOptions` import, which only a removed test used. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
…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 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
…nels A thread the viewer replied in, was mentioned in or follows vanished from the Inbox once busier channels' traffic aged it out of the single global evidence read (`#h` over up to 128 channels, limit 500, about 3.6 h of a busy roster). Nothing could recover it: membership lookups were suppressed for read replies, and a read reply whose root had left the sample fell back to a row keyed by its own ID. This commit is the production tree of 4d36397 on missing-messages-in- inbox (PR #1697, frozen at 34fc406), the reviewed and live-probed tip of the first fifteen commits there (34ffd57..4d36397), taken file for file onto main. The commits after it in this series forward-port later fixes; their bodies say what each changes. Repair now reads: - one window per joined channel, since the older of its read mark and catch-up mark (the last week when never read), sized to the roster so a first run fits the budget; - the viewer's authored page, and on a device's first repair a keyset walk of their authored history (up to 6 pages of 500), with the newest authored time kept as the walk mark; - the viewer's remembered threads: the 50 most recently read or written in whole, the rest (up to 1,000 remembered) one filter per root since the newest reply the viewer wrote or read there, passing over that boundary reply by ID; - main's roster sample (newest 500 a 128-channel chunk) behind the windows, so the evidence is a superset of main's. Windows, the authored page and thread reads ride up to 128 filters a request (`isUnreadWindowBatch`), two requests in flight. A request that fails is read once more; a response over the reader's 8 MiB budget or the native host's 16 MiB cap is split. A repair in which nothing lands fails as before; one that drops some reads ends `observed` with `partial: true`, and the Inbox shows "Some recent activity could not be checked." with Refresh. Retained evidence no longer resets when the budget fills: the busiest channel's oldest ordinary evidence is evicted first, the viewer's threads last, and a channel left with nothing is unknown rather than zero. Each asked thread holds one slot of the 1,024-reply participation budget, filled with its newest unread reply first. Lookup witnesses feed the running repair, or a background follow-up after it. Participation is remembered per relay and viewer in IndexedDB (`buzz-thread-participation-v1` here; the next commits rename it). Known at this commit on main: main's fourteen #1860 tests in unread-startup.test.ts fail, and the branch's fixtures predate main's 30-day read floor. Later commits in this series retire and fix them. session.ts (FOUNDATION): two lines wire the participation store into createUnread, approved by Matt Toohey for this project. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
…ants Layer 1's share of 34fc406 on missing-messages-in-inbox, the trim of PR #1697's tests: the same tests are removed from the snapshot's test files by name, and nothing else changes. Kept tests keep the snapshot's bodies, which assert layer 1's behaviour; 34fc406's bodies of the same tests also assert the device record and catch-up the later PRs add. Removed (each covered by a kept test, as 34fc406's body maps them): unread-repair.test.ts: - the roster sample keeps a read mention older than its channel's mark and a mention older than a never-read channel's week - an edit or deletion of a pinned message is admitted once pinned evidence fills the budget - a window that times out once is read again in its slot and the repair completes - a window dropped after its retry leaves its channel unknown and the Inbox partial, with the viewer's thread still read - reads the viewer's page with 127 windows first, remembered and newly taught threads next, then the remaining windows and the roster sample - a thread read's full-page split goes ahead of the remaining windows - a full page's split rereads count each reply once against the participation budget - each thread in a landing is given its newest unread reply before the busiest one takes the rest - a dropped read releases its thread's reserved slot to the reads still to land - remembers threads the viewer took part in, not every message they wrote - a followed thread is remembered as of the follow and refetched - the Inbox leaves loading once the viewer's page and remembered threads land, observing until the windows do - a read reply outside the viewer's conversations fetches only its root, never a membership lookup - a remembered thread the viewer's deleted reply joined leaves the Inbox after restart - a dropped walk page leaves the walk to the next repair without marking channels unchecked - a cold thread's read is inclusive of its newest time, so a peer reply in the same second as the viewer's is recovered - a cold thread's boundary reply that is still unread returns to a warm repair and shows unread, and a new reply in the same second joins it - a cold thread's reply marked unread below its mark moves the mark back to it, and the mark stays there across launches until the mark is cleared - for (const [name, target] of [ - a lookup feeding a thread whose mark was just lowered cannot re-advance it before another thread's mark is saved - a cold thread whose page comes back full is read whole, losing none of its replies - a dropped cold read leaves its thread to the next repair and the Inbox partial, with its channel still observed - a remembered thread in a channel the roster lacks is kept through a save and read again when the channel returns - a thread the walk teaches in a channel the roster lacks is remembered, not read, so the walk mark does not pass it - clearing the owner while a walk page reads retires the walk: no further page, thread read or save - a lookup that finds the viewer's reply while the windows read puts the thread's read ahead of the remaining windows unread-lookups.test.ts: - a lookup that lands as the repair's last read does still gets its thread read, in a follow-up - a follow-up read that fails is dropped without a retry, and the thread is still remembered - a follow-up pre-empted by a new repair leaves the thread it fed to that repair unread-startup.test.ts: - a relay that accepts reads but answers none costs one read per request after the first drop unread-lookups.test.ts also drops its `Priority` and `ReadOptions` import, which only a removed test used. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
…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 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
…ing every repair PR 3 of the split of PR #1697 (plan: note c2f2f5fd, layer 3). This commit is `git checkout 34fc406 -- <the 45 files #1697 changes>` on top of PR 2 (`inbox-2-device-record` at 1c79684). 34fc406 removes no file, and every file PR 1 and PR 2 change is among the 45, so nothing is deleted. Main changed none of the 45 files between #1697's merge-base cbc71c8 and this stack's c3c46c5. Over those files, `git diff 34fc406 HEAD` is empty, so this tree is #1697's, and its review history and checks apply. What this layer adds over PR 2: - route(): a pass whose record is complete and whose mark was confirmed within 24 h reads the delta; otherwise, and on Refresh, the full repair from PR 1 and PR 2. - The delta: one `#h` filter per 128 channels from the mark less 1,865 s, keyset-paged to a short page, falling to the full repair when a chunk is still full after six pages. The mark moves only when every chunk reads to its end, and is held at most 900 s past the device clock. - vouch and the read by ID: the edits and deletions since the prior mark, and, when that read is full, the held content's edits and deletions read by ID, halved down to single events and resumed when dropped. - The roster set (`covered`): the channels the mark covers. A channel outside it (joined, left, dropped) is read once beside the delta; a dropped thread read takes its channel out. - `whole` and `through`: remembered threads read whole, and threads read through to the mark, which a full repair reads past the mark in one multi-root read. - Over-cap catch-up: a selection too large to save keeps `complete`, `whole` and `through` in memory, so the session's ticks still read the delta. - A pass that drops a read stays owed, so the next recovery tick repairs again. - The mark is saved in its own meta entry apart from the record's meta, so a tick that only moves the mark writes ~93 B. - `complete`, deferred from PR 2, and the bookkeeping that serves it: evicted content, the ungrouped-root and lost-walk checks, and absent roots. - ID batches: 50 IDs a filter and 16 filters a request for the pass's reads by ID; 100 IDs a request for mark homes. FOUNDATION: src/features/relay/session.ts gains one line in the createUnread options, `rosterSettled: () => channels.roster().state === "verified"`. It is #1697's line from 8034d96, approved by Matt Toohey, and moves here as part of the split he asked for. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
…nels A thread the viewer replied in, was mentioned in or follows vanished from the Inbox once busier channels' traffic aged it out of the single global evidence read (`#h` over up to 128 channels, limit 500, about 3.6 h of a busy roster). Nothing could recover it: membership lookups were suppressed for read replies, and a read reply whose root had left the sample fell back to a row keyed by its own ID. This commit is the production tree of 4d36397 on missing-messages-in- inbox (PR #1697, frozen at 34fc406), the reviewed and live-probed tip of the first fifteen commits there (34ffd57..4d36397), taken file for file onto main. The commits after it in this series forward-port later fixes; their bodies say what each changes. Repair now reads: - one window per joined channel, since the older of its read mark and catch-up mark (the last week when never read), sized to the roster so a first run fits the budget; - the viewer's authored page, and on a device's first repair a keyset walk of their authored history (up to 6 pages of 500), with the newest authored time kept as the walk mark; - the viewer's remembered threads: the 50 most recently read or written in whole, the rest (up to 1,000 remembered) one filter per root since the newest reply the viewer wrote or read there, passing over that boundary reply by ID; - main's roster sample (newest 500 a 128-channel chunk) behind the windows, so the evidence is a superset of main's. Windows, the authored page and thread reads ride up to 128 filters a request (`isUnreadWindowBatch`), two requests in flight. A request that fails is read once more; a response over the reader's 8 MiB budget or the native host's 16 MiB cap is split. A repair in which nothing lands fails as before; one that drops some reads ends `observed` with `partial: true`, and the Inbox shows "Some recent activity could not be checked." with Refresh. Retained evidence no longer resets when the budget fills: the busiest channel's oldest ordinary evidence is evicted first, the viewer's threads last, and a channel left with nothing is unknown rather than zero. Each asked thread holds one slot of the 1,024-reply participation budget, filled with its newest unread reply first. Lookup witnesses feed the running repair, or a background follow-up after it. Participation is remembered per relay and viewer in IndexedDB (`buzz-thread-participation-v1` here; the next commits rename it). Known at this commit on main: main's fourteen #1860 tests in unread-startup.test.ts fail, and the branch's fixtures predate main's 30-day read floor. Later commits in this series retire and fix them. session.ts (FOUNDATION): two lines wire the participation store into createUnread, approved by Matt Toohey for this project. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
…ants Layer 1's share of 34fc406 on missing-messages-in-inbox, the trim of PR #1697's tests: the same tests are removed from the snapshot's test files by name, and nothing else changes. Kept tests keep the snapshot's bodies, which assert layer 1's behaviour; 34fc406's bodies of the same tests also assert the device record and catch-up the later PRs add. Removed (each covered by a kept test, as 34fc406's body maps them): unread-repair.test.ts: - the roster sample keeps a read mention older than its channel's mark and a mention older than a never-read channel's week - an edit or deletion of a pinned message is admitted once pinned evidence fills the budget - a window that times out once is read again in its slot and the repair completes - a window dropped after its retry leaves its channel unknown and the Inbox partial, with the viewer's thread still read - reads the viewer's page with 127 windows first, remembered and newly taught threads next, then the remaining windows and the roster sample - a thread read's full-page split goes ahead of the remaining windows - a full page's split rereads count each reply once against the participation budget - each thread in a landing is given its newest unread reply before the busiest one takes the rest - a dropped read releases its thread's reserved slot to the reads still to land - remembers threads the viewer took part in, not every message they wrote - a followed thread is remembered as of the follow and refetched - the Inbox leaves loading once the viewer's page and remembered threads land, observing until the windows do - a read reply outside the viewer's conversations fetches only its root, never a membership lookup - a remembered thread the viewer's deleted reply joined leaves the Inbox after restart - a dropped walk page leaves the walk to the next repair without marking channels unchecked - a cold thread's read is inclusive of its newest time, so a peer reply in the same second as the viewer's is recovered - a cold thread's boundary reply that is still unread returns to a warm repair and shows unread, and a new reply in the same second joins it - a cold thread's reply marked unread below its mark moves the mark back to it, and the mark stays there across launches until the mark is cleared - for (const [name, target] of [ - a lookup feeding a thread whose mark was just lowered cannot re-advance it before another thread's mark is saved - a cold thread whose page comes back full is read whole, losing none of its replies - a dropped cold read leaves its thread to the next repair and the Inbox partial, with its channel still observed - a remembered thread in a channel the roster lacks is kept through a save and read again when the channel returns - a thread the walk teaches in a channel the roster lacks is remembered, not read, so the walk mark does not pass it - clearing the owner while a walk page reads retires the walk: no further page, thread read or save - a lookup that finds the viewer's reply while the windows read puts the thread's read ahead of the remaining windows unread-lookups.test.ts: - a lookup that lands as the repair's last read does still gets its thread read, in a follow-up - a follow-up read that fails is dropped without a retry, and the thread is still remembered - a follow-up pre-empted by a new repair leaves the thread it fed to that repair unread-startup.test.ts: - a relay that accepts reads but answers none costs one read per request after the first drop unread-lookups.test.ts also drops its `Priority` and `ReadOptions` import, which only a removed test used. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
…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 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
…ing every repair PR 3 of the split of PR #1697 (plan: note c2f2f5fd, layer 3). This commit is `git checkout 34fc406 -- <the 45 files #1697 changes>` on top of PR 2 (`inbox-2-device-record` at 1c79684). 34fc406 removes no file, and every file PR 1 and PR 2 change is among the 45, so nothing is deleted. Main changed none of the 45 files between #1697's merge-base cbc71c8 and this stack's c3c46c5. Over those files, `git diff 34fc406 HEAD` is empty, so this tree is #1697's, and its review history and checks apply. What this layer adds over PR 2: - route(): a pass whose record is complete and whose mark was confirmed within 24 h reads the delta; otherwise, and on Refresh, the full repair from PR 1 and PR 2. - The delta: one `#h` filter per 128 channels from the mark less 1,865 s, keyset-paged to a short page, falling to the full repair when a chunk is still full after six pages. The mark moves only when every chunk reads to its end, and is held at most 900 s past the device clock. - vouch and the read by ID: the edits and deletions since the prior mark, and, when that read is full, the held content's edits and deletions read by ID, halved down to single events and resumed when dropped. - The roster set (`covered`): the channels the mark covers. A channel outside it (joined, left, dropped) is read once beside the delta; a dropped thread read takes its channel out. - `whole` and `through`: remembered threads read whole, and threads read through to the mark, which a full repair reads past the mark in one multi-root read. - Over-cap catch-up: a selection too large to save keeps `complete`, `whole` and `through` in memory, so the session's ticks still read the delta. - A pass that drops a read stays owed, so the next recovery tick repairs again. - The mark is saved in its own meta entry apart from the record's meta, so a tick that only moves the mark writes ~93 B. - `complete`, deferred from PR 2, and the bookkeeping that serves it: evicted content, the ungrouped-root and lost-walk checks, and absent roots. - ID batches: 50 IDs a filter and 16 filters a request for the pass's reads by ID; 100 IDs a request for mark homes. FOUNDATION: src/features/relay/session.ts gains one line in the createUnread options, `rosterSettled: () => channels.roster().state === "verified"`. It is #1697's line from 8034d96, approved by Matt Toohey, and moves here as part of the split he asked for. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Problem
A thread you had replied in vanished from the Inbox (reported in
#buzz-team). The Inbox was rebuilt on every launch and check from a single read: the newest 500 messages across all your channels. On a busy roster that reaches back only a few hours. Once busier channels pushed a thread out, nothing brought it back, and a read reply whose root had aged out showed up as a loose row of its own.reel-pr.mp4
Main
67e404917is on the left and this branch9cce1329con the right. Both run the web app on a fresh device against one local fixture relay with the same made-up team. Playback is 1×, with long waits cut.What the reel shows
#incident-reviewrow is correct. Main's three rows are the degraded case. The thread's root and your reply are older than its read, and it never looks up the root of a read reply, so each mention stands alone (the#buzz-teamsymptom). Main regroups only once the thread is opened.How it works now
buzz-unread-v1), per relay and viewer. They are re-verified on launch and purged when you leave a community.Details and limits are in
docs/inbox.md.Measured
These are medians of 3 runs against the live relay: read-only, one viewer, during a quiet hour. The branch side is this branch's production code, measured as
a11a08388. Since then the branch has been rebased onto newer main, which brought #2252 (below), and has taken fixes for review findings. None of that was re-measured./queryBoth sides were measured before #2252 (merged 2026-11-03): main as
67e404917, the branch asa11a08388. At that point the minute check repaired unread every minute, so the first row was each minute's read. Those main figures no longer describe main. Since #2252, a healthy minute check on main reads nothing of unread and repairs only while a repair is owed or once 15 minutes have passed since the last repair. The branch keeps that gate. The first row is now the cost of one such repair, as measured then. The idle-CPU and IndexedDB rows come from the same pre-#2252 runs and were not re-measured either.Known limits
For reviewers
src/features/relay/session.tsis FOUNDATION and needs its owner's review. The change (+8/−2) passes the device storage and a roster-settled check tocreateUnread, and makes the cache clear wait for the saved record to be wiped.Browser cases: one file added, none removed.
tests/browser/unread-storage.spec.mjshas two cases, both needing a real browser: lower layers stub IndexedDB, the reload and the broker's minute check.Each failed on the code before its change; the commits give the details.
🤖 Generated with Claude Code