Skip to content

feat(notifications): notify on all activity in chosen channels - #2956

Open
LordMelkor wants to merge 3 commits into
block:mainfrom
LordMelkor:clod/channel-all-activity
Open

LordMelkor wants to merge 3 commits into
block:mainfrom
LordMelkor:clod/channel-all-activity

Conversation

@LordMelkor

@LordMelkor LordMelkor commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

Problem

A channel notifies you only about mentions, DMs and replies in your own conversations. There's no way to follow a channel closely: an ordinary post in a channel you're in gets no notification category and never alerts. The only per-channel control is Mute.

Change

  • All activity per channel. Choose Notify for all activity from a channel's menu, beside Mute. Every post and reply by someone else in that channel then alerts, as the new channel category. Stop notifying for all activity goes back to the default. Other channels keep the default.
  • Settings → Notifications → Channels with all activity lists these channels by name, with Turn off. A Channel activity switch under Notify me about turns these alerts off without clearing the list.
  • Same rules as other alerts. Nothing while you're viewing the conversation, nothing once it's read, nothing from your own posts or agent-only posts.
    • Mute wins. So does Unfollow thread, for that thread's replies.
    • One alert per post. A post that mentions you, DMs you or replies in your conversation alerts once, with its existing category. A local Solo run's reply leaves its completion alert as the only one.
  • Read state. The unread engine's attention gains an optional activityUnread: like unread, but a reply outside your conversations counts, except in a thread you unfollowed. It decides only whether an all-activity alert is still worth showing. No count, badge or Inbox item changes.
  • Storage. The list is saved on this device, per community and account (notify-channels view state). It's read again at delivery, so turning a channel off cancels an alert that's still waiting.
  • messages.ts moves its existing per-message checks into one shared classify helper, so the new rule sits beside the others. Most of that file's diff is re-indentation.
  • Documented in docs/notifications.md, docs/unread.md and docs/buzz-help.md.

No FOUNDATION file is changed.

Intent

Original request: kray's #buzz-feedback post (Nov 2): "Would be great to be able to customize notifications: eg. channel activity vs mentions, posts by certain people". This PR is the "channel activity vs mentions" half. "Posts by certain people" is #2939.

Must change: a channel can be set to notify on all activity (any top-level post or reply by someone else in it), next to the existing Mute. Default stays "mentions and my conversations". Device-local, like the people list. One category channel, one rule in notifications/messages.ts; the toggle lives where Mute is (channel menu); Settings lists the channels so they can be turned off.
Out of scope: synced cross-device preference, unread/badge/Inbox changes, per-thread modes, keyword alerts, digests, changing Mute.
Rules: existing gates still apply (global off, category off, audience=agents, own messages, history/replay, read/visible). Mute wins over all-activity. A post that already alerts (mention, DM, thread, person) alerts once with its existing category.
Acceptance: with all-activity on for channel A, someone's ordinary post in A alerts; the same post in channel B (default) doesn't; muting A silences it; a post that mentions me in A alerts once.

