Skip to content

fix(reminders): alert on the Reminders page when the message's channel refuses the alert - #3066

Merged
matt2e merged 3 commits into
mainfrom
lost-reminders-1
Nov 9, 2026
Merged

matt2e merged 3 commits into
mainfrom
lost-reminders-1

Conversation

@matt2e

@matt2e matt2e commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

Summary

A reminder set on a message in a channel you later leave (or are removed from) never alerted. At the due time the plugin submitted only the message target, notificationAuthorized refused it because the viewer is no longer a listed member, and the reminder was already marked shown, so it went silent for good. The Reminders page listed it under Overdue with no channel name and no explanation.

This PR keeps the message target as the first attempt and, when the notifications service refuses it, resubmits the same alert (same sourceKey, title and body) with the Reminders page of that community as the target. The shown guard is still set synchronously before the first submit, so nothing is retried later and the existing no-replay behaviour on relaunch and session swap is unchanged.

Behaviour change: a due reminder in a channel you aren't in now alerts on time with "Reminder due" and the saved preview, and clicking the banner opens Reminders in the reminder's community. Two matrix rows in index.test.ts ("missing membership", "an empty roster") flip from 0 to 1 banner accordingly. A due reminder in a channel you are in still opens the message.

Why it is safe

Every other refusal path in NotificationsService.submit refuses the page target too: closed service, missing viewer, stale category, viewer mismatch, the seen dedupe (its key ignores target kind), the pending cap, the dev pause, alerts off, the Reminders category off, permission denied, and the Focus gate (which refuses page targets). So the page alert can only appear when the message was refused for channel access. A future gate that allowed pages but blocked conversations would be routed around; docs/plugin-architecture.md now says so in the gate paragraph.

Commits

  1. fix(reminders): alert on the Reminders page when the message's channel refuses the alert — the 10-line plugin change, README Details bullet, new test and the two flipped rows. Every banner-producing row now clicks the banner and asserts what opens.
  2. test(reminders): pin that the page fallback respects gates and membership — comment stating the refusal invariant, the plugin-architecture sentence, two more matrix rows ("missing membership, Focus on" through the real soloConversation gate expects 0 banners; "a listed channel without membership" expects 1 banner opening the page), and a docs/buzz-help.md bullet.
  3. docs(reminders): say the alert opens Reminders when you aren't in the channel — help wording covering channels you were never in.

Verification

  • TZ=UTC vitest run src/bundled/reminders src/features/notifications src/bundled/focus src/bundled/readmes.test.ts: 24 files, 322 tests pass. tsc --noEmit and Biome clean. src/buzz-mcp/tools.test.ts (reads buzz-help.md) passes.
  • On main with only the tests applied, the new test and the two flipped rows fail.
  • Mutation checks: making notificationAuthorized admit every conversation target fails the two flipped rows on the click assertion; removing only the members clause fails exactly "a listed channel without membership"; making soloConversation admit pages fails exactly the Focus row.
  • Reproduced in the real desktop app (Linux dev build under Xvfb with dunst banners, two local relays, throwaway keys). macOS banners were not exercised; the change is renderer-side and platform-independent.

Demo

Control on the fixed build (still in the channel), then the viewer leaves the channel with main on the left and this branch on the right, then the Reminders row close-up. The banner is the existing OS notification; only where a refused alert goes has changed.

demo-control-and-side-by-side.mp4

Shorter side-by-side of just the left-channel flow, main left and fix right, aligned on the due time:

side-by-side-before-after.mp4

Due time, side by side: main shows no banner and the Reminders dock count is 1; the fix shows the "Reminder due" banner with the saved preview

