Skip to content

OUT-3927 | Send comment & reply email notifications to IUs - #1376

Merged
arpandhakal merged 2 commits into
feature/iu-emailfrom
OUT-3927-send-comment-emails-to-ius
Jul 6, 2026
Merged

OUT-3927 | Send comment & reply email notifications to IUs#1376
arpandhakal merged 2 commits into
feature/iu-emailfrom
OUT-3927-send-comment-emails-to-ius

Conversation

@arpandhakal

Copy link
Copy Markdown
Collaborator

Summary

Implements OUT-3927 — internal users now receive email (not just in-product) for new comments and thread replies, reusing the existing CU comment/reply templates. Stacked on the IU-email milestone branch.

  • Comment job (send-comment-create-notifications.ts): IU recipients now get email, routed through the existing 5-minute grouped buffer (consistent with CU comment emails). Gated on isIuEmailEnabled() so it stays prod-safe until OUT-3929 ships the platform preference.
  • Reply job (send-reply-create-notifications.ts): the IU initiator branch now includes the email delivery target alongside inProduct, also gated on isIuEmailEnabled().
  • No template changes — the existing Commented copy is reused, per the ticket.

Also resolves a deferred routing landmine

Flipping the comment IU call to email: true is exactly what would have tripped the old ?? !email inference in buildNotificationDetails ("has email ⇒ client"), which inverts once IUs get emails — it would have routed IU comment emails to recipientClientId (an IU id in a client field). Fixed at the root:

  • createBulkNotification now takes an explicit isRecipientIu opt; recipient type is declared by the caller rather than inferred (the same Commented action fans out to both CU and IU recipient lists, so it can't be derived from the action).
  • buildNotificationDetails uses isRecipientIu directly — the email-absence fallback is gone.
  • Completion callers pass isRecipientIu: true explicitly.

Note for reviewers

IU reply emails are immediate, not grouped — the reply job dispatches directly for both CU and IU recipients, so I preserved that behavior (CU replies weren't grouped either). Only the comment path is buffered. Say the word if IU reply emails should also be batched; that's a larger change.

Test plan

  • yarn tsc clean
  • notification + jobs suites green (147 tests), incl. new IU-comment routing coverage and a regression test proving an email-enabled comment no longer misroutes without the explicit flag
  • With IU_EMAIL_ALWAYS_ENABLED=true: comment on a task → IUs with access get a grouped comment email; reply in a thread → prior IU participants get a reply email
  • With the flag unset → no IU comment/reply emails (prod-safe); CU comment/reply emails unchanged

🤖 Generated with Claude Code

Implements OUT-3927. IUs now receive email (not just in-product) for new
comments and thread replies, reusing the existing CU comment templates.

- comment job: IU recipients now get email (gated on isIuEmailEnabled()) via
  the grouped buffer, consistent with CU comment emails
- reply job: IU initiator branch now includes the email delivery target
  alongside inProduct, gated on isIuEmailEnabled()

Removes the deferred routing landmine (was tracked for this ticket): the
`?? !email` inference in buildNotificationDetails assumed "has email => client",
which inverts once IUs get emails. Flipping the comment IU call to email:true
would have routed IU comment emails to recipientClientId. Now:

- createBulkNotification takes an explicit isRecipientIu opt; recipient type is
  declared by the caller (the same Commented action fans out to both CU and IU
  lists, so it can't be inferred from the action)
- buildNotificationDetails uses isRecipientIu directly, no email-absence fallback
- completion callers pass isRecipientIu: true explicitly

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Jul 6, 2026

Copy link
Copy Markdown

OUT-3927

@vercel

vercel Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
tasks-app Ready Ready Preview, Comment Jul 6, 2026 12:15pm

Request Review

@greptile-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown

Greptile Summary

This PR enables email delivery for IU comment and reply notifications, reusing existing CU templates and routing comment emails through the 5-minute grouped buffer. It also fixes a latent routing bug where buildNotificationDetails used the absence of an email delivery target to infer IU vs. CU routing — an inference that would have silently misrouted IU comment emails to recipientClientId once email was enabled. The fix promotes the explicit isRecipientIu flag to a required field on createBulkNotification, forcing every caller to declare recipient type rather than relying on inference.

  • Comment path (send-comment-create-notifications.ts): IU recipients now receive email: isIuEmailEnabled(), routed through the grouped COMMENT buffer, with in-product still firing immediately (disableInProduct: false).
  • Reply path (send-reply-create-notifications.ts): IU initiator branch spreads email into deliveryTargets when isIuEmailEnabled() is true — replies remain immediate (not grouped), matching existing CU reply behavior.
  • createBulkNotification hardening (notification.service.ts, task-notifications.service.ts): opts.isRecipientIu is now required, the old ?? !email fallback is removed, and all active callers explicitly pass the flag.

Confidence Score: 5/5

Safe to merge — the IU email flag gates all new behavior, CU paths are untouched, and TypeScript now enforces explicit recipient-type declaration at every call site.

The routing fix removes a real footgun (the old ?? !email inference) rather than working around it, and promoting isRecipientIu to a required field means the compiler surfaces any future callers that forget to declare recipient type. New tests cover both the happy path and the regression case. All active callers were updated and yarn tsc is reported clean.

No files require special attention.

Important Files Changed

Filename Overview
src/app/api/notification/notification.service.ts Makes isRecipientIu required in createBulkNotification opts and removes the unsafe ?? !email inference in buildNotificationDetails; routing is now fully caller-declared
src/app/api/notification/notification.service.test.ts Adds IU-comment routing tests and a regression test proving the old email-absence inference no longer misroutes; all existing CU callers updated with explicit isRecipientIu: false
src/jobs/notifications/send-comment-create-notifications.ts IU comment path upgraded from email: false to email: isIuEmailEnabled() with isRecipientIu: true; CU path gets explicit isRecipientIu: false
src/jobs/notifications/send-reply-create-notifications.ts IU initiator branch now spreads email delivery target conditionally on isIuEmailEnabled(); replies remain immediate (not buffered), consistent with CU reply behavior
src/app/api/tasks/task-notifications.service.ts All five createBulkNotification call sites updated with explicit isRecipientIu values matching their recipient type

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant CommentJob as send-comment-create-notifications
    participant ReplyJob as send-reply-create-notifications
    participant NS as NotificationService
    participant Buffer as Grouped Email Buffer
    participant Copilot as Copilot API

    Note over CommentJob: Comment created

    CommentJob->>NS: "createBulkNotification(Commented, cuIds, {email:true, isRecipientIu:false})"
    NS->>Buffer: bufferGroupedEmailEvent(COMMENT, CU)
    NS->>Copilot: "createNotification({inProduct only, recipientClientId})"

    CommentJob->>NS: "createBulkNotification(Commented, iuIds, {email:isIuEmailEnabled(), isRecipientIu:true})"
    alt "isIuEmailEnabled() = true"
        NS->>Buffer: bufferGroupedEmailEvent(COMMENT, IU, recipientIuId)
        NS->>Copilot: "createNotification({inProduct only, recipientInternalUserId})"
    else "isIuEmailEnabled() = false"
        NS->>Copilot: "createNotification({inProduct only, recipientInternalUserId})"
    end

    Note over ReplyJob: Reply created

    ReplyJob->>ReplyJob: getInitiatorNotificationPromises(IU initiator)
    alt "isIuEmailEnabled() = true"
        ReplyJob->>Copilot: "createNotification({inProduct + email, recipientInternalUserId})"
    else "isIuEmailEnabled() = false"
        ReplyJob->>Copilot: "createNotification({inProduct only, recipientInternalUserId})"
    end
    ReplyJob->>Copilot: "createNotification({email only, recipientClientId}) [CU initiator - unchanged]"
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant CommentJob as send-comment-create-notifications
    participant ReplyJob as send-reply-create-notifications
    participant NS as NotificationService
    participant Buffer as Grouped Email Buffer
    participant Copilot as Copilot API

    Note over CommentJob: Comment created

    CommentJob->>NS: "createBulkNotification(Commented, cuIds, {email:true, isRecipientIu:false})"
    NS->>Buffer: bufferGroupedEmailEvent(COMMENT, CU)
    NS->>Copilot: "createNotification({inProduct only, recipientClientId})"

    CommentJob->>NS: "createBulkNotification(Commented, iuIds, {email:isIuEmailEnabled(), isRecipientIu:true})"
    alt "isIuEmailEnabled() = true"
        NS->>Buffer: bufferGroupedEmailEvent(COMMENT, IU, recipientIuId)
        NS->>Copilot: "createNotification({inProduct only, recipientInternalUserId})"
    else "isIuEmailEnabled() = false"
        NS->>Copilot: "createNotification({inProduct only, recipientInternalUserId})"
    end

    Note over ReplyJob: Reply created

    ReplyJob->>ReplyJob: getInitiatorNotificationPromises(IU initiator)
    alt "isIuEmailEnabled() = true"
        ReplyJob->>Copilot: "createNotification({inProduct + email, recipientInternalUserId})"
    else "isIuEmailEnabled() = false"
        ReplyJob->>Copilot: "createNotification({inProduct only, recipientInternalUserId})"
    end
    ReplyJob->>Copilot: "createNotification({email only, recipientClientId}) [CU initiator - unchanged]"
Loading

Reviews (2): Last reviewed commit: "refactor(notifications): make createBulk..." | Re-trigger Greptile

: undefined
// Recipient type is declared by the caller — the same action (e.g. Commented) fans out to
// both CU and IU recipient lists, so it can't be inferred from the action alone.
const isRecipientIu = opts?.isRecipientIu ?? false

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Silent false default is a future misrouting footgun

isRecipientIu now defaults to false rather than being inferred from the action. Every current IU-recipient caller was correctly updated, but a future call that dispatches to IU recipients and omits the opt will silently set recipientClientId to an IU ID in buildNotificationDetails — no compile-time warning, no runtime error, just a quietly misconfigured notification.

Consider making the default explicit in the JSDoc or using a required parameter (perhaps via a discriminated union for the opts type) so callers can't accidentally omit it when adding new IU-targeted bulk notifications.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Valid — good catch. Fixed in a3893dc by taking the stronger option you suggested: isRecipientIu is now a required field on createBulkNotification's opts, so a future IU-recipient caller can't silently omit it — the compiler demands it. All existing CU callers now pass isRecipientIu: false explicitly (which is how tsc surfaced them), and IU callers pass true. This removes the silent-false-default footgun entirely.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

addressed. please check again. @greptile

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Confirmed — the fix is solid. isRecipientIu is now a required field on the opts type:

opts: {
  isRecipientIu: boolean
  email?: boolean
  ...
}

And read directly as const isRecipientIu = opts.isRecipientIu with no default fallback. The JSDoc comment above the opts block also makes the requirement explicit for future callers. TypeScript will now enforce this at compile time — the footgun is fully closed. ✅

…quired

Addresses Greptile review: defaulting isRecipientIu to false is a silent
misrouting footgun — a future IU-recipient bulk caller that omits it would
route to recipientClientId with no compile-time or runtime signal.

Make isRecipientIu a required field on the opts object so every caller must
declare recipient type. All existing CU callers now pass isRecipientIu: false
explicitly; IU callers already pass true.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Deployment failed with the following error:

Deploying Serverless Functions to multiple regions is restricted to the Pro and Enterprise plans.

Learn More: https://vercel.link/multiple-function-regions

@arpandhakal

Copy link
Copy Markdown
Collaborator Author

@greptile please review again

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

lgtm

@arpandhakal
arpandhakal merged commit 37d9fda into feature/iu-email Jul 6, 2026
4 checks passed
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