Validation

  • messages.channel-activity.test.ts:
    • A reply whose lookup finds no root still alerts.
    • Turning the channel off cancels an alert that's waiting on a local Solo run.
    • Unfollowing a viewed thread cancels its waiting alert, with Notify while viewing on.
    • An ordinary post alerts in an all-activity channel and not in a default one, and a mention alerts once.
    • Mute silences all activity. A mention in the muted channel still alerts, as before.
    • A reply outside your conversations alerts, unless you unfollowed its thread.
    • Turning the channel off cancels an alert that's still waiting.
    • A local Solo run's reply leaves its completion alert as the only one.
  • NotificationSettings.test.tsx: the list shows channels by name, turns one off, and reports a failed save.
  • ChannelInventory.test.tsx: the menu item turns all activity on and off beside Mute, and reports a failed save.
  • Preferences, desktop and service tests: the new category round-trips and has a sound.
  • The regressions fail without the change. tsc --noEmit and biome check pass. Full suite at a2462aaa, on main 41e64bf0: 19,436 passed and 3 failed, in 3 files this diff doesn't touch (browser-host/vite-config, App.profile, codex-sh). Run alone, those files pass 65/65, so the failures are timeouts on a heavily loaded shared machine.
  • Review rounds (each finding checked against the frozen intent above):
    • Round 1 (sq agents review and Sigil): sq found three P2s and Sigil two, one of them the same. All are fixed.
      • A reply whose conversation lookup finished without finding a root waited until it expired. Found by both.
      • Turning a channel off, or muting it, didn't cancel an alert that was waiting on a local Solo run.
      • Settings didn't report a failed Turn off.
      • With Notify while viewing on, unfollowing a viewed thread didn't cancel its waiting alert. Sigil reproduced this one.
    • Final sq agents review at a2462aaa, after rebasing onto main 41e64bf0: no findings.
    • Rebase review (sq agents review) at 2a40a34e, after squashing and rebasing onto main b1436b09: two P2s, both fixed in e8550cae. Each has a test that fails without its fix. Production change: +14/−9.
      • With Notify while viewing on, a post you were already caught up through was dropped before delivery could see that it was in view. The read decision now belongs to delivery alone. An already-read post you aren't viewing stays quiet.
      • A broadcast reply could alert while its root was still being looked up, even though you had already read that thread. The reply now stays pending until that lookup ends. If the lookup finds no root, the reply alerts once as ordinary activity. No unread count, badge or Inbox value changes.
        Full suite at e8550cae: 20,531 passed, 0 failed.
    • Second rebase (2026-11-12) onto main f10e7771, squashed to one feature commit (ad1abee1). Main's new sound-fallback test now includes this PR's channel sound.
      • Rebase review (sq agents review): one P2, fixed in 09c0c206 (+11/−3 production lines). The window can evict a nested reply's thread root, and the finished lookup was never repeated. The reply then had no root, so it counted as channel activity even in a thread already read. attention() now recovers the evicted root through the existing structural lookup before deciding. Unread counts and badges are unchanged. The new test fails without the fix.
      • Final sq agents review at 09c0c206: one P2 in this PR's older delivery code, which the fix doesn't change. Per the review stop rule it's recorded as a known limitation, not fixed here: messages.ts:260, "Resolve viewed replies before choosing channel activity". Ordinary activity waits only on a running lookup, not on an unknown conversation status. So with Notify while viewing on, a reply in a viewed thread can alert as channel activity before its thread category is known.
      • Full suite at 09c0c206: 21,251 passed and 25 failed, in 7 files.
        • Four of those files don't overlap this diff and pass when run alone, so those failures were timeouts on the overloaded shared machine: InboxPage, WorkflowLanding, MessageReactions and store-system-discovery.
        • browser-host/vite-config builds Vite, isn't touched here, and times out at 30 s even when run alone under that load.
        • The last two, unread-follows and unread-startup, were a real regression from 09c0c206.
      • That regression, fixed in ea7d8d22 (+10/−1 production lines). Per the stop rule it gets one fix and no new review round.
        • Cause: the recovery also asked again for roots that the conversation lookup had already fetched, after the 1,024-event structural cache dropped them.
        • Effect: with many threads, each refetch evicted another root, so lookups never settled. A cross-channel reply was also left pending.
        • Fix: the recovery now runs only when the reply's conversation lookup didn't fetch its root, for example because the window held the root then.
        • Tests: all src/features/relay/unread* and src/features/notifications files pass (20 files, 561 tests), including both regressed tests and the new one.
      • Known limitation: past 1,024 fetched roots, a root its lookup fetched and the cache then dropped isn't fetched again. That thread's read frontier then doesn't apply, so a reply in it can count as channel activity. Refetching is what never settled.
    • Pressure test: an independent agent (Sigil) reproduced the unfollow and missing-parent defects with the real relay, unread and notification owners. At the round-1 fix head (f219e18c, now a2462aaa after a clean rebase) both probes pass. It checked missing-parent delivery, cancelling a waiting alert on unfollow, turn-off and mute precedence, and the Settings failure message, and found nothing else. 552 tests, TypeScript and Biome pass. Native notification delivery wasn't exercised.

