Skip to content

fix(unread): repair unread from the recovery tick only every 15 minutes - #2252

Merged
loganj merged 4 commits into
mainfrom
larry/unread-repair-backstop
Nov 3, 2026
Merged

loganj merged 4 commits into
mainfrom
larry/unread-repair-backstop

Conversation

@loganj

@loganj loganj commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Every connected client ran an unread repair on the 60-second recovery tick, even with a healthy connection and no outage: a read-state refresh plus one relay read per 128 joined channels, every minute.
  • The live stream already carries every unread change, and each connection's first establishment still asks unread to repair once (or to resume, when the membership replay was continuous).
  • The tick's repair is still the only fix for unread changes the live stream loses with no signal to the client. So it stays as a backstop: the tick asks for a repair only when the last one, by the tick or by an establishment, is at least 15 minutes old (UNREAD_BACKSTOP_INTERVAL).
  • Effect: about 15 times fewer unread reads from every connected client. The cost: a silent loss now takes up to 15 minutes to repair instead of up to 1 minute.
  • src/features/relay/session.ts is marked FOUNDATION. Logan asked for this change directly.

Details

  • session.ts: repairUnread() calls unread.reconnect() and, for a full repair, records when it was asked. The membership route's covered-read step (from fix(relay): skip status, emoji, read-state and reminder re-reads a continuous replay covers #2204) calls it with the replay's continuity, so each connection's full repair resets the clock; a covered resume reads no read-state snapshot and leaves the clock alone. recover() repairs at once while unread.owed(), and otherwise only when UNREAD_BACKSTOP_INTERVAL has passed. This matches the roster backstop from fix(relay): read the full channel list from the recovery tick only every 15 minutes #2215.
  • unread.ts: owed() reports a repair still owed. The unread owner tracks its own debt: a failed repair, an evidence overflow or a drop sets it, and only a completed repair clears it, so a live message that marks the evidence observed again does not clear it.
  • read-state.ts: owed() reports a snapshot read still owed: a failed snapshot read, a failed marker apply or a drop sets it, and a successful snapshot read or resume clears it. unread.owed() includes it, so a backstop whose snapshot read failed retries on the next tick even when its evidence read succeeded.
  • docs/relay-queries.md: the recovery-timer paragraph describes the unread backstop next to the roster backstop.

Silent losses this backstop repairs

A relay review (block/buzz 009dc103f) found these ways a connected client can lose an unread-relevant event (kinds 9, 40002, 40008, and read state 30078) with no signal. For channels that are not open in a window, the tick repair is the only fix today. With this change each one takes up to 15 minutes to heal:

  1. Send buffer full. Fan-out uses try_send and drops the frame. The relay cancels the connection only after 15 consecutive full sends, so short bursts of drops are silent.
  2. Cross-pod delivery through Redis pub/sub is at most once: a lagging broadcast receiver, a Redis subscriber reconnect, or a failed publish from the writing pod drops the event for other pods. Read state from another device travels the same way.
  3. A failed fan-out access check drops the event for that recipient. Only syncs are told, not live REQs.
  4. A channel removed from a batch subscription with no CLOSED (revoke followed by a regrant whose notice was lost).
  5. Replica read budget (BUZZ_REPLICA_READ_MAX_AGE_MS, if set in production): a replay or repair read can miss the newest events.

A relay-side signal for these losses (a NOTICE or CLOSED to the affected live REQs, as syncs already get) would let the client repair at once. That is relay work and is not in this PR.

Review

  • Auto reviewed the relay side and found no blockers (the list above).
  • Cerveau accepted the client-state review and did not report. This PR does not wait on it.

Testing

  • unread-startup.test.ts: after a connection's own repair, 14 healthy ticks make no unread read; the tick at 15 minutes repairs. A failed repair is retried on the next tick, also after a live message marks the evidence observed. A failed backstop snapshot read with a successful evidence read is retried on the next tick. An evidence overflow is repaired on the next tick, and a covered reconnect after a drop leaves the next tick with no read. read-state.test.ts: a live marker that fails to apply, or a drop, owes a snapshot read until a snapshot read or a covered resume succeeds. A covered reconnect at 10 minutes does not postpone the full repair at 15. Each test fails without its rule.
  • Full vitest (873 files, 15,420 tests), tsc and biome pass on the PR head's tree (19d0d4634). The pre-push hook passed.

🤖 Generated with Claude Code

The 60 s live recovery tick asked unread to repair on every tick: a
read-state refresh and one read per 128 joined channels, every minute,
on a healthy connection. The live stream already carries every unread
change, and each connection's first establishment repairs unread once.

The tick's repair is still the only one for changes the live stream
loses with no signal: a channel the relay silently pruned from a
multi-channel batch, or a dropped fan-out
frame. So it stays, as a backstop: the tick asks for a repair only
when the last one (by the tick or by an establishment) is 15 minutes
old. Those losses now take up to 15 minutes to repair instead of one.

