Repository navigation
Conversation
matt2e
marked this pull request as ready for review
November 4, 2026 10:11
matt2e
force-pushed
the
inbox-1-channel-windows
branch
3 times, most recently
from
November 5, 2026 01:29
c7de758 to
01f0b61
Compare
matt2e
force-pushed
the
inbox-2-device-record
branch
from
November 5, 2026 04:30
1c79684 to
f8e0f0e
Compare
matt2e
force-pushed
the
inbox-1-channel-windows
branch
from
November 5, 2026 06:01
13ffa9f to
e3a9a92
Compare
…unch Reapply layer 2 of the Inbox split (de2d02e over 4d36397) on top of the channel windows, leaving out what layer 1 already carries. The unread record in buzz-unread-v1 now also holds the signed evidence behind the Inbox's rows and the structure that groups it. On launch, restore verifies it again in batches of 8 (verify.restore) and admits it whole, or discards it all if any event fails. Rows then show before the network answers, and with the relay down. Reads that should have returned a restored event and did not drop it. A cache clear wipes the record, and a partition purge fences any session that read before it, so a stale window cannot save a left community back. The complete flag, and the bookkeeping that only serves it (the dropped list, the ungrouped and lost checks), stay with the catch-up layer, which is its only reader. Tests are the layer-2 tests of the trimmed head (34fc406): unread-storage and unread-restore, the deleted-events fixture in unread-repair, and the reload, refused-write and purge journey in unread-storage.spec.mjs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
…ed them The relay matches `#h` against the channel it filed an event under. For an event with no `h` tag, such as a reaction taken off (an h-less kind 5), that is its target's channel, which the client cannot see. Reconcile now counts such an event in a channel-scoped filter's page. Otherwise it leaves a full page looking short, and restored evidence below the page's cut is dropped though the relay still holds it. Every read reconcile judges in this layer asks for content kinds (9, 40002, 40008) alone, so no response holds such an event yet. The test that shows the drop (da8ed62) reads the catch-up delta and stays with that layer. This moves here from da8ed62 because it belongs to reconcile. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
matt2e
force-pushed
the
inbox-2-device-record
branch
from
November 5, 2026 06:55
f8e0f0e to
ca55105
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stack
This stack splits #1697, the frozen draft umbrella at
34fc40668, into three layers. Each PR is based on the one before and merges in order. #2408's tree equals #1697's, and #1697 closes when #2408 merges.inbox-1-channel-windows): reads each channel on its own and remembers your threads, so busier channels no longer push a thread out of the Inbox.inbox-2-device-record): saves the Inbox's evidence on the device and restores it on launch. ← this PRinbox-3-catch-up): catches up from a saved mark instead of re-reading everything on every repair.Before and after
#2401
47e9a8031, this PR's base, is on the left and this PR1c7968418on the right. Both run the web app in Chromium on a fresh device against one local fixture relay with the same made-up team. The box at each pane's bottom right shows the time since the pane loaded, its relay queries and the bytes downloaded.Saved Inbox rows within a second of a relaunch, while message reads are slow
After a normal launch, the relay holds message reads for 15 s and both panes reload. This PR shows the rows it saved on the device right away and keeps them through the read error; #2401 shows none until the reads return.
pr2407-s5b.mp4
What this PR adds
This is PR 2 of three. It saves the evidence behind the Inbox on the device and restores it on launch. It does not catch up from a mark: as in PR 1, every repair (launch, reconnect, the 15-minute backstop, and recovery while a repair is owed) reads in full.
buzz-unread-v1'seventsstore, which PR 1 created empty, so there is no IndexedDB version bump. A PR 1 record has nostructure; it reads as its threads alone.verify.restore). It admits them all once every event has passed. One event failing verification discards them all, keeping the remembered threads. Rows then show from disk before the relay answers, and with the relay down.docs/inbox.md.time:tokenfence. A session that read before the purge cannot save the left community back, even after reading again. A session started after it, as when rejoining, saves as usual.relayMatches(own commit): reconcile judges a page as the relay matched it. The relay matches#hby the channel it filed an event under, so an event with nohtag matches by its target's channel.Left for PR 3:
completeand the bookkeeping that only serves it: the list of evicted content, and the ungrouped and lost checks. Only PR 3'sroute()reads it. The test "a selection over the record's bounds, or one retention dropped, saves an incomplete record" moves with it.One side effect of the deferral: retention no longer holds a filter slot for content evicted during the repair. A selection can therefore keep the next older held event in that slot. That event is real evidence, and on relaunch it sits below the read's cut, so reconcile does not judge it.
Details and limits:
docs/inbox.md.Commits
e72cad769reappliesgit diff 4d3639704 de2d02efbwithout the hunks PR 1 already carries:keepRoot, the structure-cache order and exact bound, request shapes, storage names, and twodocs/inbox.mdlines.unread-storage.tsandthread-participation.tsstart fromde2d02efb's versions.completeis deferred, as above.34fc40668, fix(unread): keep participated threads in the Inbox and persist its evidence across launches #1697's test trim:unread-storage.test.ts: 9 tests, with the in-memory IndexedDB kept inline.unread-restore.test.ts: 6 tests, andde2d02efb's harness withfloor: () => 0.unread-repair.test.ts.unread-storage.spec.mjs.complete, the mark and its entry,roster,whole,through,absent,sameMetaand the delta.1c7968418addsrelayMatches, moved fromda8ed62cc. In this layer, every read reconcile judges asks for content kinds alone (kinds 9, 40002 and 40008, confirmed by the trace check below), so no response yet holds an event with nohtag. The test that shows the drop reads the catch-up delta and stays in PR 3.Behaviour and cost against PR 1
1c7968418vs47e9a8031: scripted reader in Vitest, 7 launches, 31 requests and 73 filters each side (see Checks). Not a live relay.unread-restore.test.ts. Time to first row: not measured.1c7968418. Restore's own loop (eventDtoover a JSON clone, 8 a batch,yieldToHostbetween). Headless Playwright Chromium and WebKit on an Apple M3 Max, macOS 27.2. 2,048 synthetic signed kind-9 events of 573 B each. Medians of 5. Not the desktop WKWebView.browserUnreadStorage.A save writes only what changed against the partition's record: the meta, a put for each event the partition lacks, and a delete for each it no longer holds. A save identical to the last issued one is skipped. Restore runs once a session.
FOUNDATION
src/features/relay/session.tsis FOUNDATION. This PR changes 3 lines (+5/−2):const unreadWiped = unread.clear();, andunreadWipedjoinschannels.clearCache()in the clear'sPromise.allSettled. A cache clear therefore settles once the record is wiped, and rejects if the wipe fails. These are #1697's lines from9815e47a0, moved here as part of the split. Matt Toohey approved them before the change. Nothing else insession.tschanges.Checks
All of these were run on
1c7968418:de2d02efb,git diff de2d02efb HEAD -- src testsover the branch's files:fold.ts+4/−2: PR 1's per-channel fold.unread-storage.ts+6/−17: thecompletedeferral only.unread.ts+87/−60: the per-channel fold (byTarget,foldable),owing = unchecked, the exact structure-cache bound (heldRoots,rememberedRoot),relayMatches, and thecompletedeferral.de2d02efb:session.ts,partition-purge.ts,thread-participation.ts,reader.ts,inbox.ts,unread-window-batch.ts,errors.ts,client-metrics.tsandInboxPage.tsx.34fc40668, so the diff againstde2d02efbshows its trim. Every test title here exists in34fc40668, except:keepRoottests and the window-drop test.34fc40668tests whose layer-3 clauses are removed. They takede2d02efb's titles or close to them.unread-restore.test.tsharness with fixed keys.reader.readfilter list.unread.tsandunread-storage.tschecked out from47e9a8031. The recorded calls are identical, 31 requests and 73 filters each, all kinds 9, 40002 and 40008./querytrace in a browser, and a live relay.bin/pnpm typecheck: clean.biome check --error-on-warningson the 8 changed code files: clean.git diff --check 47e9a8031 HEAD: clean.src/features/relay,src/bundled/inbox,src/features/notifications,src/features/messagesandsrc/features/communities: 274 files passed and 1 skipped; 6,146 tests passed and 4 skipped.--no-depsonunread-storageand PR 1's eight (sidebar-unread,message-navigation,scroll,navigation-thread-history,inbox,inbox-edit,thread-unread,channel-lifecycle), one engine after the other: Chromium 77 passed (2.5 min) and WebKit 77 passed (3.2 min), with no timeouts or retries.--no-verify.Browser cases. One added,
unread-storage.spec.mjs: "unread record survives reload, keeps the last record over a refused write, and is purged with only its partition".unread-storage.test.tsstands in for all three.save(), it fails atunread-storage.spec.mjs:129("purged by another window"). On PR 1'sunread-storage.tsit fails at the first record read.Not verified: the desktop app (restore timing in WKWebView, the native host), a live relay, record sizes on a real roster, and
just scan. Agent review has not run, nothing was exercised in the running app, and there has been no human test, so this stays a draft.🤖 Generated with Claude Code