Try it (AGENTS.md step 3, for a human)

  1. Open a channel's menu and choose Notify for all activity.
  2. Have a teammate post in that channel while you're looking at a different channel. Expected: a notification.
  3. Have them post the same message in another channel you're in. Expected: no notification.
  4. Mute the first channel and have them post again. Expected: no notification.
  5. Unmute it and have them post a message that mentions you. Expected: one notification, as a mention.
  6. Go to Settings → Notifications → Channels with all activity. Click Turn off, and the next post doesn't notify.

🤖 Generated with Claude Code

@LordMelkor
LordMelkor requested a review from a team as a code owner November 7, 2026 02:09
@morgmart

morgmart commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

🤖 Design review: this isn't critical path for the Nov 10 launch. Please hold it until after launch, or build it as an external plugin that isn't bundled into the app. Questions: #buzz-design-review.

@kazemek21

Copy link
Copy Markdown
Contributor

Instructions to get the build to run: buzz://message?channel=5909b3f1-fbe6-4f63-9cd2-06440e58b4a1&id=41538c84fc77a64cc0d124407b31805e3d485d331654a91b5185d62dc9245b05&thread=71a8d710754540d5c562226840389021badb3398752eb76d821d2a5b6c2b16ba

@LordMelkor
LordMelkor force-pushed the clod/channel-all-activity branch from a2462aa to e8550ca Compare November 10, 2026 16:17
kazemek21 added a commit that referenced this pull request Nov 10, 2026
## TL;DR
Captured explicit reads now check the current saved manual-unread
journal inside the replacing transaction, so a newer local
choice—including one saved by another window—is not erased. Retries
retain their original cut; the additive
`unread.prepareThreadRead(channelId, rootId)` captures only retained
incoming replies without reading the root or advancing the channel.

The fence also preserves earlier same-window queued manual actions,
authorizing their revisions only after a successful save whose prior
same-key intent is also captured. An earlier queued save cannot mask an
intervening unread choice from another window. It leaves reading-lease
ownership, passive observations, remote-marker ordering, journal format
and relay protocol unchanged.

Mark all now reserves one shared saved revision cut before queued
writes. Each predicate retains only the revision, not a full journal
copy.

### Validation
- Shared-cut correction: pinned Linux Blox run of both complete owner
files passed 383 tests (174 read-state; 209 unread). The per-sweep
capture-budget and capture-failure regressions fail on the prior
implementation; older queued-save controls pass. No new browser cases.
- Linux Blox, pinned tools: both owner files passed 366 tests on the
initial head. After review fixes, `pnpm exec vitest run
src/features/relay/unread.test.ts` passed all 206 tests. The final
queue-ordering fix passes all 174 tests in
`src/features/relay/read-state.test.ts`, typecheck and the commit/push
hooks.
- Pre-fix channel regressions: six failures; bypassing the new
transaction fences makes eleven targeted concurrency regressions fail.
Replacing canonical thread selection with parent-edge traversal makes
the narrow-thread regression fail. Restoring the implementation passes
both owner files.
- Three successful-save interleavings fail before the final fix and pass
afterward, with three matching controls: channel, thread and message
marks overwritten by an older queued save after another window marks
unread.
- Eight additional regressions fail before the review fixes and pass
afterward: root/reply deletion before or after persistence, and shared
sidebar hints with absent, present or cross-window forced-message
overlays.
- Touched-file Biome, `pnpm typecheck`, `pnpm author:build`, strict
generated-SDK consumer compilation and repository commit/push hooks.
- Deterministic fixture gates cover newer channel/thread/message marks,
second-window saves at the transaction boundary, queued ordering, retry
cuts, later arrivals, independent root/thread markers and
access/lifecycle expiry. No browser tests were added or removed.

