## Summary Changes the private read-state API (`/buzz/v1`) to the sidebar wire format agreed in #buzzin-in-secret. **The counting code is unchanged.** Requests and responses take the new shape, so the client integration can build against it now and later behavior fixes (counting rules, followed threads, another-device pings) ship without another wire change. Nothing ships against v1 yet (no buzz-app use on `main`), so this replaces version 1 directly. Supersedes #8188 (same branch content, rebased onto current `main`, plus the thread-cap and doc changes below). ``` GET /buzz/v1/me/sidebar?limit=&cursor= | ?channel_ids=a,b,… → { channels: [channel], next_cursor } channel: { channel_id, unread: bool, mentions, read_through_id|null, latest_id|null, threads: [thread] } thread: { root_id, unread: bool, mentions, read_through_id|null, latest_id } POST /buzz/v1/me/read-state { intents: [{ type: "mark_through", target: { channel_id [, root_id] }, message_id }] } → { outcomes: [applied|blocked|invalid|unknown], channels: [channel] } ``` What changed and why: - **Channel and thread positions are fully separate.** Channel `unread`/`mentions` count only top-level messages. Thread replies count only on their thread row. `mark_channel_read` and its whole-channel cut are gone. "Mark channel read" sends a channel `mark_through` at `latest_id`, plus one per thread row to clear threads too. - **Channel `latest_id` is always a top-level message.** It used to be able to point at a reply, and a channel `mark_through` rejects a reply as its anchor. It also skips a reply-marked message whose ancestry never resolved, because marking through one would fail on every retry. - **`read_through_id`:** frontiers stored only the anchor's arrival time. Migration 0058 adds `personal_read_frontiers.through_message_id`, which follows the greatest arrival, and drops the now-unused `threads_through_timestamp`. This is the only storage change. - **Counts are plain numbers.** The exact/at_least/unknown wrappers are gone. Evidence the relay can't use (unusable tags, unresolved ancestry, undecided conversation membership, past the scan cap) leaves the message out, so counts can undercount. - **POST returns the updated row** of each distinct channel it applied to, so the client doesn't refetch after every read. If those rows can't be read after the writes commit, `channels` is omitted and the outcomes still stand. The row refresh shares the 8 s intent deadline. - **Up to 25 thread rows per channel** (was 5). Five hid a mention in a sixth unread thread. 25 is the most that keeps a POST's rows for `MAX_INTENTS` (100) channels inside the 1 MiB response limit; 50 would be ~1.4 MB, a 503 that hides already-committed outcomes. A unit test builds that worst-case response and fails if the cap outgrows the limit. - **The doc separates shape from behavior.** `/buzz/v1` promises a stable shape (fields, types, status variants, what `mark_through` does). What the counts include (which messages are unread or directed, which threads are listed, the bounds) may change while v1 is pre-release, so clients replace their rows with each response instead of reconciling against local counts. - **Removed from the wire:** the explicit-contexts read (`GET /me/read-state?targets=`), `account`, channel name/type/archived/hidden (these come from Nostr metadata), `latest_message_at`/`_complete`, and the NIP-11 `max_contexts`/`max_context_messages`/`max_thread_summaries`. Deliberately deferred (behavior or additive, no shape change): - Thread counting rule (relay counts a reply if you wrote or replied to its direct parent; the client uses "posted anywhere in the thread or followed"), the workflow-owner attribution p-tag, and thread-row `mentions` counting every counted reply rather than directed ones. - `follow`/`unfollow` intents, listing followed threads that are already read, and a truncation flag for the thread list. Thread: buzz://channel/6f773b94-34d0-4004-bec9-a100b6ad17d5 (#buzz-bff, root 31020fff) ### Related issue Follows #8157. Supersedes #8188; replaces the wire parts of the closed #8155. ### Testing - `cargo test --no-fail-fast -p buzz-db -p buzz-relay` at1f041e46: all pass except 7 relay unit tests (`api::media::tests` ×6 needing a seeded local DB, `api::mesh_demo` round trip) that fail identically on `main` (394d4a43) in the same environment. - `scripts/postgres-test-run.sh -p buzz-db -p buzz-relay` against local PG14 + Redis at88d2f6fc: 836 passed, 26 failed. 25 also fail on `main` in the same environment (partition maintenance tests and `huddle_started_links`, which need PG16). The 26th, `boot_lifecycle::hanging_redis_peer_times_out…`, is a timing assertion that failed under full-suite load and passed 3/3 when rerun alone on this branch. - All `personal_read`, `migration`, `buzz_v1`/`accessory` and NIP-FI wire-contract tests pass, including the new response-size bound test and the 25-thread `summarize` cap test. - `cargo fmt --check` and `cargo clippy -p buzz-db -p buzz-relay --tests -D warnings` are clean. - Not done: running a standalone relay binary (the dev object store rejected writes with HTTP 403). The relay tests drive the real HTTP router with NIP-98-signed requests. A human test against a local relay is still needed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Honey <8e307ae0076a4dab6b94b036ea3edc7e08f823a625269c1e6919e881a048b4d2@buzz.block.builderlab.xyz> Co-authored-by: Honey <8e307ae0076a4dab6b94b036ea3edc7e08f823a625269c1e6919e881a048b4d2@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1.2 KiB
Read-state accessory extension constraints
Design guidance for follow-up work, not implemented wire capabilities. The current API contract remains authoritative.
Thread rows are implemented (see threads in the API contract). Richer
previews that carry signed-event content should be opt-in and preserve the base sidebar's work/byte budgets;
fetching detail separately remains valid. Reuse canonical classification and
read frontiers, not a second definition of unread. Historical-mention eligibility
or mute-aware mentions requires an explicit policy contract, not an unnoticed
change to today's mentions field.
Channel-content revisions, personal-state revisions and synchronized preferences are separate from read frontiers. Future revisions must describe the same snapshot as their payload, and are invalidation tokens, not history cursors or access grants. Count reuse also needs time/configuration validity because the unread horizon moves without writes. Mutes and manual-unread overrides must not be encoded by advancing or rewinding a read frontier.
These are extension constraints, not implemented capabilities: this version does not advertise revision-based reuse or synchronized overrides.