Signed-off-by: Logan Johnson <[email protected]>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
@loganj
loganj marked this pull request as ready for review November 3, 2026 13:17
@loganj
loganj requested a review from a team as a code owner November 3, 2026 13:18
kalvinnchau

This comment was marked as resolved.

…ff the backstop clock

The recovery tick repairs unread on the next tick while a repair failed or
the stream dropped with none completed since. A covered resume reads no
read-state snapshot, so it no longer restarts the 15-minute interval.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Logan Johnson <[email protected]>
kalvinnchau

This comment was marked as resolved.

…ness

owed() read the evidence freshness, so one live message after a failed
repair made it false while other channels stayed unrepaired, and a failed
read-state snapshot left it false once the evidence read succeeded. The
unread owner now tracks repair debt itself (a failed repair, an overflow
or a drop, paid only by a completed repair), and the read-state owner
reports a snapshot read still owed (a failed read or marker, or a drop,
paid by a successful read or resume).

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Logan Johnson <[email protected]>

@TheSentinel454 TheSentinel454 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 LGTM at 827e56e. The backstop interval, owed-retry, and covered-resume handling look correct, and the new regressions cover each case Kalvin raised.

kalvinnchau

This comment was marked as resolved.

Adds regressions for an evidence overflow (the next tick repairs), a live
marker that fails to apply and a drop (a snapshot read is owed until a
read or covered resume), and a covered reconnect (the next tick reads
nothing). The docs now state that evidence debt and marker debt each
clear only their own way.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Logan Johnson <[email protected]>
@loganj
loganj merged commit 45caf7f into main Nov 3, 2026
41 checks passed
@loganj
loganj deleted the larry/unread-repair-backstop branch November 3, 2026 16:16
tlongwell-block added a commit that referenced this pull request Nov 3, 2026
Resolves read-state.ts against #2252 (observationOwed): both sides kept;
the snapshot capacity basis and the owed-observation flag are independent.

Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Signed-off-by: Tyler Longwell <[email protected]>
dr0uleau added a commit that referenced this pull request Nov 3, 2026
…xecution-support-for-native-buzz-33ea2bea

