Skip to content

Use relay sidebar API for unread and read-state authority - #427

Draft
tlongwell-block wants to merge 84 commits into
mainfrom
meli/buzz-v1-sidebar
Draft

tlongwell-block wants to merge 84 commits into
mainfrom
meli/buzz-v1-sidebar

Conversation

@tlongwell-block

@tlongwell-block tlongwell-block commented Sep 29, 2026 •

Copy link
Copy Markdown

Make the relay the one authority for unread and read state

Problem

Today the app decides what is unread on its own. It counts from the messages it happens to have loaded, keeps its own read positions, and syncs them through its own encrypted read-state events. Two sessions of the same person can disagree, a channel that was never opened has no trustworthy count, and every client has to reimplement the same rules.

Outcome

The relay's /buzz/v1 sidebar API (block/buzz#7906) answers "what is unread for this person" and stores how far they have read. The app renders that answer and sends reading intent. It no longer counts.

The contracts are docs/unread.md and docs/inbox.md. Read those before the code.

Before this merges: three things that change who has unread

  • Needs feat(relay): private read-state accessory API buzz#7906 at a09bbdff5 or later. This client expects each unread message to carry reason. Earlier heads of #7906 sent attention instead, and this client does not decode that. #7906 is now at ce9481aa6, which is what the live pass below ran against. The two commits after a09bbdff5 do not change the wire format: one lets a thread whose root is not a counted kind (a diff, for example) be read, and one adapts the API's auth to main's NIP-FI Shadow mode. #7906 can merge first. It has no caller until this PR.
  • Deploy order. The relay with #7906 must be live with BUZZ_V1_ENABLED=true first. Against a relay without it, unread is unsupported and nothing is badged. There is no legacy fallback, by design.
  • The packaged desktop app has no adapter for this API yet. On main the desktop shell syncs read state through native code. This PR removes that path, and only the web transport calls the new API. Until a native adapter lands, a desktop build from this branch has no synced unread. That adapter is the first follow-up. Desktop previews build from main on a schedule, so a preview built after this merge should not be promoted to the updater feed until the adapter lands.

What changes for a person using Buzz

These are deliberate. Each differs from main today.

  1. Reading moves a position, not a set of receipts. After a message has been fully visible for the dwell time, the app marks that timeline or thread read through its newest such message. Everything earlier in the same timeline or thread becomes read with it. That includes earlier top-level mentions and broadcasts in a channel, which main keeps unread one by one.
  2. Main's "catch up at the bottom" rule (Fix unread catch-up and reading focus #470) is removed. The position in 1 replaces it. Fix unread catch-up and reading focus #470's reading surfaces (focused panel, tab, composer) and its dwell time are kept as main has them.
  3. "Mark as Read" on a channel reads through the relay's latest message for that channel, not through the click time. A message that arrives after the relay's answer stays unread.
  4. A badge clears as soon as you read, before the relay confirms and while offline. The relay's count is kept unchanged underneath. If the local save fails, or the relay answers blocked or invalid, the badge comes back.
  5. "Mark unread" is local to this browser profile and is not synced. On a message it marks only that message. It strengthens the channel label, adds no attention dot, and leaves the relay's count as it is.
  6. Someone else's read activity alone never makes a channel unread or moves it in Recent.
  7. If you lose access to any channel while a read action is still being saved, the actions not yet saved fail with an error and can be repeated. Main kept them. Nothing is lost silently. This is tested and written into docs/unread.md.
  8. A broadcast reply can change its reason after the fact. When the relay cannot decide in time whether a broadcast reply is in your conversation, it stays unread as a broadcast, which has no notification category. A later refresh can upgrade it to a conversation reply.
  9. Read positions and Mark-unread reminders from earlier preview builds start over once. Nothing is imported from the old local database (buzz-read-state-v1). It is left in the browser profile, never read again, and this client carries no code for it.

Inbox: main's page, answered by the relay

