Skip to content

Count unread replies only in conversations you are part of - #471

Merged
loganj merged 9 commits into
mainfrom
larry/nested-thread-unread
Oct 1, 2026
Merged

loganj merged 9 commits into
mainfrom
larry/nested-thread-unread

Conversation

@loganj

@loganj loganj commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

🤖

Summary

  • Unread badges now follow the conversations you are part of. Before, any unread reply anywhere in a channel counted, and one post from you anywhere in a thread made every nested branch under it unread for you.
  • New top-level posts in your channels still count until you read them.
  • A reply counts only when it answers your message, or when you also replied to the same message. A nested thread under someone else's reply stays quiet until you post in it.
  • Mentions of you, DMs and broadcast replies always count, at any depth.

Example

You post a message. An agent replies to you: unread. Someone starts a nested thread under the agent's reply: quiet. You reply in that nested thread: new replies there are now unread.

Details

  • One rule feeds every unread surface: channel badges, the channel Activity popover, the "new replies" dot on thread buttons, per-message attention and the thread notification category. The per-thread-root "participants" set is replaced by two sets built in the same indexing pass: messages you wrote, and messages you replied to. Checking a reply is two lookups by ID; there is no tree walk.
  • The app keeps a bounded window of recent messages (4,096 events / 8 MiB). Main already had a gap here: your post in a thread could be outside the window. Now, when an unread reply's conversation cannot be decided from loaded messages, the app asks the relay once per parent message, in background batches of up to 50 from one channel: the parent and the reply's root by ID, and your replies to that parent in the same channel.
    • A full page of your replies is split and asked again, and a single busy parent pages back in time, so one busy thread cannot hide your reply to another message.
    • Lookup results live in their own bounded store. They never count toward a badge, never fill the window, and survive the window's overflow reset. Fetched parents never start more lookups, so a lookup cannot walk up an old thread.
    • Thread attention follows the direct parent, so an agent's reply to your old nested reply is thread attention even if its root is old. The fetched root keeps grouping and navigation working.
    • While a lookup is queued or running, the reply is quiet, and a live notification for it waits instead of being dropped. A reply you have already read is never looked up, so it never holds a notification. A failed lookup retries with backoff (1 s, doubling to 60 s).
    • Lookups are keyed by channel and parent. A reset or access change during a lookup discards its answer.
    • Deleted replies of yours do not count, even when you delete them after the lookup has decided.
  • Explicit "mark unread" on a message still wins over the rule.
  • docs/unread.md and docs/notifications.md describe the new rule.

@loganj
loganj force-pushed the larry/nested-thread-unread branch 4 times, most recently from 79a6e31 to 3c652a0 Compare October 1, 2026 16:32
Larry added 4 commits October 1, 2026 13:17
A reply is unread only when it answers the viewer's message, or the viewer
also replied to the same parent. Nested threads under someone else's reply
stay quiet until the viewer posts there. Mentions, DMs and broadcasts still
count. When retained evidence cannot decide membership, look it up once per
parent from the relay instead of guessing.

Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
The unread fixture now makes the viewer author the first thread root. The
links journey holds the viewer's profile until its linked message is focused,
so a viewer-authored root started that hold at startup and could outlive the
profile read deadline on slow runners. Keep that journey's roots peer-authored.

Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Address the PR review of the out-of-window conversation lookup:
- keep fetched parents and roots in a separate bounded store that never
  counts, never fills the window and survives its overflow reset;
- split a full aggregate page, and page one busy parent back in time;
- decide thread attention from the direct parent and fetch the root for
  grouping and navigation;
- hold live notifications while the lookup is pending;
- scope membership to the reply's channel, honor deletions, never ask a
  parent twice at once, and retry failures with backoff.

Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Address the re-review of the out-of-window conversation lookup:
- a lookup in flight during a cache clear or access change discards its
  answer and re-asks only the parents the reset kept; an aborted read
  from the reset is not counted as a relay failure;
- report `pending` only while a lookup is queued or running, so a read
  reply cannot hold a notification candidate that nothing will settle;
- key lookups by channel and parent, so a reply in another channel that
  tags the same parent cannot block or share the answer;
- record which of the viewer's messages made a lookup positive, and end
  the membership when the viewer deletes all of them after the lookup.

Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
@loganj
loganj force-pushed the larry/nested-thread-unread branch from 3c652a0 to d999933 Compare October 1, 2026 17:19
@loganj
loganj marked this pull request as ready for review October 1, 2026 17:48
@loganj
loganj requested review from a team, comp615 and wesbillman as code owners October 1, 2026 17:48

@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.

One change requested: lookup-backed membership can survive deletion of the viewer’s old reply on another client, leaving unrelated replies marked unread and notification-eligible (inline).

Star Lord automated source review via Wes’s account. Head d999933e9d665a8a4960a6137e06ed5d83c4c005, base e5a70460a9a3423464e7ee97dede8ccecfd69f10. Source-only: no tests, builds or app execution. Hosted CI required passed in one snapshot; Windows validation was skipped, and native/UI retry-focus behavior remains unverified.

Comment thread src/features/relay/unread.ts Outdated
A positive lookup kept only the IDs of the viewer's messages that made it
positive. When the window did not hold such a message, a deletion from
another client could not resolve its target through the session's
visibility check, so it never reached unread and the membership survived.

Keep those verified messages in a bounded store (4,096, not counted
evidence, dropped on access loss) and expose them to the existing
deletion-visibility lookup. Evicting one forgets the lookups it backs,
so they are asked again instead of outliving an unobservable deletion.
The regression test no longer loads the old reply before deleting it.

Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@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.

One change requested: witness eviction can miss in-progress lookup results and leave completed memberships without deletion evidence (inline). The ordinary cross-client deletion path from the previous review is addressed.

Star Lord automated source review via Wes’s account. Head 019849036068ef14a9612b4d479c66258c101c69, base e5a70460a9a3423464e7ee97dede8ccecfd69f10. Source-only follow-up: no tests, builds or app execution; CI was still running in one snapshot, Windows validation was skipped, and native/UI retry-focus behavior remains unverified.

Comment thread src/features/relay/unread.ts Outdated
…e bound

A lookup batch witnessed every reply of the viewer before it installed
the batch's results. Eviction only scanned installed lookups, so a batch
that returned more than 4,096 of the viewer's replies could evict the
witnesses of its own pending decisions. Those memberships were then
installed without deletion-visible evidence, and a deletion from another
client could not end them.

Each lookup now keeps one witness, the viewer's newest message for that
parent, and records whether the viewer has others. The lookup is
remembered before it is witnessed, so eviction always sees it. Deleting
the witness ends the membership, or asks the relay again when the viewer
has other messages there. Witnesses can no longer exceed the lookup bound.

Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@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.

One change requested: choosing a newer reply as the sole witness can hide deletion of a cached viewer-authored parent, which is then reused to restore membership (inline). The previous in-progress eviction finding is addressed.

Star Lord automated source review via Wes’s account. Head 6943099676790a7fcd20480ed4af6f9f517a4381, base e5a70460a9a3423464e7ee97dede8ccecfd69f10. Source-only follow-up: no tests, builds or app execution; CI was still running in one snapshot, Windows validation was skipped, and native/UI retry-focus behavior remains unverified.

Comment thread src/features/relay/unread.ts Outdated
… visible

When the viewer wrote both a parent and a newer reply to it, the lookup
witnessed only the reply. The parent stayed in the structural cache but
was not deletion-visible, so another client's deletion of it was dropped.
After the reply was deleted too, the follow-up lookup reused the cached
parent and restored the membership.

Prefer the viewer's own parent as the witness, keep the first witness
instead of replacing it with a later message, and drop a deleted message
from the lookup cache so a follow-up lookup fetches it again.

Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
…ures

The session-level regression signed more than 5,000 events. That took
about 8 seconds locally and pushed the JavaScript CI lane past its
10-minute timeout. It also queued parents one per poll, so it did not
build the single multi-parent batch it described.

Move it to a direct unread fixture. Events are marked verified rather
than signed, as the transport does after verification, and all ten
parents are asked in one turn. One ID fetch proves the shared batch. The
test fails on 0198490, where the membership survives, and runs in about
100 ms.

Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@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.

No changes requested in this test-only follow-up: the replacement queues all ten parents together, exercises the real deletion-visibility gate, and preserves signed session-level deletion coverage.

Star Lord automated source review via Wes’s account; head 9cd80cde90a74907c4f2d76fe9a3144dc4430386, base e5a70460a9a3423464e7ee97dede8ccecfd69f10.

Source-only: no tests, builds or app execution; prior CI confirms the 10-minute timeout and 18.4-second removed case, but current CI was still running at the snapshot and the replacement’s runtime/full-suite improvement remain unverified.