* origin/main: (74 commits)
  feat(host): harden byte lifecycle and add protected image upload (#2199)
  Updated onboarding (#2183)
  perf(relay): read each signed roster once in large channels (#2244)
  AIDA-2346: Keep spoilers hidden in reminder previews (#2261)
  fix: AIDA-2371 Keep sidebar selection pill aligned during resize (#2182)
  fix(relay): skip background rereads of profiles a read found absent (#2147)
  fix: prevent startup logo selection (#1823)
  fix(agents): send sign-in evidence with agent Git credentials (#2240)
  perf(mentions): stop scanning every member on each mention keystroke (#2246)
  fix(attachments): send image dimensions so previews don't shift (#2262)
  feat(shortcuts): add Slack-style hotkeys (#2170)
  perf(plugins): share host request clients across calls (#2243)
  fix(identity): keep federated sessions and agent grants in one keychain item (#2225)
  fix(profiles): evict the least recently requested profile (#2255)
  fix(unread): repair unread from the recovery tick only every 15 minutes (#2252)
  AIDA-2374: Use readable labels in chat titles (#2185)
  perf(profiles): keep verified names checked through a planned renewal (#2234)
  feat(plugins): let a plugin ask Buzz to review an install (#2059)
  fix(identity): sign FI builds in at first run through the native session (FOUNDATION: App.tsx, services.ts) (#2249)
  Place profile verification beside the verified name (#2224)
  ...

Signed-off-by: Daniel Rouleau <[email protected]>
dr0uleau added a commit that referenced this pull request Nov 3, 2026
* commit 'c8e882165': (50 commits)
  Scope execution targets by harness session
  Read durable execution state before admission
  feat(host): harden byte lifecycle and add protected image upload (#2199)
  Updated onboarding (#2183)
  perf(relay): read each signed roster once in large channels (#2244)
  AIDA-2346: Keep spoilers hidden in reminder previews (#2261)
  fix: AIDA-2371 Keep sidebar selection pill aligned during resize (#2182)
  fix(relay): skip background rereads of profiles a read found absent (#2147)
  fix: prevent startup logo selection (#1823)
  fix(agents): send sign-in evidence with agent Git credentials (#2240)
  perf(mentions): stop scanning every member on each mention keystroke (#2246)
  fix(attachments): send image dimensions so previews don't shift (#2262)
  feat(shortcuts): add Slack-style hotkeys (#2170)
  perf(plugins): share host request clients across calls (#2243)
  fix(identity): keep federated sessions and agent grants in one keychain item (#2225)
  fix(profiles): evict the least recently requested profile (#2255)
  fix(unread): repair unread from the recovery tick only every 15 minutes (#2252)
  AIDA-2374: Use readable labels in chat titles (#2185)
  perf(profiles): keep verified names checked through a planned renewal (#2234)
  feat(plugins): let a plugin ask Buzz to review an install (#2059)
  ...

Signed-off-by: Daniel Rouleau <[email protected]>
Co-authored-by: Codex <[email protected]>
matt2e added a commit that referenced this pull request Nov 4, 2026
…re the minute tick

Review of 3f417fa (PR #1697, wesbillman): the warm-relaunch journey
timed out on hosted Chromium 7/9 waiting 75 s for the broker's minute
tick, with no unread reads.

Since the rebase onto main brought #2252, the session's `recover()`
repairs unread only when unread is owed or UNREAD_BACKSTOP_INTERVAL
(15 minutes) has passed since the last repair. After the healthy
relaunch nothing is owed and the relaunch has just repaired, so the tick
reads nothing. The tick itself comes from the broker's subscriber in
Node on a real 60 s chain, so a page clock cannot speed it up.

The test now installs the renderer's Playwright clock before the first
launch, flowing in real time, and moves its time 15 minutes forward
before waiting for the tick. It moves the time back to real time before
the next reload. Installing the clock in a running page would replace
timers the app already set and briefly read the time as 0, so it is
installed up front. The one-delta assertion is unchanged; the
production interval is unchanged.

Checks: `bin/pnpm test:browser --project=chromium --project=webkit
--no-deps tests/browser/unread-storage.spec.mjs`: 4 passed (both cases on
each engine; the warm relaunch case 1.1 m on each), at load ~60. With the
time jump removed (install kept), chromium fails at the same poll with 0
reads after 75 s, as on CI. Biome clean.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
matt2e added a commit that referenced this pull request Nov 4, 2026
…re the minute tick

Review of 3f417fa (PR #1697, wesbillman): the warm-relaunch journey
timed out on hosted Chromium 7/9 waiting 75 s for the broker's minute
tick, with no unread reads.

Since the rebase onto main brought #2252, the session's `recover()`
repairs unread only when unread is owed or UNREAD_BACKSTOP_INTERVAL
(15 minutes) has passed since the last repair. After the healthy
relaunch nothing is owed and the relaunch has just repaired, so the tick
reads nothing. The tick itself comes from the broker's subscriber in
Node on a real 60 s chain, so a page clock cannot speed it up.

The test now installs the renderer's Playwright clock before the first
launch, flowing in real time, and moves its time 15 minutes forward
before waiting for the tick. It moves the time back to real time before
the next reload. Installing the clock in a running page would replace
timers the app already set and briefly read the time as 0, so it is
installed up front. The one-delta assertion is unchanged; the
production interval is unchanged.

Checks: `bin/pnpm test:browser --project=chromium --project=webkit
--no-deps tests/browser/unread-storage.spec.mjs`: 4 passed (both cases on
each engine; the warm relaunch case 1.1 m on each), at load ~60. With the
time jump removed (install kept), chromium fails at the same poll with 0
reads after 75 s, as on CI. Biome clean.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
matt2e added a commit that referenced this pull request Nov 4, 2026
…unt the delta as a tick's read, and reuse the cached fold

The rebase onto main cbc71c8 brings #1286 (a 30-day read floor) and
#2366 (folded rows cached per channel), and leaves two #2252 tests
counting reads the delta does not make.

- Read floor. `createReadState` now reads everything older than 30 days
  before the device clock as read. The branch-only setups in
  unread-restore, unread-repair and unread-fold, and live-session's
  `caughtUp`, date their fixtures in 1970 and 2023, so 56 of their
  tests saw nothing unread. They take `floor: () => 0` (or
  `readStateFloor`), as main's setups do.
- #2252's recovery tests. "a live message after a failed unread repair
  does not cancel its retry" and "a covered reconnect pays the drop's
  debt" counted a tick's evidence read as an `include_aux` filter with
  kind 40002. A catch-up reads the delta, which carries no `include_aux`,
  so neither saw the tick's read. Both failed on hosted CI at 3f417fa
  (run 41733593360, JavaScript 4/4 and both Vitest flake checks).
  `tickReads` now counts a channel read (`#h`, no `#e` or `ids`, kind
  40002), full or delta, and the first test uses it.
- Fold cache. With #2366 an owner reuses a channel's folded rows until
  the evidence index rebuilds, so the 2,000-event fold test's
  `activity("c7")` after `inbox()` folded nothing. It now asks c7's
  activity first, which folds c7's own 23 events, and `inbox()` then
  folds the other 79 channels and reuses c7's.

The rebase resolved `afterFrontier` as the branch's `catchUp` term plus
main's `reads.floor()` in the `Math.max`, and main's `folded()` cache to
fold `foldable(channelId)`, the channel's own evidence and its overlays.

Checks: bin/pnpm typecheck; biome check --error-on-warnings on the
changed files; vitest src/features/relay, src/bundled/inbox and
src/features/notifications: 157 files, 3,348 passed, 4 skipped.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
matt2e added a commit that referenced this pull request Nov 4, 2026
…next recovery tick reads it again

Review thread on 5f4836a (PR #1697, kalvinnchau), P3.

Since #2252 the session's minute recovery tick repairs unread only while
a repair is owed or UNREAD_BACKSTOP_INTERVAL (15 minutes) has passed
since the last full repair. A pass whose read was dropped after its retry
(a delta chunk, a window, a thread read, a root fetch, the read by ID)
ended with `partial = unchecked` but `owing = false`, so every later tick
skipped unread. The reviewer's 129-channel probe, with the second delta
chunk failing and the relay healthy after, kept the partial notice and
unknown coverage through five minute ticks, with no reads, until the
backstop. A pass in which nothing landed throws and stays owed, and was
read again on the next tick. docs/inbox.md promised "the next check"
in one place and "no automatic retry" in another.

The pass's tail now sets `owing = unchecked`. A pass that drops a read
stays owed, as one that throws does, and the next tick repairs. What each
dropped read leaves behind is already the next pass's to read: a dropped
delta keeps the mark, a dropped window or thread read leaves its channel
outside the roster set, and a dropped read by ID resumes. A pass with
every read landed pays the debt, and the tick after it waits for the
backstop again. So a relay that keeps dropping one read costs one repair
a minute, the same as a relay that keeps failing whole repairs, never
more. `session.ts` (FOUNDATION) is unchanged: its gate reads
`unread.owed()`, which reads `owing`.

Docs: docs/inbox.md now says the repair stays owed after a dropped read,
so the next periodic check, a minute later, repairs again, as after a
repair that fails. The passages that already said "the next check"
stand. docs/relay-queries.md counts a repair that dropped a read as
evidence debt, cleared only by one with every read landed. The
unread-repair comment on an owner that does not retry on its own says
the session's tick does.

Test (unread-startup, through the session's real recovery gate): "a pass
that drops a delta chunk leaves unread owed: the next recovery tick reads
it again, not the 15-minute backstop, and the tick after that reads
nothing". 129 channels, so the delta is two chunks. A full reconnect
pays the marker debt. A covered reconnect then resumes the markers, and
its one-channel chunk fails twice and is dropped (`partial: true`). A
minute later, with the relay answering, the tick reads that chunk again
and `partial` clears. A minute after that, the tick reads no evidence;
the barrier is `retrySync()`, which settles after any read-state refresh
the tick began, then `ensure()`, which waits for the repair chained to
it.
- On the previous unread.ts the tick a minute later reads nothing:
  "expected 3 to be 4" at the chunk's count.
- With `owing = true` at the tail instead (always owed), the last tick
  repairs: "expected 12 to be 10" at its evidence count.
The live-session `caughtUp` harness was tried first and is not used:
its read-state snapshot reads after the first fail, so marker debt never
clears there and every tick repairs either way.

Checks: bin/pnpm typecheck; biome check --error-on-warnings on the
changed files; git diff --check; vitest src/features/relay,
src/bundled/inbox and src/features/notifications: 157 files, 3,349
passed, 4 skipped. Not run: Playwright, the native host, a live relay.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
arividar pushed a commit to minniai/minni-buzz-app that referenced this pull request Nov 4, 2026
…es (block#2252)

## Summary
- Every connected client ran an unread repair on the 60-second recovery
tick, even with a healthy connection and no outage: a read-state refresh
plus one relay read per 128 joined channels, every minute.
- The live stream already carries every unread change, and each
connection's first establishment still asks unread to repair once (or to
resume, when the membership replay was continuous).
- The tick's repair is still the only fix for unread changes the live
stream loses with no signal to the client. So it stays as a backstop:
the tick asks for a repair only when the last one, by the tick or by an
establishment, is at least 15 minutes old (`UNREAD_BACKSTOP_INTERVAL`).
- Effect: about 15 times fewer unread reads from every connected client.
The cost: a silent loss now takes up to 15 minutes to repair instead of
up to 1 minute.
- `src/features/relay/session.ts` is marked FOUNDATION. Logan asked for
this change directly.

## Details
- `session.ts`: `repairUnread()` calls `unread.reconnect()` and, for a
full repair, records when it was asked. The membership route's
covered-read step (from block#2204)
calls it with the replay's continuity, so each connection's full repair
resets the clock; a covered resume reads no read-state snapshot and
leaves the clock alone. `recover()` repairs at once while
`unread.owed()`, and otherwise only when `UNREAD_BACKSTOP_INTERVAL` has
passed. This matches the roster backstop from
block#2215.
- `unread.ts`: `owed()` reports a repair still owed. The unread owner
tracks its own debt: a failed repair, an evidence overflow or a drop
sets it, and only a completed repair clears it, so a live message that
marks the evidence observed again does not clear it.
- `read-state.ts`: `owed()` reports a snapshot read still owed: a failed
snapshot read, a failed marker apply or a drop sets it, and a successful
snapshot read or resume clears it. `unread.owed()` includes it, so a
backstop whose snapshot read failed retries on the next tick even when
its evidence read succeeded.
- `docs/relay-queries.md`: the recovery-timer paragraph describes the
unread backstop next to the roster backstop.

## Silent losses this backstop repairs
A relay review (block/buzz `009dc103f`) found these ways a connected
client can lose an unread-relevant event (kinds 9, 40002, 40008, and
read state 30078) with no signal. For channels that are not open in a
window, the tick repair is the only fix today. With this change each one
takes up to 15 minutes to heal:
1. **Send buffer full.** Fan-out uses `try_send` and drops the frame.
The relay cancels the connection only after 15 consecutive full sends,
so short bursts of drops are silent.
2. **Cross-pod delivery through Redis pub/sub** is at most once: a
lagging broadcast receiver, a Redis subscriber reconnect, or a failed
publish from the writing pod drops the event for other pods. Read state
from another device travels the same way.
3. **A failed fan-out access check** drops the event for that recipient.
Only syncs are told, not live REQs.
4. **A channel removed from a batch subscription** with no CLOSED
(revoke followed by a regrant whose notice was lost).
5. **Replica read budget** (`BUZZ_REPLICA_READ_MAX_AGE_MS`, if set in
production): a replay or repair read can miss the newest events.

A relay-side signal for these losses (a NOTICE or CLOSED to the affected
live REQs, as syncs already get) would let the client repair at once.
That is relay work and is not in this PR.

## Review
- Auto reviewed the relay side and found no blockers (the list above).
- Cerveau accepted the client-state review and did not report. This PR
does not wait on it.

## Testing
- `unread-startup.test.ts`: after a connection's own repair, 14 healthy
ticks make no unread read; the tick at 15 minutes repairs. A failed
repair is retried on the next tick, also after a live message marks the
evidence observed. A failed backstop snapshot read with a successful
evidence read is retried on the next tick. An evidence overflow is
repaired on the next tick, and a covered reconnect after a drop leaves
the next tick with no read. `read-state.test.ts`: a live marker that
fails to apply, or a drop, owes a snapshot read until a snapshot read or
a covered resume succeeds. A covered reconnect at 10 minutes does not
postpone the full repair at 15. Each test fails without its rule.
- Full vitest (873 files, 15,420 tests), tsc and biome pass on the PR
head's tree (`19d0d4634`). The pre-push hook passed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Logan Johnson <[email protected]>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <[email protected]>
matt2e added a commit that referenced this pull request Nov 4, 2026
…next recovery tick reads it again

Forward-port 2 of the split (#note:c2f2f5fd): the layer-1 part of
dd306db on missing-messages-in-inbox (PR #1697 review, kalvinnchau,
P3).

Since #2252 the session's minute recovery tick repairs unread only while
a repair is owed or UNREAD_BACKSTOP_INTERVAL (15 minutes) has passed
since the last one. A pass that dropped a read after its retry (a window
request, a thread read, a root fetch) ended with `partial` set but
`owing` false, so every later tick skipped unread and the partial notice
and unknown channels stayed until the backstop. A pass in which nothing
landed throws and stays owed.

The repair's tail now sets `owing = unchecked`. A pass that drops a read
stays owed, as one that fails does, and the next tick repairs; a pass
with every read landed pays the debt. A relay that keeps dropping one
read costs one repair a minute, as one that keeps failing whole repairs
does. session.ts (FOUNDATION) is unchanged: its gate reads
`unread.owed()`.

Docs: docs/inbox.md says the repair stays owed after a dropped read, so
the next periodic check, a minute later, repairs again, where it said
"no automatic retry". docs/relay-queries.md counts a repair that dropped
a read as evidence debt, cleared only by one with every read landed. Its
"an evidence overflow" goes from the same sentence: this series' evidence
never resets on overflow, it evicts.

Test (unread-startup, through the session's real recovery gate): "a pass
that drops a window chunk leaves unread owed: ...". 129 channels, so the
windows are two requests: the viewer's page with 127 windows, then two
windows. A full reconnect pays the marker debt; a covered reconnect then
drops the second window request after its retry (`partial: true`). A
minute later, with the relay answering, the tick reads it again and
`partial` clears; a minute after that, the tick reads no evidence.
- With `owing = false` at the tail, as before: "expected 4 to be 5" at
  the tick a minute later.
- With the tail always owed: "expected 21 to be 17" at the last tick.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
matt2e added a commit that referenced this pull request Nov 4, 2026
…t a repair's channel read, and fail only the timeline's page read

Forward-port 6 of the layer-1 split: the test fixes main's later work
needs, from 367b38c and 11f22a6.

- Read floor (#1286). `createReadState` reads everything older than 30
  days before the device clock as read, and the repair fixtures date in
  1970, so 10 unread-repair tests saw nothing unread (and the two
  `keepRoot` tests passed without checking anything). Their setup takes
  `floor: () => 0`, as main's setups do. unread-fold's floor and #2366's
  fold-cache reorder came with forward-port 1, whose fold test needed
  them.
- #2252's recovery tests counted a repair as one evidence read: a kind-9
  request, or an `include_aux` filter with kind 40002 first. A layer-1
  repair sends the authored page and windows, then the roster sample,
  so each counted two. `tickReads` counts a channel read (`#h`, no `#e`
  or `ids`, kind 40002) leading a request, which is the sample once a
  repair, and the four tests count with it.
- entity-navigation's failed-message-read test failed any kind-9 read,
  so the Inbox's reads, which run first, used up its scripted failures.
  It fails only the timeline's `top_level` page read (11f22a6).

The new "a pass that drops a window chunk leaves unread owed" test still
fails with `owing = false` (4 reads, expected 5) and with unread always
owed (16, expected 13) under the new count.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
matt2e added a commit that referenced this pull request Nov 5, 2026
…next recovery tick reads it again

Forward-port 2 of the split (#note:c2f2f5fd): the layer-1 part of
dd306db on missing-messages-in-inbox (PR #1697 review, kalvinnchau,
P3).

Since #2252 the session's minute recovery tick repairs unread only while
a repair is owed or UNREAD_BACKSTOP_INTERVAL (15 minutes) has passed
since the last one. A pass that dropped a read after its retry (a window
request, a thread read, a root fetch) ended with `partial` set but
`owing` false, so every later tick skipped unread and the partial notice
and unknown channels stayed until the backstop. A pass in which nothing
landed throws and stays owed.

The repair's tail now sets `owing = unchecked`. A pass that drops a read
stays owed, as one that fails does, and the next tick repairs; a pass
with every read landed pays the debt. A relay that keeps dropping one
read costs one repair a minute, as one that keeps failing whole repairs
does. session.ts (FOUNDATION) is unchanged: its gate reads
`unread.owed()`.

Docs: docs/inbox.md says the repair stays owed after a dropped read, so
the next periodic check, a minute later, repairs again, where it said
"no automatic retry". docs/relay-queries.md counts a repair that dropped
a read as evidence debt, cleared only by one with every read landed. Its
"an evidence overflow" goes from the same sentence: this series' evidence
never resets on overflow, it evicts.

Test (unread-startup, through the session's real recovery gate): "a pass
that drops a window chunk leaves unread owed: ...". 129 channels, so the
windows are two requests: the viewer's page with 127 windows, then two
windows. A full reconnect pays the marker debt; a covered reconnect then
drops the second window request after its retry (`partial: true`). A
minute later, with the relay answering, the tick reads it again and
`partial` clears; a minute after that, the tick reads no evidence.
- With `owing = false` at the tail, as before: "expected 4 to be 5" at
  the tick a minute later.
- With the tail always owed: "expected 21 to be 17" at the last tick.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
matt2e added a commit that referenced this pull request Nov 5, 2026
…t a repair's channel read, and fail only the timeline's page read

Forward-port 6 of the layer-1 split: the test fixes main's later work
needs, from 367b38c and 11f22a6.

- Read floor (#1286). `createReadState` reads everything older than 30
  days before the device clock as read, and the repair fixtures date in
  1970, so 10 unread-repair tests saw nothing unread (and the two
  `keepRoot` tests passed without checking anything). Their setup takes
  `floor: () => 0`, as main's setups do. unread-fold's floor and #2366's
  fold-cache reorder came with forward-port 1, whose fold test needed
  them.
- #2252's recovery tests counted a repair as one evidence read: a kind-9
  request, or an `include_aux` filter with kind 40002 first. A layer-1
  repair sends the authored page and windows, then the roster sample,
  so each counted two. `tickReads` counts a channel read (`#h`, no `#e`
  or `ids`, kind 40002) leading a request, which is the sample once a
  repair, and the four tests count with it.
- entity-navigation's failed-message-read test failed any kind-9 read,
  so the Inbox's reads, which run first, used up its scripted failures.
  It fails only the timeline's `top_level` page read (11f22a6).

The new "a pass that drops a window chunk leaves unread owed" test still
fails with `owing = false` (4 reads, expected 5) and with unread always
owed (16, expected 13) under the new count.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
matt2e added a commit that referenced this pull request Nov 5, 2026
…next recovery tick reads it again

Forward-port 2 of the split (#note:c2f2f5fd): the layer-1 part of
dd306db on missing-messages-in-inbox (PR #1697 review, kalvinnchau,
P3).

Since #2252 the session's minute recovery tick repairs unread only while
a repair is owed or UNREAD_BACKSTOP_INTERVAL (15 minutes) has passed
since the last one. A pass that dropped a read after its retry (a window
request, a thread read, a root fetch) ended with `partial` set but
`owing` false, so every later tick skipped unread and the partial notice
and unknown channels stayed until the backstop. A pass in which nothing
landed throws and stays owed.

The repair's tail now sets `owing = unchecked`. A pass that drops a read
stays owed, as one that fails does, and the next tick repairs; a pass
with every read landed pays the debt. A relay that keeps dropping one
read costs one repair a minute, as one that keeps failing whole repairs
does. session.ts (FOUNDATION) is unchanged: its gate reads
`unread.owed()`.

Docs: docs/inbox.md says the repair stays owed after a dropped read, so
the next periodic check, a minute later, repairs again, where it said
"no automatic retry". docs/relay-queries.md counts a repair that dropped
a read as evidence debt, cleared only by one with every read landed. Its
"an evidence overflow" goes from the same sentence: this series' evidence
never resets on overflow, it evicts.

Test (unread-startup, through the session's real recovery gate): "a pass
that drops a window chunk leaves unread owed: ...". 129 channels, so the
windows are two requests: the viewer's page with 127 windows, then two
windows. A full reconnect pays the marker debt; a covered reconnect then
drops the second window request after its retry (`partial: true`). A
minute later, with the relay answering, the tick reads it again and
`partial` clears; a minute after that, the tick reads no evidence.
- With `owing = false` at the tail, as before: "expected 4 to be 5" at
  the tick a minute later.
- With the tail always owed: "expected 21 to be 17" at the last tick.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
matt2e added a commit that referenced this pull request Nov 5, 2026
…t a repair's channel read, and fail only the timeline's page read

Forward-port 6 of the layer-1 split: the test fixes main's later work
needs, from 367b38c and 11f22a6.

- Read floor (#1286). `createReadState` reads everything older than 30
  days before the device clock as read, and the repair fixtures date in
  1970, so 10 unread-repair tests saw nothing unread (and the two
  `keepRoot` tests passed without checking anything). Their setup takes
  `floor: () => 0`, as main's setups do. unread-fold's floor and #2366's
  fold-cache reorder came with forward-port 1, whose fold test needed
  them.
- #2252's recovery tests counted a repair as one evidence read: a kind-9
  request, or an `include_aux` filter with kind 40002 first. A layer-1
  repair sends the authored page and windows, then the roster sample,
  so each counted two. `tickReads` counts a channel read (`#h`, no `#e`
  or `ids`, kind 40002) leading a request, which is the sample once a
  repair, and the four tests count with it.
- entity-navigation's failed-message-read test failed any kind-9 read,
  so the Inbox's reads, which run first, used up its scripted failures.
  It fails only the timeline's `top_level` page read (11f22a6).

The new "a pass that drops a window chunk leaves unread owed" test still
fails with `owing = false` (4 reads, expected 5) and with unread always
owed (16, expected 13) under the new count.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
matt2e added a commit that referenced this pull request Nov 5, 2026
…next recovery tick reads it again

Forward-port 2 of the split (#note:c2f2f5fd): the layer-1 part of
dd306db on missing-messages-in-inbox (PR #1697 review, kalvinnchau,
P3).

Since #2252 the session's minute recovery tick repairs unread only while
a repair is owed or UNREAD_BACKSTOP_INTERVAL (15 minutes) has passed
since the last one. A pass that dropped a read after its retry (a window
request, a thread read, a root fetch) ended with `partial` set but
`owing` false, so every later tick skipped unread and the partial notice
and unknown channels stayed until the backstop. A pass in which nothing
landed throws and stays owed.

The repair's tail now sets `owing = unchecked`. A pass that drops a read
stays owed, as one that fails does, and the next tick repairs; a pass
with every read landed pays the debt. A relay that keeps dropping one
read costs one repair a minute, as one that keeps failing whole repairs
does. session.ts (FOUNDATION) is unchanged: its gate reads
`unread.owed()`.

Docs: docs/inbox.md says the repair stays owed after a dropped read, so
the next periodic check, a minute later, repairs again, where it said
"no automatic retry". docs/relay-queries.md counts a repair that dropped
a read as evidence debt, cleared only by one with every read landed. Its
"an evidence overflow" goes from the same sentence: this series' evidence
never resets on overflow, it evicts.

Test (unread-startup, through the session's real recovery gate): "a pass
that drops a window chunk leaves unread owed: ...". 129 channels, so the
windows are two requests: the viewer's page with 127 windows, then two
windows. A full reconnect pays the marker debt; a covered reconnect then
drops the second window request after its retry (`partial: true`). A
minute later, with the relay answering, the tick reads it again and
`partial` clears; a minute after that, the tick reads no evidence.
- With `owing = false` at the tail, as before: "expected 4 to be 5" at
  the tick a minute later.
- With the tail always owed: "expected 21 to be 17" at the last tick.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
matt2e added a commit that referenced this pull request Nov 5, 2026
…t a repair's channel read, and fail only the timeline's page read

Forward-port 6 of the layer-1 split: the test fixes main's later work
needs, from 367b38c and 11f22a6.

- Read floor (#1286). `createReadState` reads everything older than 30
  days before the device clock as read, and the repair fixtures date in
  1970, so 10 unread-repair tests saw nothing unread (and the two
  `keepRoot` tests passed without checking anything). Their setup takes
  `floor: () => 0`, as main's setups do. unread-fold's floor and #2366's
  fold-cache reorder came with forward-port 1, whose fold test needed
  them.
- #2252's recovery tests counted a repair as one evidence read: a kind-9
  request, or an `include_aux` filter with kind 40002 first. A layer-1
  repair sends the authored page and windows, then the roster sample,
  so each counted two. `tickReads` counts a channel read (`#h`, no `#e`
  or `ids`, kind 40002) leading a request, which is the sample once a
  repair, and the four tests count with it.
- entity-navigation's failed-message-read test failed any kind-9 read,
  so the Inbox's reads, which run first, used up its scripted failures.
  It fails only the timeline's `top_level` page read (11f22a6).

The new "a pass that drops a window chunk leaves unread owed" test still
fails with `owing = false` (4 reads, expected 5) and with unread always
owed (16, expected 13) under the new count.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
matt2e added a commit that referenced this pull request Nov 6, 2026
…next recovery tick reads it again

Forward-port 2 of the split (#note:c2f2f5fd): the layer-1 part of
dd306db on missing-messages-in-inbox (PR #1697 review, kalvinnchau,
P3).

Since #2252 the session's minute recovery tick repairs unread only while
a repair is owed or UNREAD_BACKSTOP_INTERVAL (15 minutes) has passed
since the last one. A pass that dropped a read after its retry (a window
request, a thread read, a root fetch) ended with `partial` set but
`owing` false, so every later tick skipped unread and the partial notice
and unknown channels stayed until the backstop. A pass in which nothing
landed throws and stays owed.

The repair's tail now sets `owing = unchecked`. A pass that drops a read
stays owed, as one that fails does, and the next tick repairs; a pass
with every read landed pays the debt. A relay that keeps dropping one
read costs one repair a minute, as one that keeps failing whole repairs
does. session.ts (FOUNDATION) is unchanged: its gate reads
`unread.owed()`.

Docs: docs/inbox.md says the repair stays owed after a dropped read, so
the next periodic check, a minute later, repairs again, where it said
"no automatic retry". docs/relay-queries.md counts a repair that dropped
a read as evidence debt, cleared only by one with every read landed. Its
"an evidence overflow" goes from the same sentence: this series' evidence
never resets on overflow, it evicts.

Test (unread-startup, through the session's real recovery gate): "a pass
that drops a window chunk leaves unread owed: ...". 129 channels, so the
windows are two requests: the viewer's page with 127 windows, then two
windows. A full reconnect pays the marker debt; a covered reconnect then
drops the second window request after its retry (`partial: true`). A
minute later, with the relay answering, the tick reads it again and
`partial` clears; a minute after that, the tick reads no evidence.
- With `owing = false` at the tail, as before: "expected 4 to be 5" at
  the tick a minute later.
- With the tail always owed: "expected 21 to be 17" at the last tick.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
matt2e added a commit that referenced this pull request Nov 6, 2026
…t a repair's channel read, and fail only the timeline's page read

Forward-port 6 of the layer-1 split: the test fixes main's later work
needs, from 367b38c and 11f22a6.

- Read floor (#1286). `createReadState` reads everything older than 30
  days before the device clock as read, and the repair fixtures date in
  1970, so 10 unread-repair tests saw nothing unread (and the two
  `keepRoot` tests passed without checking anything). Their setup takes
  `floor: () => 0`, as main's setups do. unread-fold's floor and #2366's
  fold-cache reorder came with forward-port 1, whose fold test needed
  them.
- #2252's recovery tests counted a repair as one evidence read: a kind-9
  request, or an `include_aux` filter with kind 40002 first. A layer-1
  repair sends the authored page and windows, then the roster sample,
  so each counted two. `tickReads` counts a channel read (`#h`, no `#e`
  or `ids`, kind 40002) leading a request, which is the sample once a
  repair, and the four tests count with it.
- entity-navigation's failed-message-read test failed any kind-9 read,
  so the Inbox's reads, which run first, used up its scripted failures.
  It fails only the timeline's `top_level` page read (11f22a6).

The new "a pass that drops a window chunk leaves unread owed" test still
fails with `owing = false` (4 reads, expected 5) and with unread always
owed (16, expected 13) under the new count.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants