From 9941df1524b6d326dbab69e9c69aebc6787b3354 Mon Sep 17 00:00:00 2001 From: Dylan Jeffers Date: Thu, 16 Jul 2026 11:58:30 -0700 Subject: [PATCH 1/2] fix(notifications): send blast push notifications again MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs prevented chat blast push notifications from being delivered. 1. The dmNotificationMaxAgeMs guard (1h) dropped every blast push. Blast recipients all carry the blast's own created_at rather than a per-recipient time, so once a blast has been working through its audience for longer than maxAgeMs, every remaining recipient permanently fails the check. Blasts are now exempt from the age guard; regular DMs and reactions still honor it. 2. getNewBlasts was missing parentheses around the cursor condition. Because AND binds tighter than OR, the predicate parsed as (chat_allowed(...) AND $cursor IS NULL) OR (to_user_id > $cursor), so chat_allowed() was bypassed entirely once the cursor went non-null — blasts could be sent to users who had not allowed them. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/notifications/src/tasks/dmNotifications.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/notifications/src/tasks/dmNotifications.ts b/apps/notifications/src/tasks/dmNotifications.ts index 25756cd..3f432bf 100644 --- a/apps/notifications/src/tasks/dmNotifications.ts +++ b/apps/notifications/src/tasks/dmNotifications.ts @@ -200,7 +200,7 @@ async function getNewBlasts( FROM blast JOIN chat_blast_audience(blast.blast_id) USING (blast_id) WHERE chat_allowed(from_user_id, to_user_id) - AND ?::INTEGER IS NULL OR to_user_id > ? + AND (?::INTEGER IS NULL OR to_user_id > ?) ORDER BY to_user_id ASC LIMIT ? ) @@ -342,12 +342,18 @@ export async function sendDMNotifications( // Only send notifications that are not too old (avoids flood after plugin downtime). // Cursor still advances for all so we don't reprocess old ones next tick. + // Blasts are exempt: every recipient carries the blast's created_at, so a + // blast that takes longer than maxAgeMs to work through its audience would + // have all remaining recipients dropped. + const blastSet = new Set(blastNotifications) const maxAgeMs = config.dmNotificationMaxAgeMs const sendCutoff = maxAgeMs > 0 ? new Date(Date.now() - maxAgeMs) : null const toSend = sendCutoff === null ? notifications - : notifications.filter((n) => n.notification.timestamp >= sendCutoff) + : notifications.filter( + (n) => blastSet.has(n) || n.notification.timestamp >= sendCutoff + ) const skipped = notifications.length - toSend.length if (skipped > 0) { logger.info( From 276f7efadd7b0b09b5cfa5e435cc27acc181d3e0 Mon Sep 17 00:00:00 2001 From: Dylan Jeffers Date: Thu, 16 Jul 2026 12:34:55 -0700 Subject: [PATCH 2/2] fix(notifications): alias blast created_at as timestamp getNewBlasts selected created_at bare while DMNotification declares a timestamp field, so notification.timestamp was undefined for every blast. discoveryDB.raw returns any, so tsc never flagged the mismatch. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/notifications/src/tasks/dmNotifications.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/notifications/src/tasks/dmNotifications.ts b/apps/notifications/src/tasks/dmNotifications.ts index 3f432bf..214b6d5 100644 --- a/apps/notifications/src/tasks/dmNotifications.ts +++ b/apps/notifications/src/tasks/dmNotifications.ts @@ -204,7 +204,7 @@ async function getNewBlasts( ORDER BY to_user_id ASC LIMIT ? ) - SELECT blast_id, from_user_id AS sender_user_id, to_user_id AS receiver_user_id, created_at FROM targ; + SELECT blast_id, from_user_id AS sender_user_id, to_user_id AS receiver_user_id, created_at AS timestamp FROM targ; `, [effectiveBlastId, userId, userId, config.blastUserBatchSize] )