Generated type-only SDK declarations expose the additive method through
the existing `UnreadCapability` export; existing channel and message
APIs remain compatible. Capture requires initialized read-state storage.
The thread API does not discover history; receipts remain device-local.
A narrow read leaves the shared sidebar hint until channel open
reconciles it, since that hint has no subtree ownership. Its shared
message-force fence is conservative: a newer mark elsewhere in the same
channel can also reject the save. This does not establish native runtime
behavior, identical remote-tuple ordering or atomicity through an
operation's eventual return.

Related independent work remains with #2060 (quiet Solo/Inbox), #2610
(selected-message UI), #3181 (passive recovery), and #2939/#1666/#2956
(notification/audience behavior). This change is limited to the explicit
mutation/preparation seam.

Fixes NEXUS-4426

[Initiative
status](https://g2.sqprod.co/apps/kylez-buzz-improvements-status)

*Posted by Codex (gpt-6.1-sol) on behalf of kylez.*

Signed-off-by: Kyle Zemek <kylez@squareup.com>
Co-authored-by: Codex <noreply@openai.com>
kazemek21 added a commit that referenced this pull request Nov 12, 2026
## TL;DR

This supports retained readers that need to undo an explicit read action
without overwriting newer unread intent. It offers Undo only for saved
local-unread markers removed by that action.

## How

Undo checks the current journal revision inside the storage transaction
and revalidates access, visit, and captured-message evidence. It
restores only markers removed by that exact saved action.

## Boundary and risk

Undo never restores forced-message overlays or rewinds or publishes read
frontiers. Results omit Undo if the completed read also cleared
forced-message overlays, or if force work was queued while the read
saved.

The handle expires after any later journal save, even if unrelated or
quiet. This whole-journal lifetime is deliberately conservative. Older
hosts may omit the optional handle.

## Related PRs

This change is independent of
[#3264](#3264),
[#2918](#2918),
[#2939](#2939),
[#1666](#1666), and
[#2956](#2956).

Generated with Codex (gpt-6-luna) on behalf of kylez

Signed-off-by: Kyle Zemek <kylez@squareup.com>
Co-authored-by: Codex (gpt-6-luna) <noreply@openai.com>
LordMelkor and others added 3 commits November 12, 2026 11:26
A channel's menu, beside Mute, can now set it to notify on every post or
reply by someone else; Settings lists those channels to turn off. The new
`channel` category applies only when no other category does, mute and
thread unfollow still win, and the device-local set is read at delivery.

Co-authored-by: Clod <c7b21872e9ddb79ca62967d0e21e6a4518a669eb04648e963cdfb2426e62fafb@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: LordMelkor <kray@squareup.com>
…annel activity

A reply's thread read frontier applies only once its root is known. After a
window reset, a decided reply whose root was dropped skipped that frontier, so
an already-read thread's reply could still count as channel activity. Asking
for attention now fetches such a root (after the conversation lookup, which
already fetches it for an undecided reply) and holds the alert until it lands.

Co-authored-by: Clod <c7b21872e9ddb79ca62967d0e21e6a4518a669eb04648e963cdfb2426e62fafb@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: LordMelkor <kray@squareup.com>
…etched it

The conversation lookup already fetches a reply's root. Past the structural
cache's 1,024 events, asking for an evicted root again only evicted another,
so many threads' lookups never settled. The recovery now runs only for a
reply whose lookup did not fetch its root, such as one the window held then.

Co-authored-by: Clod <c7b21872e9ddb79ca62967d0e21e6a4518a669eb04648e963cdfb2426e62fafb@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: LordMelkor <kray@squareup.com>
@LordMelkor
LordMelkor force-pushed the clod/channel-all-activity branch from e8550ca to ea7d8d2 Compare November 12, 2026 18:09

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants