Repository navigation
fix(reminders): alert on the Reminders page when the message's channel refuses the alert - #3066
Conversation
…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>
29af4ad to
576d611
Compare
wesbillman
left a comment
There was a problem hiding this comment.
🤖 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
left a comment
There was a problem hiding this comment.
🤖 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
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,
notificationAuthorizedrefused 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. Theshownguard 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.submitrefuses the page target too: closed service, missing viewer, stale category, viewer mismatch, theseendedupe (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.mdnow says so in the gate paragraph.Commits
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.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 realsoloConversationgate expects 0 banners; "a listed channel without membership" expects 1 banner opening the page), and adocs/buzz-help.mdbullet.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 --noEmitand Biome clean.src/buzz-mcp/tools.test.ts(reads buzz-help.md) passes.notificationAuthorizedadmit every conversation target fails the two flipped rows on the click assertion; removing only themembersclause fails exactly "a listed channel without membership"; makingsoloConversationadmit pages fails exactly the Focus row.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
Notes for reviewers
authorizedfails would keep this policy in the host.viewerMemberchange is compatible, the fallback follows whatever the access check decides.🤖 Generated with Claude Code