Skip to content

feat(read-state): sync mark read, channel mark unread and thread follows - #9501

Draft
wpfleger96 wants to merge 12 commits into
mainfrom
duncan/read-state-sync-spec-mobile
Draft

wpfleger96 wants to merge 12 commits into
mainfrom
duncan/read-state-sync-spec-mobile

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented Nov 14, 2026 •

Copy link
Copy Markdown
Member

Reading, mark read, whole-channel mark unread and thread follow choices now sync across a person's installations. This PR specifies the rules in NIP-RS and implements them in the mobile app.

Spec (docs/nips/NIP-RS.md)

  • Defines catch-up and displayed.
  • A catch-up advances the channel's own frontier (top-level messages) or thread:<root>. activity: and thread-activity: become read-only legacy keys.
  • Explicit mark read is synchronized state. A first explicit read of a message with no override group writes only its msg: frontier. Display, dwell, scroll, catch-up and mention visibility create no msg: entries.
  • Only whole-channel override groups are published, merged and cleared with the counter. Readers ignore peer msg: and thread: groups.
  • Single-message and thread mark unread stay on the device. They end once the merged frontier covering them passes their baseline, from any installation, or on display, catch-up or explicit read. The section also states clearing scope, the accepted concurrency limitation and capacity.
  • When the merged override union no longer fits the primary, the client rewrites the primary compactly from the groups its last published primary carried, each canonical against current frontiers. It never omits merged groups or tombstone floors that primary carried, and override actions fail visibly.
  • Adds Buzz Thread Follow Choices: thread-follows:<32hex> coordinates, validation, three-state verdict, last-writer merge with ties going to unfollow, publication and recovery, legacy import at at = 0, and capacity.

Mobile

  • Catch-up writes the main frontier. A flat timeline reads up to the newest top-level message it reached.
  • Explicit mark read publishes. Passive msg: marks stay on the device.
  • Whole-channel mark unread publishes an override group and merges peer groups. An explicit read or catch-up clears it, even when no frontier moves. The action fails visibly at the uint32 limit or when the primary is full. A synced clear the primary can't carry is rolled back, and the channel view reports it once while the failure stands.
  • Device-local message and Inbox unread overrides record a baseline and clear when a covering frontier passes it, including one merged from a peer.
  • Thread follows move from local storage to the synced thread-follows: slots. They share the read-state client_id, journal, created_at sequence, snapshot and live subscription, and spill across up to 16 slots with stable placement. An unreadable coordinate marks follow state incomplete and blocks this installation's follow publication.
  • Follow and unfollow are refused on every cold start of the writer until its first complete recovery of its own state succeeds and every owned follow head is readable. Cached follows show at once. A refused tap changes nothing and shows "Follow choices are not ready. Connect and try again." Reads, catch-up and both kinds of mark unread are not gated.
  • A follow tap is accepted only once journaled. The accepted own head, pending edits and the exact in-flight packet are kept apart, so a retry or restore never drops or re-attributes an edit.
  • Pre-sync local follows are imported at at = 0 after a complete merge, once their channel is known from a relay event. An imported key is settled, and its legacy copy retired, only when the journal write the import awaited covered it and that entry is still current.
  • An explicit unfollow suppresses ordinary replies at live and catch-up admission. A change in follow choices drops the unread replies already counted for an unfollowed thread, and rechecks the channels of a newly followed one.
  • Two-device fixtures against an in-memory relay cover channel round-trips and clears, stray message groups, the first explicit message read, the primary cap, follow propagation, same-second and backward-clock follow publication, legacy import, retry after a failed publish, unreadable coordinates and slot packing. Recovery fixtures cover the startup gate, journal failures during flush, interleaved legacy imports, and restore of an older journal.

Related: block/buzz-app#3552

wpfleger96 and others added 11 commits November 14, 2026 13:36
Specify and implement cross-device sync for manual read state and thread
follow choices in NIP-RS and the mobile client.

- Catch-up advances the channel frontier (top-level messages) or
  thread:<root>; activity: and thread-activity: become read-only legacy keys.
- Explicit mark read publishes; a first explicit read of a message writes
  only its msg: frontier. Passive marks stay on the device.
- Whole-channel mark unread publishes an override group that merges and
  clears with its counter across installations; msg:/thread: groups from
  peers are ignored. Message and thread mark unread stay on the device and
  end once a covering frontier passes their baseline.
- Thread follows sync on thread-follows:<slot> coordinates, with legacy
  local follows imported at at=0. An explicit unfollow suppresses ordinary
  replies at admission, and retained unread state is re-evaluated when
  follow choices change.

Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Signed-off-by: Will Pfleger <wpfleger@block.xyz>
…played

Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Signed-off-by: Will Pfleger <wpfleger@block.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Signed-off-by: Will Pfleger <wpfleger@block.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Signed-off-by: Will Pfleger <wpfleger@block.xyz>
… apart

Every follow edit and packet takes a saved, strictly increasing
reservation. Pending edits stay in every later full-slot packet until an
own head at or above them, or the exact packet that captured them by
event ID and generation, covers them. Adds witnesses W1, W2, no-omission
and the same-second own-head tie.

Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Signed-off-by: Will Pfleger <wpfleger@block.xyz>
Reading one Inbox row clears only that row's marks; catch-up and explicit
channel or thread reads still clear the whole scope. A failed synced clear
stays reported through unrelated successful syncs until it lands or the
override ends. Adds the real-seam two-row Inbox test, the isolated
unreadable peer head across restart and the failed-clear lifetime test.

Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Signed-off-by: Will Pfleger <wpfleger@block.xyz>
setThreadFollow resolves after the journal write; a failed write reverts
that tap and reports it. Reservations past uint32 fail visibly. Adds
tests for spill no-omission, exact-byte retry after an unknown outcome
and a writer retired while signing.

Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Signed-off-by: Will Pfleger <wpfleger@block.xyz>
Freeze each follow candidate's plaintext, captures, basis and reservation
together and journal the reservation before signing. Allocate owned slot
IDs when an edit is placed. Resolve an OK placeholder against the retained
signed packet. Floor reservations at accepted own heads, hold follow
retries and refuse taps behind an unreadable own head, settle legacy
imports only once journaled, and never prune an explicit unfollow.

Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Signed-off-by: Will Pfleger <wpfleger@block.xyz>
Every import awaits the serial journal queue, so a key is settled only
once a write covering it succeeded, including an overlapping import's or
tap's still-saving entry, and only if that entry survived. Reserve before
touching a key so a refused reservation leaves nothing behind.

Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Signed-off-by: Will Pfleger <wpfleger@block.xyz>
…aved state

Refuse follow and unfollow until this writer's first complete recovery,
with every owned follow head readable. Settle a legacy import only for
entries the awaited journal write covered and that are still current.
Describe the overfull primary's compact rewrite in NIP-RS.

Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Signed-off-by: Will Pfleger <wpfleger@block.xyz>
A writer accepts follow taps only once a complete recovery is durably
journaled, on both the startup and publish recovery paths.

Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Signed-off-by: Will Pfleger <wpfleger@block.xyz>
@wpfleger96

Copy link
Copy Markdown
Member Author

Desktop counterpart (new Buzz): block/buzz-app#3552

Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Signed-off-by: Will Pfleger <wpfleger@block.xyz>

This branch has not been deployed

No deployments
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.

1 participant