The same banner on the fixed build: "Reminder due · #release-planning" while still a member, plain "Reminder due" after leaving (the channel is no longer in the viewer's list)

Fixed build, left channel: with community Acme selected and the banner up, clicking it switches to Beacon, the reminder's community, and opens its Reminders page with the Overdue row

The Reminders row for the left-channel reminder on main and on the fix: identical Overdue row, preview, Due at, sender avatar and menu. The page is unchanged by this PR; only the alert differs

Notes for reviewers

  • Already-missed reminders are not recovered on upgrade: the plugin checks only reminders due after its saved check time, so an old silent miss stays under Overdue without an alert.
  • A preview from a private channel the viewer was removed from now reaches the OS notification surface. The same text was already on the viewer's Reminders page.
  • Follow-ups, not in this PR: explain "you're no longer in this channel" on the Reminders row; a host-side fallback target used only when authorized fails would keep this policy in the host.
  • Overlaps: AIDA-2346: Fix spoiler preview review regressions #2285 and feat(reminders): add the /remind slash command #2075 have trivial textual conflicts in the same submit/mock lines; BUZZ-615: Separate viewer membership from peer rosters #2700's viewerMember change is compatible, the fallback follows whatever the access check decides.

🤖 Generated with Claude Code

@matt2e
matt2e marked this pull request as ready for review November 9, 2026 06:13
@matt2e
matt2e requested a review from a team as a code owner November 9, 2026 06:13
matt2e and others added 3 commits November 9, 2026 06:56
…l refuses the alert

A due reminder whose message is in a channel the viewer has left, or was
never in, never alerted. The plugin marks the reminder shown before it
submits, and notification access refuses conversation targets in
channels that aren't listed with the viewer as a member. The refused
submit resolved false and was dropped. The saved check time then moved
past the due time, so the reminder never alerted again, in that session
or after a relaunch. Reminders listed it under Overdue with no channel
and no explanation.

When access refuses a message reminder's alert, the plugin now submits
it again with the same source key, title and body, targeting the
Reminders page in that community. Clicking the banner opens that page.
Page targets need only community membership, so the channel-access rule
still decides whether a click can open the message. Alerts off, the
Reminders category off, Focus and the development pause refuse page
targets too, so the fallback can't get past them. Denied permission
still drops the alert at delivery. The service's de-duplication key
ignores the target kind, so a reminder whose message alert was admitted
can't come back as a page alert. `shown` is still set before the
submit, so nothing is retried later and turning alerts back on still
replays no backlog.

Tests:
- New: "opens a reminder's notification on its page when its channel
  refuses the message". A fake submit admits only page targets. The
  test expects a message attempt and then a page attempt with the same
  text, and no third attempt after a same-community session swap.
- The catch-up matrix rows "missing membership" and "an empty roster"
  go from 0 banners to 1. They were negative controls for the roster
  wait, not a decision that left channels stay silent.
- Every matrix row that shows a banner now clicks it and asserts what
  opens: the Reminders page for those two rows, and the message in
  "room" for member rows. This runs through the real
  NotificationsService and notificationAuthorized.
- Two new rows, a left channel with the Reminders category off and a
  left channel with permission denied, expect no banner from the
  fallback.
- The mount() fake submit now resolves true, matching
  Promise<boolean>. launch() hands the service a copy of navigation
  with a fake open, because the real navigation object is frozen.

The Reminders README gains a Details note. docs/buzz-help.md has no
Reminders entry on main to extend.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: mtoohey <mtoohey@squareup.com>
…ship

Follow-up to the review of the Reminders page fallback. Production
behaviour is unchanged.

The fallback resubmits on any refused submit, not only an access
refusal. The comment above it now states why that is safe. Every other
refusal also refuses the page target: a closed service, a missing or
different viewer, a stale category, the `seen` key (which ignores the
target kind), the pending cap, the development pause, alerts or the
category off, and the Focus gate. Denied permission drops both at
delivery. A future gate that allows pages but blocks conversations would
be routed around. docs/plugin-architecture.md now says, in the gate
paragraph, that Reminders re-targets a refused message alert to its
page, so a gate that should silence reminders must refuse page targets
too, as Focus does.

Tests, in the catch-up matrix:
- "missing membership, Focus on": a test plugin registers Focus's real
  soloConversation gate. The page alert is refused, so no banner shows.
  If that gate admits page targets, this row fails with 1 banner.
- "a listed channel without membership": the channel list includes the
  channel but leaves the viewer out of its members. The banner opens
  Reminders. Discovery never lists such a channel today, because list()
  holds only rosters that tag the viewer plus pending Solo chats that add
  them. So the row uses a synthetic list. It is the only test that fails
  when the `members` clause is dropped from notificationAuthorized.
- Launch gains `focus` and `listed` options. The plugin loader serves
  the Focus test plugin by manifest id.

docs/buzz-help.md: a bullet next to the reminder Spoilers note says a
reminder notification opens Reminders instead of the message if you are
no longer in its channel when it comes due.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: mtoohey <mtoohey@squareup.com>
… channel

The buzz_help bullet said a reminder notification opens Reminders if you
are "no longer in" the message's channel. The fallback also covers a
channel you were never in, so it now says "aren't in its channel",
matching the Reminders README ("a channel you aren't in").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: mtoohey <mtoohey@squareup.com>

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

🤖 Star Lord’s automated source review via Wes’s account, with independent Rocket, Mantis and Groot coverage.

No actionable findings in the reviewed change at 576d6110faa8895b40fc657e87d35d7ac4da6c8e against target base 40e6ccb597cf9d8b47f9692de8285458cc304017 (merge-base 42a761725a6837d239ff6806bb2a699fa2d74075). Checked notification admission/deduplication and lifecycle, current authorization/preferences/Focus gates, saved-preview handling, scoped page navigation, and regression assertions. The fallback preserves the source key and community scope; accepted candidates do not retry after later delivery failure. Target-base changes do not intersect the relevant runtime paths.

The disclosed saved-preview exposure on the OS surface and the documented requirement for custom gates to reject page targets are intentional behavior changes. The current bundled Focus gate rejects the fallback page. No additional defect was established from those changes.

Validation limits: source review only; no tests, builds or apps run, and no CI verification. The test’s click handler spies on navigation; actual switching was traced separately in source, not exercised. All four attached images were visually inspected, including older build labels 67cd426/8005bb5; both videos remain uninspected. Author test/mutation/Linux-demo claims were not independently reproduced, and native banner, sound, focus and click behavior remain unverified. This is a COMMENT review, not approval.

@kazemek21 kazemek21 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 Kyle reviewed this PR and the Nexus review-tracker notes and approved with no comments.

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

@matt2e
matt2e merged commit 5c18e73 into main Nov 9, 2026
60 checks passed
@matt2e
matt2e deleted the lost-reminders-1 branch November 9, 2026 21:48
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