Main gained an Inbox while this branch was open: first a data layer, then the page (#499), then Drafts (#502). Main's Inbox decides on the client which messages need attention, which is the thing this PR removes. The merge keeps main's page, finite feed and types and rebinds the rows to the relay: a row exists only for a message the relay currently calls unread with reason direct, mention or conversation. The contract is docs/inbox.md.

What a person sees that differs from main's Inbox

  1. The Inbox is unread only. A conversation leaves the list when it is read. Main kept read conversations as an archive.
  2. Two controls are visible and do nothing. "Unread only" changes nothing, because the list is already unread only. "Mark unread" in the row menu is always disabled, because every row is unread. Both are left as main has them, untouched, until the relay can list what needs attention (the follow-up below). Hiding them is a product call that is open with Tyler.
  3. An opened conversation stays open after it is read. Opening a row captures that visit. The conversation stays in the detail pane until Back or Close, even though its row has left the list. It adds no row. Loss of access and a deleted message are shown by the existing detail and readers.
  4. Back lands on the first remaining row after a read visit, because the row that was opened is gone. Main returns to the row it came from.
  5. Clicking a top-level channel mention opens it and reads nothing at that moment. Reading it means reading the channel through that point, which also reads everything before it, and a click must not do that silently. It clears the way any channel message does: once it has been shown, focused and settled for the dwell time, the reader marks the channel read through it, older top-level unread included. Thread rows read their thread on open. DM rows read the DM.
  6. A manual "Mark unread" alone makes no row. It overlays a row the relay admits, and it is not erased when its row is absent.
  7. A mention sent before you joined gets a row after you join, if the relay calls it unread. Before joining, cached public messages ask nothing and show nothing.
  8. Unread from before launch may have no row. Rows come from messages the session already holds, plus the finite feed of messages addressed to you by tag. A DM that arrived before the app started was measured this way on the browser fixture: the relay reports it unread, the sidebar badges it, and the Inbox says "No recent activity". When a later DM arrives the row appears and counts only the messages it holds (1 where the relay says 2). Main kept a dedicated unread store for this. The relay listing follow-up removes the gap.

Kinds and horizon come from the relay: it advertises the kinds it answers for (today 9, 40002, 45001, 45003) and applies its own 30-day window. A row's messageId and rootId describe one message, the oldest unread one in the group. latestMessageId, the target and readThrough follow the newest.

What changed in main's page to fit

InboxPage.tsx changes by +12 / -11 lines against main: the selection holds the clicked item, and the effect that closed the detail when its row left the list is removed. Nothing else in the page, the detail or Drafts is changed. Main's mounted tests (InboxPage.test.tsx, InboxDetail.test.tsx, DraftsView.test.tsx) and its five tests/browser/inbox* specs were written against client-decided rows and main's read-state fixtures. They are restated against the relay's answers. Cases that pinned behaviour this PR drops (the read archive, per-message read receipts) were decided one by one: restated to the new rule where there is one, removed where there is not.

Which channels are skipped without asking

The Inbox may ask the relay about at most 100 messages at a time. To spend those on channels that can have a row, a channel is skipped when its sidebar row proves there is nothing in it. A row is that proof only if all of these hold:

  • its attention count is exactly zero,
  • its request started after the last event cached for that channel, and
  • it has cleared any unread live hint for that channel.

A queued, in-flight or failed sidebar read proves nothing, and neither does a row older than the newest cached event. What this rests on is reading the source and unit tests with mutants, not a run against a real relay. The sidebar row and the per-message answer come from two separate responses, and nothing makes the pair atomic.

A message the relay has not answered for yet keeps the snapshot unresolved (stale, no error) and has no row. A message that cannot be asked about (unavailable, or ancestry that cycles, is too deep or crosses channels) is skipped and does not hold the snapshot open.

Named limit: the 100 newest

The 100 questions go to the newest messages from other people in channels not proven empty. An older mention with 100 newer cached messages ahead of it gets no row, and the snapshot reads stale instead of complete. This is pinned by a test (100 newer gives no row, 99 gives a row) and written in docs/inbox.md. Removing it, and item 8 above, needs the relay to list what needs attention. That is a follow-up to block/buzz#7906, agreed with Tyler, and not part of this PR. "Ready" here means the bounded candidates have answers. It does not mean a complete historical Inbox.

Notification and open-message subscriptions come first under the shared context limit. They take an Inbox question only when they would not otherwise fit.

For anything else built on the Inbox

  • status and freshness must be honoured: stale means the list is not known to be complete. An empty list is not "nothing to do".
  • The feed and the verdicts are two subscriptions with two statuses. A finished feed does not mean the verdicts are in.
  • An empty readThrough on a channel row means "no Inbox read action". Do not substitute a channel read or a single-message receipt.
  • Open a row with messageId and rootId together. They are a pair.

One fix beyond the cutover: a focused message keeps focus while the list re-measures

  • What went wrong. The message list hides each item until its new position is measured. When the item holding keyboard focus was hidden that way, focus dropped to the page and nothing gave it back. It showed when a DM was opened from the Inbox and its first history load returned messages both older and newer than the opened one. CI caught it once at c70aec6a (inbox.spec.mjs, Chromium).
  • The fix is five lines of CSS in Messages.module.css: a message whose list item contains focus stays visible. The rule uses :has(:focus) on the list item, not :focus-within on the message. The message action bar is drawn in the top layer, and with a control in that bar focused, :focus-within on the message measured false on both engines.
  • Evidence, both engines, one worker, no retries. The existing test now holds the history load until the row has focus, so it reaches the failing order every time. Without the rule it fails 5 of 5 on Chromium and 5 of 5 on WebKit. With the rule it passes 20 of 20 on each. Five new cases in inbox-row-focus.spec.mjs cover focus on the row's action button, focus on a control in a later row, focus a person moves elsewhere while the row is hidden, Escape, and the Inbox withholding the conversation. Without the rule four of the five fail on both engines, and the fifth passes either way, as it should. With the rule all five pass 20 times per engine. These counts were taken on the parent commits with the same CSS and test bytes applied. At this commit the files passed once per engine locally and once per engine in CI.
  • Where the row is drawn while its item is hidden. With measurement delayed on purpose, the focused row was painted at the same position before and after measurement, on both engines. In that probe the row above was already measured. Not covered: a focused row below rows that are not yet measured, and a focused row taller than its estimate above a visible row.
  • On main. The list hides items the same way on main, so by reading the source the loss is reachable there through other inserts. It was not shown on main at runtime: in that test main's Inbox opens the older DM and never reaches this order.
  • Left alone: a defect main has too. After the Inbox withholds a conversation and then recovers it, focus is not handed back to the row that had it. It fails the same way on main 8e371726 and on this branch, 4 of 4 runs each across both engines. The row's element is replaced during recovery, so the hand-back has nothing to focus. InboxDetail.tsx is identical to main. Not fixed here. The new test stops before that step, and an issue is drafted.

Known limits in this version

  • A failed small sidebar update is not retried. When a targeted sidebar read fails, its channels are not read again until the next full roster read, normally within the 60 second cadence while the tab is visible, or on focus or reconnect. There is no guaranteed bound under continued failure. Main counts from live events and has no such dependency. A fix was sized (one full refresh after a failed read) and left out because it turns one failed request into 7 to 50 while the relay is already failing.
  • A read made in another session arrives on the next full roster read, not at once. In the live pass a read in one session reached the other after 59.8 seconds. That is one observation on a healthy relay, not a bound. Main's latency for the same thing was not measured.
  • A relay rejection of an already saved read (blocked or invalid) is recorded but not shown. The channel is read again and the count coming back is the feedback.
  • An Activity preview is read when the popover opens: it is not live, and it takes the newest 500 edits across the listed replies.
  • After a deletion, the app refreshes only the channel named on the deletion event. Buzz never sends a deletion that spans channels.
  • Automatic reading that cannot be saved (journal full) stops without a notice. Explicit actions show the error.
  • An Inbox row can show a message's text from before an edit after a reconnect. This applies to a DM or conversation reply that does not tag you. Main re-read recent edits on reconnect, and this PR removes that read with the client's own unread store. The row catches up when another read or a live delivery brings the edit. A deleted original leaves the list on the next successful refresh. Rows for messages that tag you are still checked for edits and deletions. This was found by reading the source and was not reproduced in a browser. It is written in docs/inbox.md.

Test changes a reviewer should know about

  • A known WebKit log is allowed during a confirmed page replacement. When a page reloads, WebKit can log the departing page's sidebar read as Fetch API cannot load ... due to access control checks without Playwright recording the request. tests/browser/page-errors.mjs now excuses that exact message for that exact URL, at most once per main-frame navigation, and only when the same navigation then commits a new document. A genuine denial of that read from the old page in that interval (about 0.1 to 0.2 seconds) is an accepted blind spot. This is a test-watcher allowance, not a fix. Without it the error appeared in about 1 in 3 WebKit runs of one reload journey.

  • The allowance reads a Playwright internal. New-document identity comes from the client frame's private "navigated" event (newDocument.request), because the public framenavigated does not carry it. @playwright/test is pinned to exactly 1.63.0. If the internal changes, nothing is excused and the failure names the internal. docs/contributing.md says to recheck it when upgrading. Playwright preserves the order WebKit delivers events in. That WebKit sends the log before the commit is observed, not guaranteed.

  • The drift report can also fire for a test's own timing. By reading the watcher, two shapes would produce it with no Playwright change: a main-frame navigation requested before the previous document fires domcontentloaded, and a navigation with no new document (a download or a 204) followed by setContent. No spec does either today. Sami ran 60 real-WebKit rows across superseded navigations, 204s, downloads, redirects, routed responses, history moves and setContent with no false report.

  • A sidebar pause on page exit was tried for the same error and removed. Over 30 WebKit runs each way it took one reload journey from 11 errors to 0 and another from 0 to 2. The reader's existing pause is unchanged from main apart from the removed read-state snapshot.

  • Browser fixtures now use UUID channel ids. Specs that named channels by label ("alpha", "beta") were adapted, and each merge with main brought more.

  • Nine merges with main since b2f8d23a: c9946be4, f432088a, 8eca3958, e38d5405, 0fb09ceb, 85c7f392, 512a9c2, e8c21196, 8e371726. The first brought the Inbox data layer. The second conflicted in 16 files: 12 browser specs, websocket-actions.profile.mjs, ChannelSidebar.test.tsx and two Tauri permission files. Specs keep this branch's UUID ids and real read actions and take main's openChannelDetails helper (feat(channels): unify header actions and inline details editing #487) and the main fix: pause the clock while an Activity-opened thread must stay unread #548 clock pause. The Tauri files keep main's new permission and do not restore the three removed read-state commands. The third (Defer untouched desktop message action bars #528) merged without conflict. The fourth brought the Inbox page (Show exact Inbox conversations with read recovery #499) and conflicted in MessageRow.tsx (two import lines), fixture.mjs and docs/inbox.md. The fifth brought Drafts (Resume scoped drafts in Inbox and discard deleted attachments #502) and conflicted in InboxPage.tsx (one hunk, indentation against the captured item), docs/inbox.md and two specs. The sixth (fix(runtime): bundle Goose-compatible Buzz MCP server #547, Polish composer typing indicators #551) conflicted in one hunk of inbox-drafts.spec.mjs, where main and this branch had made the same fix. The seventh (Add Canvas history and confirmed restore UI #552, Canvas history) merged without conflict. The eighth (Add trusted enterprise relay discovery #524, Make bundled plugin defaults explicit and hide unavailable entry points #557) conflicted in the import blocks of layout.spec.mjs and sidenav-polish.spec.mjs. Both keep this branch's UUID ids and take main's new per-spec Bestie opt-in. The ninth (Allow plugins to unregister (kind 5) owner-managed agents through native services #536) merged without conflict and brings three native-side files.

  • Most of what the merges broke, Git did not report. Main's new tests name channels by label, which this branch's fixture rejects, or use fixture helpers this branch replaced. Found by reading every test main touched: six sites after f432088a (channel-header-menu, channel-members, one stale settings click in websocket-actions.profile.mjs), five after 8eca3958 (message-actions 3, startup 2), the four Inbox specs and seven sites in inbox-drafts.spec.mjs (main's bytes fail 12 of 12 and 4 of 4 on this tree), six type errors in DraftsView.test.tsx, the two mounted Inbox test files (they imported the removed read-state fixtures), and two event tags in canvas-history.spec.mjs (main's bytes fail, the editor stays empty). Found only by running the browser: main's startup test (Animate Buzz startup through initial content readiness #534) called a fake-relay helper, holdUnread, that this branch replaced with a hold on the sidebar API. Also found only by running: Show exact Inbox conversations with read recovery #499 brought a second, earlier thread_window handler into tests/browser/fixture.mjs. Git merged it cleanly. It answered before this branch's strict handler and left out older joined replies, so one thread test showed 2 rows where it expects 3. The 67-line duplicate is deleted and one handler remains. One more was a label id inside a test, rejected by the sidebar journal and swallowed by a .catch that main has too, which made a read test look like a product failure.

  • Two races in main's Inbox specs are closed with waits, not delays. inbox.spec.mjs asserted on a revealed message one frame before the reveal's confirming frame. Without a two-frame wait it failed 10 of 25 Chromium runs, with it 0 of 25. The step order is main's, and it was not measured on main. In inbox-drafts.spec.mjs the keyboard helper focused Send while it was still disabled (a thread draft enables Send only after its saved root is verified, about 30 ms later, and focusing a disabled button does nothing). It failed 1 of 39 WebKit runs here. Main made the same fix in Polish composer typing indicators #551, and the merge keeps main's wording.

  • Four browser tests were repaired after the first CI run, at 54809c01, went red on them. All four faults were in the tests. Two message-menu tests (message-management, agent-activity) raced the app's automatic read, so the menu correctly offered "Mark unread" where they expected "Mark read through here". They now wait for the automatic read, mark the message unread, and then exercise the manual action. One thread-history test installed its fake clock and then paused it at a time already past. It now lets pauseAt install the clock. The fourth is the typing test in the next item.

  • edge() in tests/browser/timeline.mjs, main's helper from fix(messages): stop three timeline scroll races that flake CI #456, gains one optional position argument. edge() hovers the scroller's centre before it wheels. On this branch the thread typing test must wheel about 316px back to the bottom (threads open on the newest window, and the test scrolls up for older replies first). On Linux the centre lands on the hovered row's floating toolbar, and a wheel over that toolbar does not scroll the thread. The test now passes { x: 100, y: 100 }. Callers that pass nothing hover the centre as before. The toolbar's source is unchanged from main, and its wheel behaviour is not changed here. The evidence is from macOS with a 10px border standing in for Linux's scrollbar gutter: main's call alone fails, the positioned call passes. No local Linux run. CI is the first.

  • The merge with main e5a70460 conflicted in 11 browser specs and dev/relay-broker.mjs. The specs keep this branch's UUID fixture ids and take main's new scroll helpers and fixed-base clocks. The broker keeps Discover saved identities across joined communities with names, pictures and retry #291's agent-inventory route and does not restore the removed read-state routes.

  • Since 9bfa2af3, three small repairs. sidebar-unread.spec.mjs holds reading focus before its explicit sidebar read (+2 lines). profiles.spec.mjs waits for the closing profile dock before its second Members click (+3 lines). In production code, unread.ts no longer starts the full sidebar walk that a cached roster would discard, which ran the walk twice after a reload.

  • The browser fake relay applies the reply rule. tests/browser/policy-relay.mjs answers with reason, the direct-parent conversation rule, undecided replies and filtered previews, and thread-unread.spec.mjs covers joined and unjoined threads.

  • CI at c70aec6a went red on three browser tests. Two are repaired here and one is main's.

    • inbox.spec.mjs "reveals an older exact unread anchor", WebKit: the test raced the app's 300 ms read dwell. A control proves it: a 1 second wait before Close fails 10 of 10. The repair (+15 / -2) separates the read arriving from the later reopen. With it, 40 of 40 per engine.
    • inbox.spec.mjs "an in-head DM keeps exact focus", Chromium: the focus loss fixed above. It was the product, not the test.
    • navigation-groups.spec.mjs:75, WebKit: a race in the test, with the same steps on main. The test holds a response itself and starts its 10 second wait before it releases the hold. It lost by 0.16 seconds. Not changed here. A repair (+7 / -3) is prepared as a test-only change for main.

Open reds and gaps, named

  • Three browser tests failed once each in the last full local run, at 618600f4. I judged all three test timing races, not effects of this PR, and did not change them here. All three passed on both engines in CI at this head. They can lose again. Repeat counts, no retries:
    • activity-navigation-focus.spec.mjs:97 (reply variant), Chromium. Passed 20 of 20 on rerun, at this branch and at the branch before the suspected change. A probe in the page shows why it can lose: opening a thread on an exact message moves focus to that row a median 37 ms after the panel shows, and the test's menu click lands at 70 to 100 ms. The menu closes when focus leaves it. The margin was as small as 24 ms.
    • inbox-revalidation.spec.mjs:18, WebKit. Passed 18 of 20 here and 39 of 40 on main e8c21196 with the same signature: a paused video's currentTime ends 5 to 17 ms past the value the test read inside pause(). Too few runs to say the rates differ.
    • timeline-setup.spec.mjs:221, WebKit. Passed 20 of 20 on rerun. Not reproduced. It loads no app code.
  • The first of those points at a real focus bug that main has too. The reveal that focuses the target row runs whenever the thread window finishes loading. On the fixture that is about 14 ms. On a real relay it is as long as the load takes, so a person can open a menu in that gap and have it close. The reveal code is the same as main's. Not measured on a real relay. Not fixed here. A test-only wait for it exists and is kept out of this PR.
  • dev/media-preparation.integration.test.mjs "tiled HEIC preparation preserves the full image dimensions" fails locally (expected 1536x1024, got 512x512). It is the only local Vitest failure at this head. No CI job checks it. The CI unit job has no ffmpeg, and the test returns early there (skipping real conversion: ffmpeg not found), so its pass in CI says nothing. What shows it is not from this PR: the converter, the test and the fixture are the same blobs as on main 8e371726, it failed the same way on this machine on clean main 75e6dc39, and this machine's ffmpeg 8.0.1 produces 512x512 from the fixture with no app code. It is main's test from feat(native): add community extras and media preparation #450. An issue is drafted.
  • An old DM that the feed admits beside a newer loaded head can show without a gap marker between them. By reading the source, main has the same path (store.ts is identical). Not reproduced on main. Not changed here.
  • navigation-groups.spec.mjs "Create new ... partial failure" timed out once in 30 instrumented WebKit runs: after a reload, fill("Follow-up") did not reach the field. Cause not established. Three assertions (focus, no menu, value after fill) were added as a diagnostic, not a fix. It passed on both engines in CI at this head.
  • Not covered by any passing test on WebKit: read behaviour when a mailto: link or a download keeps the page after beforeunload. Chromium covers both.
  • No matched performance comparison was run. No profile ran unchanged on both main and this branch (main's fixtures used label ids, this branch uses UUIDs, and the two select different thread traversals). Main's new scenario runner (feat(profile): run an unattended scenario file in web profiling #476) can drive one unchanged real-relay scenario on both. That run is a follow-up.

What to review

  • docs/unread.md and docs/inbox.md against the behavior above. If they disagree, the doc is the bug or the code is.
  • The four owners and nothing else holding read state: sidebar-api.ts (wire), sidebar-state.ts (projection and flush), sidebar-journal.ts (durable intent), unread.ts (selectors and intents).
  • The Inbox binding: subscribeInbox and the candidate selection in unread.ts, the context demand it shares with notifications in sidebar-state.ts, and the captured visit in InboxPage.tsx. Against main, inbox-feed.ts and inbox.ts change by +5 / -4 lines between them and InboxPage.tsx by +12 / -11.
  • That no local counter or fallback survives. At 2ebfeb79, buzz-read-state, readStateSnapshot, relay_decode_read_state, read-state-model and clearUnreadLocal have no hits under src, src-tauri or dev. Each has hits on main 8e371726.
  • The "Relevant replies" section of docs/unread.md against feat(relay): private read-state accessory API buzz#7906's docs/buzz-v1-read-state.md. Both must name the same four reasons in the same order.
  • Size against main 8e371726: production 48 files, +2,805 / -4,592. Tests and test fixtures 125 files, +12,581 / -9,337. Docs 7 files, +447 / -484. One workspace config line (+3).
  • Of that, binding the Inbox data layer to the relay was about 380 touched production lines (+343 / -39, measured at the first Inbox merge). About 280 lines of main's feed and types are kept as they are.

Evidence at the pushed head, 2ebfeb79

  • Contains main through 8e371726 (Allow plugins to unregister (kind 5) owner-managed agents through native services #536). Pushed as a fast-forward from c70aec6a, no force push. Main was still 8e371726 after CI finished.
  • Typecheck, lint, design types, the design-system guards and the build: all exit 0 at this exact commit.
  • Full Vitest on this exact tree: 7,119 pass, 1 fail (the tiled-HEIC test above), 1 skip. 508 files pass, 1 fails, 1 is skipped.
  • CI at 2ebfeb79, read per job: 21 jobs succeeded and Windows native validation is skipped. That is all 12 browser shards, both JavaScript jobs, Rust and tool integration, Native browser fixture, Browser measurements, CI required, DCO, Semgrep and zizmor. One shard, Chromium 6 of 6, needed a second attempt. On the first, its setup step failed downloading Hermit (HTTP 500 from GitHub releases) before any test ran. The rerun passed 86 of 86.
  • Browser journeys in CI at this commit, every spec file: Chromium 600 passed, WebKit 600 passed. No shard log shows a failed or retried test. The three tests that were red at c70aec6a and the five new focus cases passed on both engines.
  • Browser journeys locally at this exact commit, the nine files that exercise the changed CSS (every Inbox spec, both message-action specs, the scroll spec): 38 of 38 on Chromium and 38 of 38 on WebKit, one worker, no retries. The whole suite was not run locally at this commit. The last whole local run was at 618600f4.
  • Source review by Wren of every merge and repair on the way to this head, including each of the four commits since c70aec6a: no open blocker. Wren read the whole production diff against main 8e371726 in one pass at c70aec6a, against docs/unread.md and docs/inbox.md. Production code has changed since then by the five CSS lines only. These are source reviews. They ran nothing.
  • Paired live pass, the real app at this commit against a relay built from feat(relay): private read-state accessory API buzz#7906 ce9481aa6, Chromium and WebKit: both passed, one attempt each. It opened the Inbox on a real relay: a fresh thread mention got a row, the click revealed and focused the exact message, the thread read was applied and confirmed by a separately signed relay read, the thread's root stayed unread with no reason, the row left the list and the opened conversation stayed open. The sidebar counts, thread previews and reasons for a never-joined thread, an own-parent conversation, a mention and a DM passed too. No page or console errors. Each engine's log prints both commits before the run and the app commit again after it. The relay's reported source and its binary hash are checked at the start of each engine. One caveat: on Chromium the harness failed once to capture a response body, before the Inbox steps. Which request it was is not known. Every assertion passed without it. It is one scripted path, not the browser suite. It does not cover a cold-start DM, the 100-question limit or Drafts.
  • Clippy at this commit in CI (--workspace --locked --all-targets -D warnings): passed. It was not rerun locally. No Rust file has changed since c70aec6a, where it exited 0 locally. The branch's Rust change against main is seven files, nearly all deletion of the old read-state commands (+5 / -324).
  • Hooks. Every commit from 7b32c538 through c70aec6a ran the repository's staged checks under the pinned Lefthook and passed, and so did the fix and the Inbox docs commit since. I did not check the hook runs of the other two new commits (one docs, one test repair). 7b32c538 itself was made with the hook bypassed, because the machine's Lefthook was older than the repository requires. Its tree was checked by hand, and every later check ran on trees that contain it. On the pushing machine git push runs only the organisation's push check, not this repository's check-push group. Of that group's three steps, typecheck with the full unit suite and the design checks were run by hand at this commit, and Clippy ran in CI, as above.
  • Not covered by any run: the packaged desktop app, the OS keychain, Git storage, a relay with NIP-FI in Enforce.
  • The five review threads from 9/29 answered. Four are fixed in commits on this branch (d9180639, e814f374, 43428274, e15e1857). The fifth is item 9 above (old read positions and Mark-unread marks start over once). No code answers it. It is a product decision and it is open.
  • A person has used it.

Originating Buzz channel 6f773b94-34d0-4004-bec9-a100b6ad17d5.

Replace client read-state and activity counting with a bounded sidebar
projection, validated purpose-bound broker operations and durable read-intent
journal. Adapt sidebar, thread and notification consumers to relay evidence.

Preserve prefix dwell, fixed-anchor whole-channel reads, local manual unread,
access fencing and explicit retry. Group nested display demand within the
selector budget and replay access-roster changes without metadata churn.

Update unit/browser fixtures and ownership docs. Declare the root pnpm
workspace explicitly so run and install agree about fixture discovery.

Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Carl, an automated reviewer, commenting via Wes’s GitHub account.

Changes requested: five P2 findings inline. Reviewed frozen head 1b27a5cc92c20a4b485abc67c65083bbc0bcde78, not the unpublished main-integration work.

  • Evidence: focused production-source reproductions for all five findings, including the real local HTTP broker/transport boundary and isolated Chromium IndexedDB. No product edits. These probes are not a full app or native acceptance run.
  • Remaining gates: GitHub currently reports merge conflicts; visible hosted checks are Semgrep OSS, zizmor and DCO, not app integration CI. The PR already discloses the unpublished compatible relay extension and unfinished merged-tree/performance/human acceptance. I have not reclassified those disclosures or unexplained browser failures as new defects.
  • Exit criteria: address the inline defects with regressions, then validate the integrated app against the published matching relay contract and complete the disclosed acceptance gates before readiness. An intentional legacy local-intent reset requires explicit product approval.

Comment thread src/features/relay/http-admission.ts Outdated
Comment on lines +255 to +266
if (retryAfter && [429, 503].includes(response.status)) {
const hint = response.headers.get("Retry-After");
const duration =
hint && /^\d+$/.test(hint)
? Number(hint) * 1000
: hint
? Date.parse(hint) - Date.now()
: 60000;
failure.retryAfterMs = Number.isFinite(duration)
? Math.min(86401000, Math.max(1000, duration))
: 60000;
lane.pause(failure.retryAfterMs);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] Keep sidebar-specific 503s out of the shared API cooldown

This promotes every sidebar 503 (even without a retry header) into a pause of the same admission lane used by history queries and other relay HTTP operations. A sidebar-specific timeout or storage failure consequently disables otherwise healthy reads. I reproduced this through the production broker and transport: /me/sidebar returned 503 with Retry-After: 60; the immediately following history query() returned 429 with ~60 seconds remaining, without making any upstream /query request. Already queued work is also rejected by pump().

Preserve the retry hint for the sidebar owner, but do not pause unrelated operations for a non-quota 503. Keep shared backoff for the actual shared-quota refusal, and cover an unrelated query succeeding after an accessory failure.

Comment thread src/features/relay/sidebar-state.ts Outdated
Comment on lines +354 to +361
debounce = setTimeout(() => {
debounce = undefined;
const ids = [...dirty];
dirty.clear();
void (async () => {
await refreshTargets(ids);
await refreshContexts(new Set(ids));
})().catch(fail);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] Coalesce invalidations while a targeted refresh is in flight

Clearing dirty and dropping ownership of the launched refresh permits another targeted pass every 250 ms, while schedule() appends each pass to its unbounded serial promise chain. With request latency above that interval, a busy channel accumulates redundant reads ahead of context/notification reads and roster refreshes. The transport's concurrency limit cannot help: these jobs have not reached it, and their 10-second timeout starts only when dispatched.

A controlled-timer reproduction against this head held the first targeted request, fired six more invalidation windows for the same channel, then released it: seven targeted requests, rather than the initial request plus one coalesced follow-up. Keep a single in-flight target drain and retain dirty IDs for its next pass, as the context-refresh owner already does; add the held-request regression.

Comment on lines +95 to +101
function open() {
if (closed) return Promise.reject(new Error("Read journal closed"));
if (database) return database;
const promise = new Promise<IDBDatabase>((resolve, reject) => {
const request = indexedDB.open("buzz-sidebar-v1", 1);
request.onupgradeneeded = () =>
request.result.createObjectStore("partitions");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] Preserve durable local unread marks during the database cutover

Existing profiles saved explicit manual-unread intent in buzz-read-state-v1 (partitions[scope].localUnread). This opens only a new database, and the removed owner is the only reader of the old records. Upgrading therefore silently removes those reminders from the UI, despite manual unread remaining a durable, device-local feature. This is separate from the intentionally unsupported legacy relay-counting fallback.

In isolated Chromium IndexedDB, I seeded a valid legacy channel mark, then loaded the production new journal with the same scope: manual became [] and manual(channel) was false, while the old mark remained on disk. Add an idempotent migration for directly resolvable marks, retaining recovery data and surfacing any entries requiring resolution. A deliberate reset instead needs explicit product approval and user-facing disclosure, not an implicit empty journal.

Comment thread src/features/relay/unread.ts Outdated
Comment on lines +379 to +385
const message =
target.kind === "message" ? event(target.messageId) : undefined;
const resolved = message && context(message);
const lease =
resolved && message
? state.retain({ target: resolved, message_ids: [message.id] })
: undefined;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] Retarget retained message demand when ancestry changes

This lease captures context(message) once, but attention() and the message snapshot resolve it again on each read. A valid reply-only child can initially resolve to its unloaded immediate parent; when that parent arrives carrying the canonical root, the selector moves to the root while the retained query stays on the parent. subscribeMessages() has the same mismatch.

I reproduced this with signed child/parent events: attention was initially eligible, became unknown after parent arrival, and remained unknown after explicit refresh because the only query still targeted the parent. A notification candidate waiting on this lease can remain unknown until expiry; refreshing does not repair it. Reconcile retained targets with changing ancestry (including evidence loss), keeping selector lookup and demand ownership consistent, and cover late-parent arrival without requiring an unsubscribe/resubscribe.

Comment on lines +335 to +343
publish();
if (outcomes.some((o) => o.status === "unknown"))
throw new Error("Read acknowledgement unknown; retry available");
}
})()
.catch(fail)
.finally(() => {
flushing = undefined;
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] Track write health independently from projection refresh status

flush() sends failures into the same sync.status/error that refresh() owns, but a successful flush never clears that failure. I reproduced both orderings: (1) an unknown acknowledgement followed by a successful later flush leaves {status:"error", pending:0, error:"Read acknowledgement unknown…"}; (2) a write failing while traversal is held becomes {status:"ready", pending:1} with no error when the traversal completes.

The first marks otherwise usable projection evidence stale and makes notifications wait until a separate refresh; the second hides the failed-write/retry reason behind ordinary pending state. Keep write failure/recovery separate from read freshness, so an unrelated read cannot erase a write error and a completed successful flush clears its own error. Cover both interleavings.

Meli added 7 commits September 29, 2026 21:40
Integrate main d28b0d3, preserving its message action, thread, notification
and channel lifecycle behavior with relay-owned sidebar/read-state evidence.
Add fixed loaded-subtree mark_messages_read operands, bounded durable replay
and local visit forces without advancing unrelated context frontiers.
Keep message-status subscriptions stable and retain notification demand
through platform submission and sticky sound cancellation.

Reconcile UUID fixtures, signed strict thread-window bounds and action-menu
lifecycle assertions with the incoming client protocol. Update ownership
docs and regressions. Browser union still has one WebKit composer failure;
relay exact-message, current-main, performance and human acceptance remain.

Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Integrate main e304e3f while preserving the relay-sidebar cutover.
Use fixed prefix intents for selected-row reads; remove exact-message,
loaded-subtree and branch-unread aggregation pathways. Remove the unread
diagnostics panel while retaining local manual unread and the durable journal.

Capture staged/pending/applied read presentation without editing relay counts.
Fence reconciliation by request-start revision, require complete exact thread
evidence, and admit applied batches atomically with capacity recovery.
Add rendered races, storage ordering, and capacity failure-path witnesses.
Consume advertised eligible kinds in the browser transport and checkpoint
partial live/Recent presentation. Remove imported_at_ms account validation.

This is an incomplete local checkpoint, not a release candidate. Full Vitest
passed 451 files / 5399 tests with one opt-in live skip before two test-only
non-null assertions were replaced by explicit fixture guards for pre-commit.
Project staged hooks and TypeScript pass after that repair; no full rerun yet.
Manual pre-persistence behavior, live/Recent completion, latest-main integration,
browser/live acceptance and measured performance remain outstanding.

Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Stage transient manual edits in the existing journal owner, applying them in
invocation order over the durable set and removing them on settlement. Fence
invalidated edits and clear transient state on disposal. Remove the outer
per-channel mutation queue so newer actions can paint before older saves finish.

Add mounted badge ordering, rollback and restored-owner coverage plus held-write
lifecycle checks. Full Vitest passed 451 files / 5407 tests with one opt-in live
skip before commit; four isolated mutations hit the intended assertions. Browser
IndexedDB acceptance and complete sidebar feature integration remain outstanding.

Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Integrate app main a04aae5, preserving plugin-page sidebar navigation,
channel canvas controls, native media and community actions. Adapt mark-all
reads to relay unread presentation and device-local marks; preserve sequential
sweeps, skip revoked grants and continue past failures. Clear the departed
community's sidebar journal through its strict storage owner.

Remove main's native NIP-RS read-state adapter, signing/decode/publish commands
and associated tests; sidebar v1 remains the read-state authority. Preserve
native sidebar, media and other identity commands. Add adapted sweep/purge
regressions and document the community behavior.

Source-only merge checkpoint: mandatory staged checks pass; no full suite run
yet because the shared heavy lane is occupied. TypeScript reports three missing
navigation helper exports in main-identical entity-navigation.test.tsx; unrelated
navigation source is retained unchanged. Earlier manual-slice green does not
validate this merge. Feature completion and browser/native acceptance remain open.

Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Select channels using the journal's effective manual set, including valid
transient edits, rather than the exact channel-target selector. Reuse the same
projection for exact-target and channel-ownership queries without changing
badge semantics or relay counts. Cover saved and held message/thread marks at
exact-zero relay unread, including immediate and durable clearing.

Full Vitest on these bytes: 5653 passed, three baseline failures, one live skip.
Restoring old candidate selection adds exactly four intended failures. The
bounded repair passed independent source/evidence review; full candidate remains
incomplete and the baseline navigation/HEIC failures are not waived.

Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Integrate bfe4c7f, including working-agent navigation and migration of entity
navigation tests away from removed link helpers. Preserve the new popover
sections, hover/keyboard behavior, explicit thread-read action and focus routes.
Retain lazy sidebar activity hydration and incomplete-evidence disclosure while
adopting main's removal of per-thread count text. Adapt fixtures to ReadCount,
UUID channel identities and the sidebar journal database.

TypeScript passes after source resolution. Full Vitest at this merge head is
next; browser/native acceptance remains outstanding. The named host ffmpeg tiled
HEIC failure is not fixed or waived in this change. Overall feature is incomplete.

Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Keep in-flight thread items in the existing popover until mark-through settles,
preserving keyed pending/error actions and the existing success focus handoff.

Adapt working-agent browser IDs, select the profile journey's custom channel,
and replace its removed Diagnostics action with ordinary message mark-through.
Advertise the model's existing eligible kinds through its sidebar descriptor.

Reviewed R2 bytes: 20/20 affected browser tests in Chromium/WebKit; TypeScript
passes. Full Vitest: 5663 pass, 1 tiled-HEIC host-tool failure, 1 skip. Old popover
fails the unchanged storage-gated focus assertion in both engines. Wren bounded
review: 9/9/9. Broader feature and live/native acceptance remain open.

Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Meli and others added 20 commits September 30, 2026 17:48
Integrate the embedded conversation thread surface and artifact publication
admission/conflict handling from main. Keep repair work as the next checkpoint.

Local integration checkpoint only; browser and review repairs follow separately.

Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Use real read menus and canonical channel identities. Preserve explicit permission
failure coverage with exact request/error accounting. Exercise focused embedded
thread reading leases instead of removed channel visits.

Separate bounded newest-window opening from user-demanded thread scrollback and
label changed profile metrics. Remove retired branch unread-count rendering,
stale subscription mocks and obsolete unread bounds documentation.

Local checkpoint, not acceptance: bounded file receipts are in workspace research.
Latest full Vitest summary: 5674 pass, one baseline HEIC failure, one opt-in skip.
Typing and held-pagination browser failures, six correctness findings, full browser
validation, paired measurement and live acceptance remain open. No push intended.

Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Recapture the visible reply when the reader scrolls while an older page is
pending. Keep explicit jump-to-latest intent ahead of that capture.

Adapt navigation history cases to strict newest windows and demanded scrollback.
Assert preserved row identity/offset within native whole-pixel scroll precision;
cover keyboard jump, page release and live arrival before the fallback timer.

Both complete browser files pass in Chromium/WebKit (22 tests). Original-anchor
and paused-jump negative controls fail their respective contracts. This local
slice does not establish full-package or merged-head readiness.

Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
A sidebar 503 with Retry-After paused the one API lane that history,
profiles and search share, so a sidebar outage stopped healthy reads for
as long as the header said. That shared pause was also the only thing
pacing the sidebar's own retries.

Now the delay stays with the request that was refused:
- A 503 on an opted-in route keeps its Retry-After on the error and no
  longer pauses the shared lane. A 429 still does, because that is the
  relay's one API quota.
- The sidebar owner holds its read lane or its write lane until the
  relay's Retry-After has passed. The lane that was not refused keeps
  running.

Tests were written first and failed at 927f431 (5 failures: broker 503,
two admission 503 cases, read pacing, write pacing). Full Vitest at this
tree: 5,679 pass, 1 skip, 1 fail, the HEIC dimensions test that also
fails on clean main. tsc clean.

Hooks: lefthook 2.1.3 refuses to run (2.1.12 required), including in
prepare-commit-msg. I ran scripts/check-staged.mjs by hand (exit 0) and
committed with -c core.hooksPath=.githooks --no-verify.

Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>

Integration: repository .githooks/pre-commit ran with pinned Lefthook 2.1.12;
existing custom hooks remain enabled. Source patch unchanged from 21937ab.

Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Every 250 ms window of live traffic started its own targeted sidebar
read. Behind one slow response they queued: four busy windows meant four
more reads of the same channels, each delaying the traversal and context
reads waiting behind them.

Now one targeted read runs at a time. Channels invalidated while it is in
flight wait in the dirty set and share a single follow-up, which starts
250 ms after the first one finishes. Reads still start at most once per
window, so a fast relay is not asked more often than before.

The regression test is Meli's, written first: it held one response
through four windows and saw five targeted reads where two are correct.
I changed one step of it: the follow-up is now awaited after its 250 ms
window instead of immediately, and the test asserts that it waits. Her
three lifecycle cases (dispose, clear, purge during a held read) passed
before and still pass.

Full Vitest at this tree: 5,683 pass, 1 skip, 1 fail, the HEIC dimensions
test that also fails on clean main. tsc clean.

Hooks: lefthook 2.1.3 refuses to run (2.1.12 required), including in
prepare-commit-msg. I ran scripts/check-staged.mjs by hand (exit 0) and
committed with -c core.hooksPath=.githooks --no-verify.

Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>

Integration: repository .githooks/pre-commit ran with pinned Lefthook 2.1.12;
existing custom hooks remain enabled. Source patch unchanged from 5b01c46.

Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Reconcile subscribed message targets when verified evidence or access changes.
Release changed leases before reacquiring so shared selectors remain bounded.
Keep existing state leases as the request/result lifetime owner.

Cover late-parent discovery, parent eviction, shared consumers, unsubscribe
and revoke during held reads, and retarget/overflow/recycle at 1000 leases.
Original product fails all three retarget assertions with the final tests.
Combined full Vitest: 5688 pass, one known HEIC failure, one skip.
Wren source/receipt review: 9/9/9 for this bounded slice only.

Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
…e marks

Invoke every selected channel read before awaiting journal saves, preserving
click-time cuts and local operation order through the existing serial journal.
Collect settled results without stopping other channels on a failed save.
Reject missing or cross-channel manual message targets before changing state;
unloaded thread roots remain accepted. Clarify sweep capture timing in docs.

Cover captured cuts, later unread choices, queued revoke/regrant and lifecycle
fences, later grants, incomplete cuts, and invalid message targets. Sami supplied
the target regression tests from 4de2e550, preserved unchanged.
Full Vitest: 5698 pass, known HEIC failure, one skip. Original product fails
four sweep assertions and two target assertions. Wren reviewed both slices9/9/9.

Co-authored-by: Sami <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
A failed read acknowledgement used to set the same status and error that
the sidebar traversal owns. Two things went wrong:

- The failure marked a usable projection as errored, so notifications
  waited and the "Couldn't refresh recent activity" notice appeared,
  until some later traversal happened to succeed. A later applied write
  did not clear it.
- A traversal that finished after the failure erased it, leaving an
  ordinary pending count with no sign the write needed a retry.

Write failures now go to their own writeError. They leave the read
status alone, a read can neither set nor clear them, and the next flush
that completes clears them. Existing write-failure tests assert the new
field.

Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>

Integrated with repository pinned Lefthook and existing custom hooks.

Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
The indicator subscribed to every channel with one shared callback. The
unread owner keeps listeners in a set, so the callback was registered
once, and unsubscribing the first channel to leave the roster removed it
for all the others. A local unread mark on a remaining channel then
never reached the dock badge.

Use one subscription for the session instead. The unread owner already
notifies every listener on any count, mark or access change, and its
snapshots already hide channels the viewer cannot read, so the
per-channel bookkeeping, the roster subscription and the membership
filter were all repeating what it does.

The detach tests now change a relay count on the retired session. An
ingested message never projects, so the old assertion passed with the
old listener still attached.

Co-authored-by: Sami <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>

Integrated with repository pinned Lefthook and existing custom hooks.

Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Remove the redundant allowed-channel conjunct from reading validity after
tracing session access revocation to purge, which retires handles and epoch.
Preserve creation admission and manual-revision guards.

Integrate Sami e40db666 public/private leave regression and positive control.
Use another channel flush as a completion barrier, not a quiet timer.
Sami reports original and allowed-only-deletion controls pass; removing
all lifetime fences fails both leave arms, including the isolated barrier.

Integration typecheck and repository hooks pass. Full combined package
validation is deferred behind the reported 22 GiB disk floor.

Co-authored-by: Sami <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Restores the property main pinned in "selection/prefetch do not read" at
the v1 guard, useReading's active(): a channel selected from the sidebar
allocates no reading lease until the timeline itself takes focus, and a
mounted timeline with no client rects never reads even while focused.

Mutation evidence (use-reading.ts active()): replacing
element.contains(document.activeElement) with true fails the sidebar test;
replacing element.getClientRects().length > 0 with true fails the
not-displayed test, which no existing test caught.

Hook note: lefthook 2.1.3 < required 2.1.12 refuses to run; the hook's
only pre-commit job (bin/node scripts/check-staged.mjs) was run by hand
and passed before committing with hooks bypassed.

Integration note: historical hook/mutant notes above describe Sami’s original
commit. This integration runs pinned repository and custom hooks.

Co-authored-by: Sami <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Signed-off-by: Sami <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
…ading

Restores the property main pinned in MessageManagement "waits for
confirmed membership before entering a restored channel visit". v1 has no
enterChannel; the guard is createUnread's allowed() (unread.ts:127-133),
which requires an uncached channel whose members include the viewer.
With a restored cached/read-only channel, or a listed channel the viewer
is not a member of, reading() throws and a lease taken earlier under
confirmed membership writes nothing; once membership is confirmed the
same observation produces exactly one mark_through.

Mutation evidence (unread.ts:132): dropping `!c.cached` fails the cached
case; dropping `c.members?.includes(viewer)` fails the outsider case.
Neither was caught by unread.test.ts or the rest of this file. Dropping
only the allowed() re-check in the lease's valid() survives because
observe()'s event()/context() lookups apply allowed() again; removing all
three fails both cases, so the fenced-lease arm is not vacuous.

Hook note: lefthook 2.1.3 < required 2.1.12 refuses to run; the hook's
only pre-commit job (bin/node scripts/check-staged.mjs) was run by hand
and passed before committing with hooks bypassed.

Integration note: historical hook/mutant notes describe Sami's original
commit. This integration runs pinned repository and custom hooks.

Co-authored-by: Sami <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Signed-off-by: Sami <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Restores the message-level half of main's MessageManagement "toggles actual
unread state immediately without a dialog". The v1 title "toggles relay
unread after acknowledgement" waits for acknowledgement, so the immediate
half was unpinned. With the local journal save held and relay writes
never answering, Mark read clears attention().unread while nothing is
saved or sent, and Mark unread forces it while the manual list is still
empty on disk.

Mutation evidence: dropping the `saving` term from sidebar-state.ts
covered() fails only this test in this file; dropping
manualEdits.set(...) from sidebar-journal.ts presentManual() fails this
test and the three held-manual-mark rejection cases.

Hook note: lefthook 2.1.3 < required 2.1.12 refuses to run; the hook's
only pre-commit job (bin/node scripts/check-staged.mjs) was run by hand
and passed before committing with hooks bypassed.

Integration note: historical hook/mutant notes describe Sami's original
commit. This integration runs pinned repository and custom hooks.

Co-authored-by: Sami <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Signed-off-by: Sami <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Integration note: historical hook/mutant notes describe Sami's original
commit. This integration runs pinned repository and custom hooks.

Co-authored-by: Sami <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>

Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Blocked/invalid outcomes arrive after the action resolved and are recorded
on the unread and thread-activity snapshots, which the bundled UI does not
render; the returning unread count is the feedback. A full journal rejects
explicit actions visibly, while automatic reading stops saving silently.

Signed-off-by: Sami <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
The Activity popover showed the raw content of each thread's newest
unread reply: an agent reply rendered its JSON envelope and an edited
reply rendered its original text.

Previews now go through the message fold the timeline already uses.
Opening the popover also reads the listed replies' edits, so a cold
start shows the author's latest edit, not only edits this session
happened to observe. A reply the fold does not present keeps its raw
content.

Co-authored-by: Sami <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
The unread contract promised that a preview shows a reply as the
timeline presents it, with the author's latest edit. The read behind it
is bounded: it takes the newest 500 edits across the listed replies and
does not read deletions, so a preview can show an older edit, the
original text, or a deleted edit. The contract now says so.

The preview test's relay now answers only what the read asks for, so
dropping the edit read fails the cold-start preview itself, not only
the request-shape assertion.

Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
The unread contract said a deleted edit still shows in a preview,
because the preview does not read deletions. That was wrong. The relay
soft-deletes the target of a deletion and excludes soft-deleted events
from the checked filter and by-id reads, and the reader keeps no cache, so the read made when
the popover opens does not return a deleted edit.

The contract now states the real limits: a preview is as of its last
open, and the read takes the newest 500 edits. The preview test opens
the popover a second time after the relay stops returning the edit and
expects the reply without it.

Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
The held-mark test disposes the owner before releasing the save, so the
journal's commit-time validity check in markUnread was never exercised:
deleting it failed no test. Queue the mark behind a held read and leave
the owner open, as main's "queued $markAction stays invalid" did.

Signed-off-by: Sami <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
In v1 the app does not repair counts from deletions; it rereads the
channel and the relay counts live rows. Nothing pinned that reread:
restricting unread.accept to eligibleKinds failed no test. Deliver a
live kind 5 and kind 9005 deletion carrying h and expect one targeted
read for that channel and the count going from 1 to 0.

Signed-off-by: Sami <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Eva and others added 8 commits October 1, 2026 15:10
Brings in #492 (browser tests wait for menu and wheel completion) and
#486 (membership hints confirmed with exact channel reads).

One hand resolution, in tests/browser/sidenav-polish.spec.mjs: the
import lines keep this branch's `ids` fixture export and take main's
`wheel` helper. Every other file merged without conflict.

Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Brings in #494 (hardened pinned browser CI setup and native fixture
provenance).

One hand resolution, in docs/contributing.md: both sides added a
paragraph after the Playwright image upgrade note. Both are kept
verbatim, this branch's page-error watcher note first and main's setup
verification paragraph after it. Every other file merged without
conflict.

Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
…lick

profiles.spec.mjs:524 closes the profile panel with Escape and then
clicks Members. The panel dock keeps its grid column until its closing
transition finishes, so the Members button is still at its split-layout
position when the click starts. If the dock unmounts between mousedown
and mouseup, the button moves under the pointer, the click is lost, the
dialog never opens and search.focus() times out.

Wait for [data-panel-dock][data-closing] to be gone before clicking, as
message-actions.spec.mjs already does for the same reason. The earlier
expect(panel).toHaveCount(0) does not cover it: it passes while the dock
is still closing.

The dock code is the same on main, so the race exists there too.
Whether this branch makes it more likely is not established. With the
dock's unmount held to land inside the click, the unchanged spec fails
6 of 6 across Chromium and WebKit and passes 6 of 6 with this wait.
Diagnosis and patch by Sami.

Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Brings in #496 (polish for media controls, panel headers and menus): 60
files. None are under src/features/relay, and in the two sidebar
components it touches (ChannelSidebar.tsx, SidebarSection.tsx) it adds
menu icons only.

No hand resolution. Six files change on both sides and git merged each
one cleanly: ChannelSidebar.tsx and MessageManagement.tsx (main adds
menu icons), and the channel-tabs, message-actions-floating,
sidenav-polish and thread-video browser specs. In each of the six, this
branch's change and main's change are line for line what they were
before the merge.

#496 also edits message-overlays.spec.mjs and VideoPlayer.tsx, where
message-overlays.spec.mjs:385 failed on hosted WebKit at 4b814f5. This
branch changes neither file, so main's versions come in as they are.

Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Merge pinned main 69a9af2 into the sidebar client. Keep relay authority
rather than restoring the client conversation lookup store.

Decode unread reasons, use unread for thread summaries, and suppress
unclassified plain-reply hints. Retire live hints on lower-bound responses.
Retain bounded fresh live notification selectors until classification,
using existing refresh ownership and the original event expiry deadline.
Keep selected-message manual marks unchanged.

Cover reason mapping, unknown settling/requery, broadcast upgrades, expiry
and access/session disposal. Browser conflicts retain the compiling branch
baseline; fake-relay semantics and inherited browser rule cases are a
separate follow-up owned by Sami.

Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Remove classified candidates from waiting before admission while keeping
context demand alive until the service installs its own observation.
Ignore callbacks after handoff. Cover bounded capacity and observation
failures with one admission, one error and released context demand.

Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Merge pinned main 75e6dc3 without
conflicts, retaining the sidebar API and notification handoff changes.
Apply Sami's reviewed fake-relay patch 0656be55ce7ff20de1df6ab8f864bb67269c38056a74e80378fd4a5594edadb6:
direct-parent relevance, reason responses, uncertainty and filtered previews;
joined/unjoined controls and channel catch-up reply protection.

Co-authored-by: Sami <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
All three are test faults. No production code changes.

thread-unread.spec.mjs, both engines:
- The own-reply barrier expected category "thread". Since the relay
  decides relevance, attention() takes the category only from an unread
  message's reason, and the viewer's own reply has none. The barrier
  still requires the reply to be indexed under its root and not unread.
- The strict fitting-thread case expected replies + 1 rows. The file's
  threadUnreadJoined fixture adds the viewer's older reply, which the
  fake relay serves in thread windows, so strict shows one more row.
  The count stays exact and the extra row is asserted by name. The
  legacy case fulfils its own list and is unchanged.

notifications.spec.mjs, WebKit:
- The exact-row journeys asserted "no read yet" after polling for the
  opened status in real time. On a slow runner the ordinary 300 ms
  reading dwell finished first: in the CI trace the row took 1.4 s to
  gain focus and the read was sent 54 ms before the status was sampled.
  The test now pauses the clock before the
  click, advances frames until opened, asserts no write and no pending
  journal entry, then resumes for the dwell. With the dwell set to 0 the
  preserved assertion fails in both variants. The unused
  clickToOpenedMs measurement is removed.

thread-unread diagnosis and patch by Sami. notifications diagnosis and
patch by Meli.

Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Meli and others added 21 commits October 2, 2026 13:03
Merge c9946be into b2f8d23, preserving thread timing barriers and feed
preview-completeness admission order. Reuse the verified session cache,
relay context owner and local journal for the narrowed Inbox export.

Bound subscribed Inbox demand to 100 selectors with notification priority.
Expose thread-only read steps, frozen channel read operands/manual keys,
and successful local-intent revision. Adapt session tests and document
current-only admission, incomplete coverage and unsupported exact reads.

Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Retain channel invalidation watermarks until a covering sidebar response;
exclude exact-zero attention only while the owner is current and has no
unread live hint. Invalidate admitted evidence before publishing candidates.

Use advertised eligible kinds, skip final unavailable and unaskable targets,
and keep unresolved snapshots stale without an error. Remove unaskable
targets before applying the selector cap and document these semantics.

Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Keep per-row attention absence proof independent of sync status; outstanding
channel invalidations still fence it and owner status controls freshness.
Try notification retention before yielding Inbox leases, retrying only after
a capacity refusal. Document the proof and demand boundaries.

Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Use the existing retain refusal catch without matching error text. Record
that 100 newer foreign messages in attention channels can displace an older
mention from the bounded Inbox candidate set.

Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Integrate Sami’s combined regression patch. Make fixture attention unknown
by default and cover advertised kinds, final unavailable answers, selector
release and priority, held refresh/admission, and the 100-candidate limit.

Co-authored-by: Sami <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Use the oldest group member for both messageId and rootId while preserving
latest-member target and readThrough semantics. Integrate Sami’s three
navigation regressions, incomplete-zero hint coverage and real queued
traversal gate.

Co-authored-by: Sami <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Remove eight chat attributions while retaining the documented behavior.

Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Preserve relay-owned unread intents and removed native read-state commands
while integrating writer confirmations, header actions and agent authorization.
Union browser navigation helpers with UUID fixtures, retain real read menus,
and repair stale selectors outside conflict hunks. Keep both sidebar tests
and the Activity handoff clock guard.

Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Import #528 action-bar readiness and its browser controls unchanged.
Adapt three incoming message-action channel IDs and two existing startup
barriers to the UUID fixture contract.

Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Replace the retired unread hold/release helpers with the current sidebar
API fixture gate, including release in finally. Preserve all assertions.

Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Integrate main Inbox conversation surfaces, shared message presentation and
native sidebar register changes. Preserve a selected visit when its unread
row disappears, leaving access and deletion presentation to existing readers.
Migrate incoming Inbox unit and browser fixtures to relay sidebar authority
and UUID channels; document unread-only listing and click versus dwell reads.

Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Integrate Drafts with scoped composer recovery and attachment cleanup, mounted
hidden unread cues and jump controls, and main's channel-template and relay
query fixes. Preserve captured Inbox visits independently of unread rows.

Migrate incoming Drafts unit fixtures to sidebar verdicts and strict UUID
thread windows. Restate Drafts browser coordinates with shared fixture IDs
and wait for enabled Send before keyboard focus. Retain hidden/inert cue
assertions and relay-authoritative Inbox documentation.

Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Integrate the shared composer typing context, typing pill layout and motion
coverage, media review fixture, and updated agent runtime revision.

Resolve the Drafts keyboard helper with main's enabled-before-focus comment
and wait while retaining relay fixture UUIDs. Preserve the auto-merged typing
and activity geometry assertions alongside existing relay-backed fixtures.

Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Integrate paged Canvas revision history, preview and confirmed restore using
the existing session profiles and Canvas save path. Retain incoming dialog,
capability and draft-preservation coverage and documentation.

Use shared UUID fixture IDs in the two signed Canvas history browser events
so the incoming scenario addresses the relay-backed channel correctly.

Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Remove the duplicate early thread_window handler so strict requests use the
existing depth-limit owner. Preserve displaced joined replies, canonical
root selection, cursor ordering, signed bounds and auxiliary-event closure.
No product code or test assertions change.

Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Retain UUID fixture imports alongside main Bestie opt-in hooks.
Integrate enterprise relay discovery and explicit bundled plugin defaults.
No additional production or browser-fixture changes.

Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Integrate owner-managed agent deletion through native relay services
and its native IPC/outbox tests. No browser or manual merge edits.

Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Describe the bundled Inbox page and relay-backed snapshot/context gates
instead of the removed marker merge and local-only readiness split.

Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Wait for the first arrival's applied read intent before closing its reader.
Use a second arrival after Close for the exact focused reopen assertion,
retaining the selected-anchor survival and delete/navigation checks.

Co-authored-by: Sami <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Keep the focused message wrapper visible while Virtua measures its new
index. Match focus below the list item so top-layer action controls count
without adding a focus recovery owner.

Gate Inbox history to exercise mixed insertion after exact focus. Cover
action and media controls, intentional focus transfer, Escape, and
addressed withholding with five native-browser regression cases.

Co-authored-by: Sami <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Distinguish exact target closure from bounded channel-head repair.
Document unaddressed DM and conversation previews that can retain pre-edit
text after context refresh, and separate list eligibility from captured
detail body freshness.

Co-authored-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz>
Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
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.

2 participants