Two session-level lookup tests each signed 4,096 flood events to
overflow the evidence window. They took about 14 seconds each on CI and
made unread-startup.test.ts the slowest file in the JavaScript lane,
which is now timing out at its 10-minute limit.

Move them next to the witness-bound test in a shared direct-unread
fixture (unread-lookups.test.ts). The fixture marks events verified, as
the transport does after verification. The assertions are unchanged.
Disabling the live-reply witness still fails the moved test.

Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@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.

No changes requested in this test-only follow-up: the moved cases retain the overflow/refresh and no-repeat-lookup assertions, exercise the real unread owner, and leave signed-session deletion, access and failure/retry coverage in place.

Star Lord automated source review via Wes’s account; head 3f90ee873a41c9cbeb55187c35a18afe967bfd0c, base e5a70460a9a3423464e7ee97dede8ccecfd69f10.

Source-only: no tests, builds or app execution; prior hosted logs confirm the two cases took 14.35 s and 13.95 s and that JavaScript exceeded its 10-minute limit, but current CI was still running at the snapshot, so the new runtime/full-suite improvement remains unverified.

@loganj
loganj merged commit 69a9af2 into main Oct 1, 2026
37 of 39 checks passed
@loganj
loganj deleted the larry/nested-thread-unread branch October 1, 2026 20:13
bostonaholic added a commit to bostonaholic/buzz-app that referenced this pull request Oct 1, 2026
…page-icon

* origin/main:
  Count unread replies only in conversations you are part of (block#471)
  Animate the terminal welcome with a compact hex wordmark (block#508)
  Use top tabs in the new-tab picker (block#505)
  Polish media controls, panel headers, and menus (block#496)
  harden pinned browser CI setup and native fixture provenance (block#494)
  perf(relay): confirm membership hints with exact channel reads (block#486)
  test(browser): wait for menu and wheel completion (block#492)
  fix(links): render one hash on completed channel links (block#506)
  ci: publish Windows and Linux alongside macOS previews (block#491)
  fix(channels): keep conversations open through archive and restore (block#452)
  feat(channels): align create and edit forms with draft protection (block#482)
  Test provider connections before model selection (block#500)

Signed-off-by: Matthew Boston <mboston@squareup.com>
bradseiler pushed a commit that referenced this pull request Oct 2, 2026
…delegate

* origin/main:
  Fix Pi and Goose environment overrides (#517)
  feat(ui): Switch shared icons to Tabler (#523)
  Improve message media contrast and thumbnail fill (#521)
  Document unified inventory and verify focused import and compact-row acceptance (#293)
  fix(ux): clarify Pi installation and setup errors (#511)
  Count unread replies only in conversations you are part of (#471)
  Animate the terminal welcome with a compact hex wordmark (#508)
  Use top tabs in the new-tab picker (#505)

Signed-off-by: bb-expert <51cde9442e46eac81c83ec71552708c3b0cb96a88ff8e1581d8fb4de281afbf3@buzz.block.builderlab.xyz>

# Conflicts:
#	src/features/relay/unread.test.ts
johnmatthewtennant pushed a commit that referenced this pull request Oct 2, 2026
* origin/main: (21 commits)
  Add remote agent owner attestation fn to host service (#532)
  fix(build): pin Rust 1.98.1 to unblock macOS 27 agent builds (#529)
  main fix: Pi model test ETXTBSY flake (#513)
  fix(composer): remove phantom text-field focus outlines (#519)
  fix(relay): use writer reads for channel confirmations (#501)
  feat(channels): unify header actions and inline details editing (#487)
  Show app-managed agents working in the sidebar (#539)
  Add recoverable hosted community deletion (#403)
  Add verified Inbox evidence and exact edit closure (#495)
  Animate Buzz startup through initial content readiness (#534)
  Polish profile avatar picker and custom colors (#533)
  Add Send to channel for authored thread replies (#531)
  Allow plugins to send managed-agent registration events (kind 30177) (#535)
  Fix Pi and Goose environment overrides (#517)
  feat(ui): Switch shared icons to Tabler (#523)
  Improve message media contrast and thumbnail fill (#521)
  Document unified inventory and verify focused import and compact-row acceptance (#293)
  fix(ux): clarify Pi installation and setup errors (#511)
  Count unread replies only in conversations you are part of (#471)
  Animate the terminal welcome with a compact hex wordmark (#508)
  ...

Signed-off-by: Sol <49aa1f65411fd096d2e2ec144f1e7aa36fdc76d1b907cfdf7be000c66f9d3b8e@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