CedarCopilot wants to merge 4 commits into staging from fix/notify-honor-explicit-email-off-20260922-180835
Live on prod, 5 of 10 surfaces working, 2 days left
Libra measured 5 of 10 production surfaces on prod. 2 surfaces had under 20 requests, so Libra has not judged them; 3 surfaces had no requests at all. Libra checks hourly for 3 days after each deploy.
resolvePrimaryChannel (apps/server/src/services/notifications/primary-channel.ts:100) resolves which single channel a notify-user call goes to. When no channel is explicitly enabled: true in a user's settings, it falls through to a hardcoded DEFAULT_NOTIFICATION_CHANNEL = 'email' , by design, for someone who never touched their notification settings at all.
The gap: that fallback branch never checks whether emailNotifications.enabled was itself explicitly set to false. So a user who deliberately turned email off, and hasn't connected an alternate channel yet, gets emailed anyway , every single notify-user call, not just the daily agenda digest (which has its own, separate enabled/agendaEmail check in process-agenda-emails.ts that already handles this correctly).
Evidence: Kaelin Pallavicini (Strac, <email>) asked to stop Cedar emails until Slack is connected. Her notification_settings had no channel explicitly enabled (Slack not connected). Pulling her last 30 hours of agent_tool_calls for notify-user showed 15 events, all delivered on email , task/draft/reply alerts, not just the digest, including two that explicitly requested slack as the preferred channel and still fell back to email. Separately, 3 production users already have emailNotifications.enabled: false stored with nothing else enabled, and are being silently emailed anyway today , this bug already affects real accounts, not just a hypothetical.
primary-channel.ts: resolvePrimaryChannel returns null instead of 'email' when nothing is enabled and emailNotifications.enabled === false was explicitly set. Every other case (nothing configured at all, one channel enabled, explicit caller preference) is unchanged.deliver-to-user.ts: resolveChannelFor's return type widens to NotificationChannel | null; notifyUserPlain (used by access-request and agent-share-invite notifications) treats null as "suppress, report success" rather than forcing a send.notifyUserTool.ts: the notify-user tool's apply() treats null the same way , logs it, returns { success: true, muted: true } instead of calling deliverWithEmailFallback, so the agent doesn't see an error or retry.Untouched, checked explicitly:
emailNotifications.enabled , still resolves to 'email'.login-notifier.ts's new-device sign-in alert , never routed through resolvePrimaryChannel at all (it always mails directly, on purpose, since Slack could be reached via the very Google account being alerted about).enabled === false and skips.pnpm --filter @zero/server exec vitest run src/services/notifications/__tests__/primary-channel.test.ts , 15 passing, including 4 new cases for the explicit-opt-out path.src/mastra/tools/notify/__tests__/notifyUserTool.test.ts, src/services/agents/__tests__/share-invite.test.ts, src/services/users/__tests__/resolve-user-settings.test.ts, src/services/users/__tests__/categorized-key-access.test.ts , all passing, no changes needed.pnpm --filter @zero/server run types , clean.user_settings row will be updated with emailNotifications.enabled: false + agendaEmail: false , verify her subsequent notify-user tool calls report muted: true in agent_tool_calls.result instead of deliveredOn: 'email'.π€ Generated with Claude Code
| Surface | Requests | Errors | p95 | Users | Verdict |
|---|---|---|---|---|---|
| /a8n/notify/google | 1134 β 1788 | 0 β 0 (0%) | 195 ms β 236 ms | 0 | Working 1,788 requests since the deploy with 0 errors (0 errors in the 1,134-request baseline before it). p95 236 ms, was 195 ms. |
| POST /a8n/notify/google | 378 β 596 | 0 β 0 (0%) | 198 ms β 237 ms | 0 | Working 596 requests since the deploy with 0 errors (0 errors in the 378-request baseline before it). p95 237 ms, was 198 ms. |
| /a8n/notify/google-calendar | 195 β 285 | 0 β 0 (0%) | 73 ms β 72 ms | 0 | Working 285 requests since the deploy with 0 errors (0 errors in the 195-request baseline before it). p95 72 ms, was 73 ms. |
| POST /a8n/notify/google-calendar | 65 β 95 | 0 β 0 (0%) | 75 ms β 76 ms | 0 | Working 95 requests since the deploy with 0 errors (0 errors in the 65-request baseline before it). p95 76 ms, was 75 ms. |
| orchestrator_agent_step | 24 β 29 | 0 β 0 (0%) | 36298 ms β 191942 ms | 18 | Working 29 requests since the deploy with 0 errors (0 errors in the 24-request baseline before it). p95 191942 ms, was 36298 ms. |
| model_chunk On-Event Execution Agent (Understudy) | 0 β 8 | 0 β 0 (0%) | not measured β 3 ms | 0 | Insufficient traffic 8 requests, under the 20 Libra needs |
| model_chunk Automation Agent (Understudy) | 9 β 5 | 0 β 0 (0%) | 1 ms β 1 ms | 0 | Insufficient traffic 5 requests, under the 20 Libra needs |
| /api/trpc/agentExecutions.getAgentExecutions | 0 β 0 | 0 β 0 (0%) | not measured | 0 | No traffic No requests recorded since this deploy. |
| /api/trpc/aopAgents.listForConversation | 0 β 0 | 0 β 0 (0%) | not measured | 0 | No traffic No requests recorded since this deploy. |
| /api/trpc/agentExecutions.findRecentTrees | 0 β 0 | 0 β 0 (0%) | not measured | 0 | No traffic No requests recorded since this deploy. |