Skip to content

Comments

fix: prevent double draft emails when multi-rule selection enabled#1163

Merged
elie222 merged 5 commits intomainfrom
fix/double-draft-prevention
Jan 1, 2026
Merged

fix: prevent double draft emails when multi-rule selection enabled#1163
elie222 merged 5 commits intomainfrom
fix/double-draft-prevention

Conversation

@elie222
Copy link
Owner

@elie222 elie222 commented Jan 1, 2026

User description

Fixes the double drafting bug where users with multi-rule selection enabled receive two draft replies for the same email.

TLDR: Moves limitDraftEmailActions to execute after conversation meta-rules are resolved, so it sees all DRAFT_EMAIL actions.

  • Root cause: limitDraftEmailActions was called before the conversation meta-rule was resolved to the actual TO_REPLY rule, so it couldn't see both drafts
  • Fix: Separate regular matches from conversation meta-rule, resolve conversation status, then limit drafts
  • Adds integration test that would have caught this bug
  • Refactors conversation rule resolution for clarity (filter/find instead of complex loop)

Summary by CodeRabbit

  • New Features

    • Agent-driven meeting briefings that use web-search tools with caching and graceful fallback when tools are unavailable.
    • Added an AI-generated briefing disclaimer below guest briefings.
  • Bug Fixes

    • Expanded public email domain recognition to include mail.com.
  • Tests

    • Extensive tests for meeting briefing generation and rule execution, including multi-guest and draft-email scenarios.

✏️ Tip: You can customize this high-level summary in your review settings.


Generated description

Below is a concise technical summary of the changes proposed in this PR:

graph LR
    subgraph "inbox-zero-ai" ["inbox-zero-ai"]
    runMeetingBrief_("runMeetingBrief"):::modified
    aiGenerateMeetingBriefing_("aiGenerateMeetingBriefing"):::modified
    buildSearchTools_("buildSearchTools"):::added
    createWebSearchTool_("createWebSearchTool"):::added
    getCachedResearch_("getCachedResearch"):::added
    setCachedResearch_("setCachedResearch"):::added
    deleteUser_("deleteUser"):::modified
    clearCachedResearchForUser_("clearCachedResearchForUser"):::added
    runMeetingBrief_ -- "Passes event logger into aiGenerateMeetingBriefing, enabling agent logs." --> aiGenerateMeetingBriefing_
    aiGenerateMeetingBriefing_ -- "Builds agent search tools (perplexity/web) for guest research." --> buildSearchTools_
    buildSearchTools_ -- "Wraps provider-specific web search as tool for agent." --> createWebSearchTool_
    buildSearchTools_ -- "Checks Redis cache for prior guest research by source." --> getCachedResearch_
    buildSearchTools_ -- "Caches fresh external research results keyed by user." --> setCachedResearch_
    createWebSearchTool_ -- "Retrieves cached web-search summaries for guest before querying." --> getCachedResearch_
    createWebSearchTool_ -- "Stores web-search findings in Redis for reuse." --> setCachedResearch_
    deleteUser_ -- "Evicts all research cache entries for user upon deletion." --> clearCachedResearchForUser_
    classDef added stroke:#15AA7A
    classDef removed stroke:#CD5270
    classDef modified stroke:#EDAC4C
    linkStyle default stroke:#CBD5E1,font-size:13px
    end
    subgraph "@inboxzero/resend" ["@inboxzero/resend"]
    classDef added stroke:#15AA7A
    classDef removed stroke:#CD5270
    classDef modified stroke:#EDAC4C
    linkStyle default stroke:#CBD5E1,font-size:13px
    end
Loading

Refactor the AI rule execution flow to prevent duplicate email drafts by ensuring limitDraftEmailActions processes all resolved rules, and significantly enhance AI meeting briefing generation with an agentic workflow that leverages configurable web search tools for comprehensive guest research. Additionally, improve email domain recognition and add a disclaimer to AI-generated briefing content.

TopicDetails
Agentic AI Briefings Implement an advanced agentic workflow for aiGenerateMeetingBriefing that dynamically integrates various web search tools (e.g., Perplexity, OpenAI, Google search) for comprehensive external guest research. This enhancement replaces the previous direct Perplexity research module, generalizes the caching mechanism for all research results, and introduces a user-facing disclaimer in the briefing email to indicate AI-generated content. It also includes updates to how meeting context is gathered and processed for the new agentic system.
Modified files (8)
  • apps/web/utils/meeting-briefs/gather-context.ts
  • apps/web/utils/ai/meeting-briefs/generate-briefing.ts
  • apps/web/utils/redis/research-cache.ts
  • apps/web/__tests__/ai-meeting-briefing.test.ts
  • apps/web/utils/user/delete.ts
  • apps/web/utils/meeting-briefs/process.ts
  • apps/web/utils/ai/meeting-briefs/research-guest.ts
  • packages/resend/emails/meeting-briefing.tsx
Latest Contributors(1)
UserCommitDate
elie222adjust-prompt-formatDecember 31, 2025
Other Other files
Modified files (1)
  • apps/web/utils/email.ts
Latest Contributors(2)
UserCommitDate
rsnodgrass@gmail.comfeat-enhance-meeting-c...December 31, 2025
elie222fixesDecember 17, 2025
Prevent Double Drafts Refactor the runRules function to correctly resolve conversation meta-rules before applying limitDraftEmailActions. This ensures that only a single DRAFT_EMAIL action is executed, even when multiple rules suggest drafting an email, thereby fixing a critical double drafting bug. The change involves separating regular matches from conversation meta-rules, resolving the conversation status, and then applying the draft limiting logic. This also includes new integration tests to validate the fix.
Modified files (2)
  • apps/web/utils/ai/choose-rule/run-rules.test.ts
  • apps/web/utils/ai/choose-rule/run-rules.ts
Latest Contributors(2)
UserCommitDate
elie222adjust-loggerDecember 18, 2025
eduardoleliss@gmail.comMerge-branch-staging-o...July 21, 2025
This pull request is reviewed by Baz. Review like a pro on (Baz).

…ave DRAFT_EMAIL

Moves limitDraftEmailActions to execute AFTER conversation meta-rules are resolved,
so it can see all DRAFT_EMAIL actions and properly limit to one draft.
@vercel
Copy link

vercel bot commented Jan 1, 2026

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

Project Deployment Review Updated (UTC)
inbox-zero Ready Ready Preview Jan 1, 2026 3:16pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 1, 2026

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

The PR refactors AI-powered meeting briefing generation from a sequential perplexity-based research approach to an agentic workflow with configurable search tools, updates rule execution logic to better handle conversation meta-rules and draft email limit enforcement, and consolidates research caching under a generic source-aware abstraction.

Changes

Cohort / File(s) Summary
Test Suites
apps/web/__tests__/ai-meeting-briefing.test.ts, apps/web/utils/ai/choose-rule/run-rules.test.ts
Comprehensive test coverage: AI briefing tests for prompt building, context formatting, and multi-guest scenarios; run-rules tests for draft email action limiting and multi-rule interactions, with extensive mocking of dependencies.
Meeting Briefing Agentic Workflow
apps/web/utils/ai/meeting-briefs/generate-briefing.ts
Replaces static prompt with agentic AGENTIC_SYSTEM_PROMPT; adds search tools (Perplexity, web search via OpenAI/Google/OpenRouter) with caching and fallback behavior; removes aiResearch field; adds MAX_\* constants for email/meeting/step limits; exports BriefingContent type and adds logger parameter to aiGenerateMeetingBriefing.
Rule Execution Engine
apps/web/utils/ai/choose-rule/run-rules.ts
Separates conversation meta-rule handling from regular matches; adds resolution step via determineConversationStatus; makes limitDraftEmailActions generic and type-safe with enhanced draft-selection logic; streamlines execution loop by removing per-iteration re-resolution.
Research Infrastructure
apps/web/utils/redis/research-cache.ts
Introduces ResearchSource type ("perplexity" | "websearch"); generalizes cache key generation and operations (getCachedResearch, setCachedResearch, clearCachedResearchForUser) to support per-source tracking; updates naming and logging from "perplexity-research" to "research".
Research Workflow Removal
apps/web/utils/ai/meeting-briefs/research-guest.ts
File deleted: removed researchGuestWithPerplexity and getLlmModel functions that implemented legacy per-guest Perplexity-based research.
Integration Updates
apps/web/utils/meeting-briefs/gather-context.ts, apps/web/utils/meeting-briefs/process.ts, apps/web/utils/user/delete.ts
Removes aiResearch field and research logic from gather-context; adds logger parameter to aiGenerateMeetingBriefing call in process; updates import paths and function calls from perplexity-research to research-cache.
Public Email Domains & UI
apps/web/utils/email.ts, packages/resend/emails/meeting-briefing.tsx
Added "mail.com" to PUBLIC_EMAIL_DOMAINS; added italic AI-accuracy disclaimer to meeting briefing email template.

Sequence Diagram(s)

sequenceDiagram
    participant Host as Host/App
    participant Agent as AI Agent
    participant Tools as Search Tools
    participant Cache as Cache (Redis)
    participant Model as LLM Model
    
    Host->>Agent: Start agentic briefing<br/>(guests, context, tools)
    Agent->>Cache: Check cached results<br/>(perplexity, websearch)
    Cache-->>Agent: Return cached results<br/>(if available)
    
    rect rgb(220, 240, 255)
        note over Agent,Model: Agent Loop (max steps)
        Agent->>Model: Process prompt + tools<br/>(with tool definitions)
        Model->>Agent: Next action or tool call
        
        alt Tool Call Needed
            Agent->>Tools: Execute search<br/>(perplexity or web search)
            Tools-->>Agent: Return search results
            Agent->>Cache: Store results<br/>(per source)
            Cache-->>Agent: Ack
        else Finalization
            Agent->>Agent: Prepare final briefing
            break Agent Done
                Agent-->>Host: Return BriefingContent
            end
        end
    end
    
    rect rgb(245, 245, 220)
        note over Host,Cache: Fallback Path
        Agent-->>Host: Return empty/fallback<br/>(if no tools or agent timeout)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • PR #312: Modifies run-rules.ts and match/types signatures (matchReasons, RunRules behavior) in the ai/choose-rule pipeline.
  • PR #1096: Updates meeting briefing generation with structured BriefingContent (guests array) and aiGenerateMeetingBriefing consumer code.
  • PR #1089: Modifies meeting-briefs codepaths including generate-briefing.ts, gather-context.ts, research-cache changes, and meeting-briefs orchestration.

Suggested reviewers

  • anakarentorosserrano-star

Poem

🐰 An agentic hop through search and tools,
No more sequential research rules,
With caches deep and drafts now clean,
Briefings built by a curious machine!
The meta-rules now dance in line,
Meeting prep has never been so fine!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.38% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title directly addresses the main fix: preventing double draft emails when multi-rule selection is enabled, which is the core purpose of this PR.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@macroscopeapp
Copy link
Contributor

macroscopeapp bot commented Jan 1, 2026

Prevent duplicate draft email actions when multi-rule selection is enabled by limiting runRules.limitDraftEmailActions across conversation and custom rules in run-rules.ts

Refactors conversation rule resolution in runRules, applies a single draft limit across AI and static drafts, and introduces an agentic meeting briefing flow with tool-backed research and a minimal fallback; updates research caching and removes legacy guest research.

📍Where to Start

Start with runRules and limitDraftEmailActions in run-rules.ts; then review the agent entry point aiGenerateMeetingBriefing in generate-briefing.ts.


Macroscope summarized e1c7c88.

Comment on lines +366 to +372
if (
env.DEFAULT_LLM_PROVIDER === Provider.OPEN_AI ||
env.DEFAULT_LLM_PROVIDER === Provider.GOOGLE ||
env.DEFAULT_LLM_PROVIDER === Provider.OPENROUTER
) {
availableTools.push("webSearch");
}
Copy link
Contributor

Choose a reason for hiding this comment

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

webSearch is advertised based only on env.DEFAULT_LLM_PROVIDER, so the prompt may claim the tool exists even when the required API key isn’t set, causing runtime errors. Consider also gating webSearch on the provider’s API key to match actual tool availability.

Suggested change
if (
env.DEFAULT_LLM_PROVIDER === Provider.OPEN_AI ||
env.DEFAULT_LLM_PROVIDER === Provider.GOOGLE ||
env.DEFAULT_LLM_PROVIDER === Provider.OPENROUTER
) {
availableTools.push("webSearch");
}
if (
(env.DEFAULT_LLM_PROVIDER === Provider.OPEN_AI && !!env.OPENAI_API_KEY) ||
(env.DEFAULT_LLM_PROVIDER === Provider.GOOGLE && !!env.GOOGLE_API_KEY) ||
(env.DEFAULT_LLM_PROVIDER === Provider.OPENROUTER && !!env.OPENROUTER_API_KEY)
) {
availableTools.push("webSearch");
}

🚀 Want me to fix this? Reply ex: "fix it for me".

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (4)
apps/web/utils/ai/meeting-briefs/generate-briefing.ts (4)

75-143: Result capture via closure side-effect.

The result variable is set via side-effect inside the tool's execute callback. While this works, it relies on the tool execution happening synchronously within the generateText call. Consider adding a comment explaining this pattern for future maintainers.

The early return for empty guests and the fallback when the agent doesn't finalize are good defensive patterns.


185-224: Provider string inconsistency.

Line 196 uses the string literal "perplexity" while other parts of the codebase use the Provider enum from @/utils/llms/config. While Perplexity isn't in the standard Provider enum (it's a search-specific service), consider adding a comment or defining a constant for consistency.

🔎 Suggested improvement
+// Note: "perplexity" is not in the standard Provider enum as it's a search-specific service
 const perplexityGenerateText = createGenerateText({
   emailAccount,
   label: "Perplexity Search",
   modelOptions: {
     modelName: "sonar-pro",
     model: perplexity("sonar-pro"),
     provider: "perplexity",
     backupModel: null,
   },
 });

248-272: Consider extracting provider check to a shared helper.

The logic for determining web search availability is duplicated:

  • Here in getWebSearchConfig (lines 249-271)
  • In buildPrompt (lines 366-372)

If new providers are added, both locations need updating.

🔎 Suggested refactor
+function isWebSearchProvider(provider: string): boolean {
+  return [Provider.OPEN_AI, Provider.GOOGLE, Provider.OPENROUTER].includes(provider as any);
+}
+
 function getWebSearchConfig(): WebSearchConfig | null {
   switch (env.DEFAULT_LLM_PROVIDER) {
     case Provider.OPEN_AI:
     // ...

Then in buildPrompt:

-  if (
-    env.DEFAULT_LLM_PROVIDER === Provider.OPEN_AI ||
-    env.DEFAULT_LLM_PROVIDER === Provider.GOOGLE ||
-    env.DEFAULT_LLM_PROVIDER === Provider.OPENROUTER
-  ) {
+  if (isWebSearchProvider(env.DEFAULT_LLM_PROVIDER)) {
     availableTools.push("webSearch");
   }

363-377: Tool availability check is duplicated.

This logic mirrors buildSearchTools and getWebSearchConfig. See earlier comment about extracting to a shared helper.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 45e6da3 and bf92508.

📒 Files selected for processing (11)
  • apps/web/__tests__/ai-meeting-briefing.test.ts
  • apps/web/utils/ai/choose-rule/run-rules.test.ts
  • apps/web/utils/ai/choose-rule/run-rules.ts
  • apps/web/utils/ai/meeting-briefs/generate-briefing.ts
  • apps/web/utils/ai/meeting-briefs/research-guest.ts
  • apps/web/utils/email.ts
  • apps/web/utils/meeting-briefs/gather-context.ts
  • apps/web/utils/meeting-briefs/process.ts
  • apps/web/utils/redis/research-cache.ts
  • apps/web/utils/user/delete.ts
  • packages/resend/emails/meeting-briefing.tsx
💤 Files with no reviewable changes (1)
  • apps/web/utils/ai/meeting-briefs/research-guest.ts
🧰 Additional context used
📓 Path-based instructions (24)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/data-fetching.mdc)

**/*.{ts,tsx}: For API GET requests to server, use the swr package
Use result?.serverError with toastError from @/components/Toast for error handling in async operations

**/*.{ts,tsx}: Use wrapper functions for Gmail message operations (get, list, batch, etc.) from @/utils/gmail/message.ts instead of direct API calls
Use wrapper functions for Gmail thread operations from @/utils/gmail/thread.ts instead of direct API calls
Use wrapper functions for Gmail label operations from @/utils/gmail/label.ts instead of direct API calls

**/*.{ts,tsx}: For early access feature flags, create hooks using the naming convention use[FeatureName]Enabled that return a boolean from useFeatureFlagEnabled("flag-key")
For A/B test variant flags, create hooks using the naming convention use[FeatureName]Variant that define variant types, use useFeatureFlagVariantKey() with type casting, and provide a default "control" fallback
Use kebab-case for PostHog feature flag keys (e.g., inbox-cleaner, pricing-options-2)
Always define types for A/B test variant flags (e.g., type PricingVariant = "control" | "variant-a" | "variant-b") and provide type safety through type casting

**/*.{ts,tsx}: Don't use primitive type aliases or misleading types
Don't use empty type parameters in type aliases and interfaces
Don't use this and super in static contexts
Don't use any or unknown as type constraints
Don't use the TypeScript directive @ts-ignore
Don't use TypeScript enums
Don't export imported variables
Don't add type annotations to variables, parameters, and class properties that are initialized with literal expressions
Don't use TypeScript namespaces
Don't use non-null assertions with the ! postfix operator
Don't use parameter properties in class constructors
Don't use user-defined types
Use as const instead of literal types and type annotations
Use either T[] or Array<T> consistently
Initialize each enum member value explicitly
Use export type for types
Use `impo...

Files:

  • apps/web/utils/email.ts
  • apps/web/utils/user/delete.ts
  • apps/web/__tests__/ai-meeting-briefing.test.ts
  • apps/web/utils/ai/meeting-briefs/generate-briefing.ts
  • apps/web/utils/ai/choose-rule/run-rules.test.ts
  • apps/web/utils/ai/choose-rule/run-rules.ts
  • apps/web/utils/meeting-briefs/process.ts
  • apps/web/utils/redis/research-cache.ts
  • packages/resend/emails/meeting-briefing.tsx
  • apps/web/utils/meeting-briefs/gather-context.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/prisma-enum-imports.mdc)

Always import Prisma enums from @/generated/prisma/enums instead of @/generated/prisma/client to avoid Next.js bundling errors in client components

Import Prisma using the project's centralized utility: import prisma from '@/utils/prisma'

Files:

  • apps/web/utils/email.ts
  • apps/web/utils/user/delete.ts
  • apps/web/__tests__/ai-meeting-briefing.test.ts
  • apps/web/utils/ai/meeting-briefs/generate-briefing.ts
  • apps/web/utils/ai/choose-rule/run-rules.test.ts
  • apps/web/utils/ai/choose-rule/run-rules.ts
  • apps/web/utils/meeting-briefs/process.ts
  • apps/web/utils/redis/research-cache.ts
  • packages/resend/emails/meeting-briefing.tsx
  • apps/web/utils/meeting-briefs/gather-context.ts
apps/web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/project-structure.mdc)

Import specific lodash functions rather than entire lodash library to minimize bundle size (e.g., import groupBy from 'lodash/groupBy')

apps/web/**/*.{ts,tsx}: Use TypeScript with strict null checks
Do not export types/interfaces that are only used within the same file. Export later if needed

Files:

  • apps/web/utils/email.ts
  • apps/web/utils/user/delete.ts
  • apps/web/__tests__/ai-meeting-briefing.test.ts
  • apps/web/utils/ai/meeting-briefs/generate-briefing.ts
  • apps/web/utils/ai/choose-rule/run-rules.test.ts
  • apps/web/utils/ai/choose-rule/run-rules.ts
  • apps/web/utils/meeting-briefs/process.ts
  • apps/web/utils/redis/research-cache.ts
  • apps/web/utils/meeting-briefs/gather-context.ts
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/security.mdc)

**/*.ts: ALL database queries MUST be scoped to the authenticated user/account by including user/account filtering in WHERE clauses to prevent unauthorized data access
Always validate that resources belong to the authenticated user before performing operations, using ownership checks in WHERE clauses or relationships
Always validate all input parameters for type, format, and length before using them in database queries
Use SafeError for error responses to prevent information disclosure. Generic error messages should not reveal internal IDs, logic, or resource ownership details
Only return necessary fields in API responses using Prisma's select option. Never expose sensitive data such as password hashes, private keys, or system flags
Prevent Insecure Direct Object References (IDOR) by validating resource ownership before operations. All findUnique/findFirst calls MUST include ownership filters
Prevent mass assignment vulnerabilities by explicitly whitelisting allowed fields in update operations instead of accepting all user-provided data
Prevent privilege escalation by never allowing users to modify system fields, ownership fields, or admin-only attributes through user input
All findMany queries MUST be scoped to the user's data by including appropriate WHERE filters to prevent returning data from other users
Use Prisma relationships for access control by leveraging nested where clauses (e.g., emailAccount: { id: emailAccountId }) to validate ownership

Files:

  • apps/web/utils/email.ts
  • apps/web/utils/user/delete.ts
  • apps/web/__tests__/ai-meeting-briefing.test.ts
  • apps/web/utils/ai/meeting-briefs/generate-briefing.ts
  • apps/web/utils/ai/choose-rule/run-rules.test.ts
  • apps/web/utils/ai/choose-rule/run-rules.ts
  • apps/web/utils/meeting-briefs/process.ts
  • apps/web/utils/redis/research-cache.ts
  • apps/web/utils/meeting-briefs/gather-context.ts
**/*.{tsx,ts}

📄 CodeRabbit inference engine (.cursor/rules/ui-components.mdc)

**/*.{tsx,ts}: Use Shadcn UI and Tailwind for components and styling
Use next/image package for images
For API GET requests to server, use the swr package with hooks like useSWR to fetch data
For text inputs, use the Input component with registerProps for form integration and error handling

Files:

  • apps/web/utils/email.ts
  • apps/web/utils/user/delete.ts
  • apps/web/__tests__/ai-meeting-briefing.test.ts
  • apps/web/utils/ai/meeting-briefs/generate-briefing.ts
  • apps/web/utils/ai/choose-rule/run-rules.test.ts
  • apps/web/utils/ai/choose-rule/run-rules.ts
  • apps/web/utils/meeting-briefs/process.ts
  • apps/web/utils/redis/research-cache.ts
  • packages/resend/emails/meeting-briefing.tsx
  • apps/web/utils/meeting-briefs/gather-context.ts
**/*.{tsx,ts,css}

📄 CodeRabbit inference engine (.cursor/rules/ui-components.mdc)

Implement responsive design with Tailwind CSS using a mobile-first approach

Files:

  • apps/web/utils/email.ts
  • apps/web/utils/user/delete.ts
  • apps/web/__tests__/ai-meeting-briefing.test.ts
  • apps/web/utils/ai/meeting-briefs/generate-briefing.ts
  • apps/web/utils/ai/choose-rule/run-rules.test.ts
  • apps/web/utils/ai/choose-rule/run-rules.ts
  • apps/web/utils/meeting-briefs/process.ts
  • apps/web/utils/redis/research-cache.ts
  • packages/resend/emails/meeting-briefing.tsx
  • apps/web/utils/meeting-briefs/gather-context.ts
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)

**/*.{js,jsx,ts,tsx}: Don't use accessKey attribute on any HTML element
Don't set aria-hidden="true" on focusable elements
Don't add ARIA roles, states, and properties to elements that don't support them
Don't use distracting elements like <marquee> or <blink>
Only use the scope prop on <th> elements
Don't assign non-interactive ARIA roles to interactive HTML elements
Make sure label elements have text content and are associated with an input
Don't assign interactive ARIA roles to non-interactive HTML elements
Don't assign tabIndex to non-interactive HTML elements
Don't use positive integers for tabIndex property
Don't include "image", "picture", or "photo" in img alt prop
Don't use explicit role property that's the same as the implicit/default role
Make static elements with click handlers use a valid role attribute
Always include a title element for SVG elements
Give all elements requiring alt text meaningful information for screen readers
Make sure anchors have content that's accessible to screen readers
Assign tabIndex to non-interactive HTML elements with aria-activedescendant
Include all required ARIA attributes for elements with ARIA roles
Make sure ARIA properties are valid for the element's supported roles
Always include a type attribute for button elements
Make elements with interactive roles and handlers focusable
Give heading elements content that's accessible to screen readers (not hidden with aria-hidden)
Always include a lang attribute on the html element
Always include a title attribute for iframe elements
Accompany onClick with at least one of: onKeyUp, onKeyDown, or onKeyPress
Accompany onMouseOver/onMouseOut with onFocus/onBlur
Include caption tracks for audio and video elements
Use semantic elements instead of role attributes in JSX
Make sure all anchors are valid and navigable
Ensure all ARIA properties (aria-*) are valid
Use valid, non-abstract ARIA roles for elements with ARIA roles
Use valid AR...

Files:

  • apps/web/utils/email.ts
  • apps/web/utils/user/delete.ts
  • apps/web/__tests__/ai-meeting-briefing.test.ts
  • apps/web/utils/ai/meeting-briefs/generate-briefing.ts
  • apps/web/utils/ai/choose-rule/run-rules.test.ts
  • apps/web/utils/ai/choose-rule/run-rules.ts
  • apps/web/utils/meeting-briefs/process.ts
  • apps/web/utils/redis/research-cache.ts
  • packages/resend/emails/meeting-briefing.tsx
  • apps/web/utils/meeting-briefs/gather-context.ts
!(pages/_document).{jsx,tsx}

📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)

Don't use the next/head module in pages/_document.js on Next.js projects

Files:

  • apps/web/utils/email.ts
  • apps/web/utils/user/delete.ts
  • apps/web/__tests__/ai-meeting-briefing.test.ts
  • apps/web/utils/ai/meeting-briefs/generate-briefing.ts
  • apps/web/utils/ai/choose-rule/run-rules.test.ts
  • apps/web/utils/ai/choose-rule/run-rules.ts
  • apps/web/utils/meeting-briefs/process.ts
  • apps/web/utils/redis/research-cache.ts
  • packages/resend/emails/meeting-briefing.tsx
  • apps/web/utils/meeting-briefs/gather-context.ts
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (.cursor/rules/utilities.mdc)

**/*.{js,ts,jsx,tsx}: Use lodash utilities for common operations (arrays, objects, strings)
Import specific lodash functions to minimize bundle size (e.g., import groupBy from 'lodash/groupBy')

Files:

  • apps/web/utils/email.ts
  • apps/web/utils/user/delete.ts
  • apps/web/__tests__/ai-meeting-briefing.test.ts
  • apps/web/utils/ai/meeting-briefs/generate-briefing.ts
  • apps/web/utils/ai/choose-rule/run-rules.test.ts
  • apps/web/utils/ai/choose-rule/run-rules.ts
  • apps/web/utils/meeting-briefs/process.ts
  • apps/web/utils/redis/research-cache.ts
  • packages/resend/emails/meeting-briefing.tsx
  • apps/web/utils/meeting-briefs/gather-context.ts
**/{utils,helpers,lib}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/logging.mdc)

Logger should be passed as a parameter to helper functions instead of creating their own logger instances

Files:

  • apps/web/utils/email.ts
  • apps/web/utils/user/delete.ts
  • apps/web/utils/ai/meeting-briefs/generate-briefing.ts
  • apps/web/utils/ai/choose-rule/run-rules.test.ts
  • apps/web/utils/ai/choose-rule/run-rules.ts
  • apps/web/utils/meeting-briefs/process.ts
  • apps/web/utils/redis/research-cache.ts
  • apps/web/utils/meeting-briefs/gather-context.ts
apps/web/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (apps/web/CLAUDE.md)

apps/web/**/*.{ts,tsx,js,jsx}: Use @/ path aliases for imports from project root
Prefer self-documenting code over comments; use descriptive variable and function names instead of explaining intent with comments
Add helper functions to the bottom of files, not the top
All imports go at the top of files, no mid-file dynamic imports

Files:

  • apps/web/utils/email.ts
  • apps/web/utils/user/delete.ts
  • apps/web/__tests__/ai-meeting-briefing.test.ts
  • apps/web/utils/ai/meeting-briefs/generate-briefing.ts
  • apps/web/utils/ai/choose-rule/run-rules.test.ts
  • apps/web/utils/ai/choose-rule/run-rules.ts
  • apps/web/utils/meeting-briefs/process.ts
  • apps/web/utils/redis/research-cache.ts
  • apps/web/utils/meeting-briefs/gather-context.ts
apps/web/**/*.{ts,tsx,js,jsx,json,css}

📄 CodeRabbit inference engine (apps/web/CLAUDE.md)

Format code with Prettier

Files:

  • apps/web/utils/email.ts
  • apps/web/utils/user/delete.ts
  • apps/web/__tests__/ai-meeting-briefing.test.ts
  • apps/web/utils/ai/meeting-briefs/generate-briefing.ts
  • apps/web/utils/ai/choose-rule/run-rules.test.ts
  • apps/web/utils/ai/choose-rule/run-rules.ts
  • apps/web/utils/meeting-briefs/process.ts
  • apps/web/utils/redis/research-cache.ts
  • apps/web/utils/meeting-briefs/gather-context.ts
apps/web/**/*.{example,ts,json}

📄 CodeRabbit inference engine (apps/web/CLAUDE.md)

Add environment variables to .env.example, env.ts, and turbo.json

Files:

  • apps/web/utils/email.ts
  • apps/web/utils/user/delete.ts
  • apps/web/__tests__/ai-meeting-briefing.test.ts
  • apps/web/utils/ai/meeting-briefs/generate-briefing.ts
  • apps/web/utils/ai/choose-rule/run-rules.test.ts
  • apps/web/utils/ai/choose-rule/run-rules.ts
  • apps/web/utils/meeting-briefs/process.ts
  • apps/web/utils/redis/research-cache.ts
  • apps/web/utils/meeting-briefs/gather-context.ts
apps/web/__tests__/**/*.test.ts

📄 CodeRabbit inference engine (.cursor/rules/llm-test.mdc)

apps/web/__tests__/**/*.test.ts: Place all LLM-related tests in apps/web/__tests__/ directory
Use vitest imports (describe, expect, test, vi, beforeEach) in LLM test files
Mock 'server-only' module with empty object in LLM test files: vi.mock("server-only", () => ({}))
Set timeout constant const TIMEOUT = 15_000; for LLM tests
Use describe.runIf(isAiTest) with environment variable RUN_AI_TESTS === "true" to conditionally run LLM tests
Use console.debug() for outputting generated LLM content in tests, e.g., console.debug("Generated content:\n", result.content);
Prefer using existing helpers from @/__tests__/helpers.ts (getEmailAccount, getEmail, getRule, getMockMessage, getMockExecutedRule) instead of creating custom test data helpers

Files:

  • apps/web/__tests__/ai-meeting-briefing.test.ts
apps/web/{utils/ai,utils/llms,__tests__}/**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/llm.mdc)

LLM-related code must be organized in specific directories: apps/web/utils/ai/ for main implementations, apps/web/utils/llms/ for core utilities and configurations, and apps/web/__tests__/ for LLM-specific tests

Files:

  • apps/web/__tests__/ai-meeting-briefing.test.ts
  • apps/web/utils/ai/meeting-briefs/generate-briefing.ts
  • apps/web/utils/ai/choose-rule/run-rules.test.ts
  • apps/web/utils/ai/choose-rule/run-rules.ts
**/*.{test,spec}.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)

**/*.{test,spec}.{js,jsx,ts,tsx}: Don't nest describe() blocks too deeply in test files
Don't use callbacks in asynchronous tests and hooks
Don't have duplicate hooks in describe blocks
Don't use export or module.exports in test files
Don't use focused tests
Make sure the assertion function, like expect, is placed inside an it() function call
Don't use disabled tests

Files:

  • apps/web/__tests__/ai-meeting-briefing.test.ts
  • apps/web/utils/ai/choose-rule/run-rules.test.ts
**/{scripts,tests,__tests__}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/logging.mdc)

Use createScopedLogger only for code that doesn't run within a middleware chain, such as standalone scripts or tests

Files:

  • apps/web/__tests__/ai-meeting-briefing.test.ts
apps/web/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (apps/web/CLAUDE.md)

Co-locate test files next to source files (e.g., utils/example.test.ts). Only E2E and AI tests go in __tests__/

Files:

  • apps/web/__tests__/ai-meeting-briefing.test.ts
  • apps/web/utils/ai/choose-rule/run-rules.test.ts
**/*.test.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/notes.mdc)

Co-locate test files next to source files (e.g., utils/example.test.ts). Only E2E and AI tests go in __tests__/

Files:

  • apps/web/__tests__/ai-meeting-briefing.test.ts
  • apps/web/utils/ai/choose-rule/run-rules.test.ts
**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/testing.mdc)

**/*.test.{ts,tsx}: Use vitest as the testing framework
Colocate test files next to the tested file with .test.ts or .test.tsx naming convention (e.g., dir/format.ts and dir/format.test.ts)
Mock server-only using vi.mock("server-only", () => ({}))
Mock Prisma using vi.mock("@/utils/prisma") and the provided mock from @/utils/__mocks__/prisma
Use test helper functions getEmail, getEmailAccount, and getRule from @/__tests__/helpers for creating mock data
Clear all mocks between tests using beforeEach(() => { vi.clearAllMocks(); })
Use descriptive test names that clearly indicate what is being tested
Do not mock the Logger in tests

Files:

  • apps/web/__tests__/ai-meeting-briefing.test.ts
  • apps/web/utils/ai/choose-rule/run-rules.test.ts
**/__tests__/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/testing.mdc)

Place AI tests in the __tests__ directory and do not run them by default as they use a real LLM

Files:

  • apps/web/__tests__/ai-meeting-briefing.test.ts
apps/web/utils/ai/**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/llm.mdc)

apps/web/utils/ai/**/*.ts: LLM feature functions must import from zod for schema validation, use createScopedLogger from @/utils/logger, chatCompletionObject and createGenerateObject from @/utils/llms, and import EmailAccountWithAI type from @/utils/llms/types
LLM feature functions must follow a standard structure: accept options with inputData and emailAccount parameters, implement input validation with early returns, define separate system and user prompts, create a Zod schema for response validation, and use createGenerateObject to execute the LLM call
System prompts must define the LLM's role and task specifications
User prompts must contain the actual data and context, and should be kept separate from system prompts
Always define a Zod schema for LLM response validation and make schemas as specific as possible to guide the LLM output
Use descriptive scoped loggers for each LLM feature, log inputs and outputs with appropriate log levels, and include relevant context in log messages
Implement early returns for invalid LLM inputs, use proper error types and logging, implement fallbacks for AI failures, and add retry logic for transient failures using withRetry
Use XML-like tags to structure data in prompts, remove excessive whitespace and truncate long inputs, and format data consistently across similar LLM functions
Use TypeScript types for all LLM function parameters and return values, and define clear interfaces for complex input/output structures
Keep related AI functions in the same file or directory, extract common patterns into utility functions, and document complex AI logic with clear comments

Files:

  • apps/web/utils/ai/meeting-briefs/generate-briefing.ts
  • apps/web/utils/ai/choose-rule/run-rules.test.ts
  • apps/web/utils/ai/choose-rule/run-rules.ts
**/*.tsx

📄 CodeRabbit inference engine (.cursor/rules/ui-components.mdc)

**/*.tsx: Use the LoadingContent component to handle loading states instead of manual loading state management
For text areas, use the Input component with type='text', autosizeTextarea prop set to true, and registerProps for form integration

Files:

  • packages/resend/emails/meeting-briefing.tsx
**/*.{jsx,tsx}

📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)

**/*.{jsx,tsx}: Don't use unnecessary fragments
Don't pass children as props
Don't use the return value of React.render
Make sure all dependencies are correctly specified in React hooks
Make sure all React hooks are called from the top level of component functions
Don't forget key props in iterators and collection literals
Don't define React components inside other components
Don't use event handlers on non-interactive elements
Don't assign to React component props
Don't use both children and dangerouslySetInnerHTML props on the same element
Don't use dangerous JSX props
Don't use Array index in keys
Don't insert comments as text nodes
Don't assign JSX properties multiple times
Don't add extra closing tags for components without children
Use <>...</> instead of <Fragment>...</Fragment>
Watch out for possible "wrong" semicolons inside JSX elements
Make sure void (self-closing) elements don't have children
Don't use target="_blank" without rel="noopener"
Don't use <img> elements in Next.js projects
Don't use <head> elements in Next.js projects

Files:

  • packages/resend/emails/meeting-briefing.tsx
🧠 Learnings (25)
📚 Learning: 2025-11-25T14:37:56.430Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/llm-test.mdc:0-0
Timestamp: 2025-11-25T14:37:56.430Z
Learning: Applies to apps/web/__tests__/**/*.test.ts : Use `describe.runIf(isAiTest)` with environment variable `RUN_AI_TESTS === "true"` to conditionally run LLM tests

Applied to files:

  • apps/web/__tests__/ai-meeting-briefing.test.ts
  • apps/web/utils/ai/choose-rule/run-rules.test.ts
📚 Learning: 2026-01-01T10:42:29.767Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/testing.mdc:0-0
Timestamp: 2026-01-01T10:42:29.767Z
Learning: Applies to **/__tests__/**/*.{ts,tsx} : Place AI tests in the `__tests__` directory and do not run them by default as they use a real LLM

Applied to files:

  • apps/web/__tests__/ai-meeting-briefing.test.ts
  • apps/web/utils/ai/choose-rule/run-rules.test.ts
📚 Learning: 2025-11-25T14:38:07.606Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/llm.mdc:0-0
Timestamp: 2025-11-25T14:38:07.606Z
Learning: Applies to apps/web/utils/ai/**/*.ts : Keep related AI functions in the same file or directory, extract common patterns into utility functions, and document complex AI logic with clear comments

Applied to files:

  • apps/web/__tests__/ai-meeting-briefing.test.ts
  • apps/web/utils/ai/choose-rule/run-rules.test.ts
  • apps/web/utils/ai/choose-rule/run-rules.ts
  • apps/web/utils/meeting-briefs/gather-context.ts
📚 Learning: 2025-11-25T14:38:07.606Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/llm.mdc:0-0
Timestamp: 2025-11-25T14:38:07.606Z
Learning: Applies to apps/web/utils/ai/**/*.ts : Use XML-like tags to structure data in prompts, remove excessive whitespace and truncate long inputs, and format data consistently across similar LLM functions

Applied to files:

  • apps/web/__tests__/ai-meeting-briefing.test.ts
  • apps/web/utils/ai/choose-rule/run-rules.ts
📚 Learning: 2025-12-21T12:21:37.794Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: apps/web/CLAUDE.md:0-0
Timestamp: 2025-12-21T12:21:37.794Z
Learning: Applies to apps/web/**/*.test.{ts,tsx} : Co-locate test files next to source files (e.g., `utils/example.test.ts`). Only E2E and AI tests go in `__tests__/`

Applied to files:

  • apps/web/__tests__/ai-meeting-briefing.test.ts
  • apps/web/utils/ai/choose-rule/run-rules.test.ts
📚 Learning: 2025-11-25T14:38:07.606Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/llm.mdc:0-0
Timestamp: 2025-11-25T14:38:07.606Z
Learning: Applies to apps/web/utils/ai/**/*.ts : LLM feature functions must import from `zod` for schema validation, use `createScopedLogger` from `@/utils/logger`, `chatCompletionObject` and `createGenerateObject` from `@/utils/llms`, and import `EmailAccountWithAI` type from `@/utils/llms/types`

Applied to files:

  • apps/web/__tests__/ai-meeting-briefing.test.ts
  • apps/web/utils/ai/meeting-briefs/generate-briefing.ts
  • apps/web/utils/ai/choose-rule/run-rules.test.ts
  • apps/web/utils/ai/choose-rule/run-rules.ts
  • apps/web/utils/meeting-briefs/process.ts
  • apps/web/utils/meeting-briefs/gather-context.ts
📚 Learning: 2025-11-25T14:38:07.606Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/llm.mdc:0-0
Timestamp: 2025-11-25T14:38:07.606Z
Learning: Applies to apps/web/utils/ai/**/*.ts : Implement early returns for invalid LLM inputs, use proper error types and logging, implement fallbacks for AI failures, and add retry logic for transient failures using `withRetry`

Applied to files:

  • apps/web/__tests__/ai-meeting-briefing.test.ts
  • apps/web/utils/ai/choose-rule/run-rules.test.ts
  • apps/web/utils/ai/choose-rule/run-rules.ts
  • apps/web/utils/meeting-briefs/gather-context.ts
📚 Learning: 2026-01-01T10:42:29.767Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/testing.mdc:0-0
Timestamp: 2026-01-01T10:42:29.767Z
Learning: Applies to **/*.test.{ts,tsx} : Use descriptive test names that clearly indicate what is being tested

Applied to files:

  • apps/web/__tests__/ai-meeting-briefing.test.ts
📚 Learning: 2025-11-25T14:38:07.606Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/llm.mdc:0-0
Timestamp: 2025-11-25T14:38:07.606Z
Learning: Applies to apps/web/utils/ai/**/*.ts : System prompts must define the LLM's role and task specifications

Applied to files:

  • apps/web/__tests__/ai-meeting-briefing.test.ts
📚 Learning: 2025-11-25T14:37:56.430Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/llm-test.mdc:0-0
Timestamp: 2025-11-25T14:37:56.430Z
Learning: Applies to apps/web/__tests__/**/*.test.ts : Use vitest imports (`describe`, `expect`, `test`, `vi`, `beforeEach`) in LLM test files

Applied to files:

  • apps/web/__tests__/ai-meeting-briefing.test.ts
📚 Learning: 2025-11-25T14:38:07.606Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/llm.mdc:0-0
Timestamp: 2025-11-25T14:38:07.606Z
Learning: Applies to apps/web/utils/ai/**/*.ts : LLM feature functions must follow a standard structure: accept options with `inputData` and `emailAccount` parameters, implement input validation with early returns, define separate system and user prompts, create a Zod schema for response validation, and use `createGenerateObject` to execute the LLM call

Applied to files:

  • apps/web/utils/ai/meeting-briefs/generate-briefing.ts
  • apps/web/utils/ai/choose-rule/run-rules.test.ts
  • apps/web/utils/ai/choose-rule/run-rules.ts
📚 Learning: 2025-11-25T14:38:07.606Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/llm.mdc:0-0
Timestamp: 2025-11-25T14:38:07.606Z
Learning: Applies to apps/web/utils/ai/**/*.ts : User prompts must contain the actual data and context, and should be kept separate from system prompts

Applied to files:

  • apps/web/utils/ai/meeting-briefs/generate-briefing.ts
📚 Learning: 2025-11-25T14:37:56.430Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/llm-test.mdc:0-0
Timestamp: 2025-11-25T14:37:56.430Z
Learning: Applies to apps/web/__tests__/**/*.test.ts : Prefer using existing helpers from `@/__tests__/helpers.ts` (`getEmailAccount`, `getEmail`, `getRule`, `getMockMessage`, `getMockExecutedRule`) instead of creating custom test data helpers

Applied to files:

  • apps/web/utils/ai/choose-rule/run-rules.test.ts
📚 Learning: 2026-01-01T10:42:29.767Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/testing.mdc:0-0
Timestamp: 2026-01-01T10:42:29.767Z
Learning: Applies to **/*.test.{ts,tsx} : Use test helper functions `getEmail`, `getEmailAccount`, and `getRule` from `@/__tests__/helpers` for creating mock data

Applied to files:

  • apps/web/utils/ai/choose-rule/run-rules.test.ts
📚 Learning: 2025-11-25T14:38:07.606Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/llm.mdc:0-0
Timestamp: 2025-11-25T14:38:07.606Z
Learning: Applies to apps/web/{utils/ai,utils/llms,__tests__}/**/*.ts : LLM-related code must be organized in specific directories: `apps/web/utils/ai/` for main implementations, `apps/web/utils/llms/` for core utilities and configurations, and `apps/web/__tests__/` for LLM-specific tests

Applied to files:

  • apps/web/utils/ai/choose-rule/run-rules.test.ts
📚 Learning: 2026-01-01T10:42:29.767Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/testing.mdc:0-0
Timestamp: 2026-01-01T10:42:29.767Z
Learning: Applies to **/*.test.{ts,tsx} : Mock Prisma using `vi.mock("@/utils/prisma")` and the provided mock from `@/utils/__mocks__/prisma`

Applied to files:

  • apps/web/utils/ai/choose-rule/run-rules.test.ts
📚 Learning: 2025-11-25T14:39:23.326Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/security.mdc:0-0
Timestamp: 2025-11-25T14:39:23.326Z
Learning: Applies to app/api/**/*.ts : Use `withEmailAccount` middleware for operations scoped to a specific email account (reading/writing emails, rules, schedules, etc.) - provides `emailAccountId`, `userId`, and `email` in `request.auth`

Applied to files:

  • apps/web/utils/ai/choose-rule/run-rules.test.ts
📚 Learning: 2025-11-25T14:39:27.909Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/security.mdc:0-0
Timestamp: 2025-11-25T14:39:27.909Z
Learning: Applies to **/app/api/**/*.ts : Use `withEmailAccount` middleware for operations scoped to a specific email account, including reading/writing emails, rules, schedules, or any operation using `emailAccountId`

Applied to files:

  • apps/web/utils/ai/choose-rule/run-rules.test.ts
📚 Learning: 2025-11-25T14:42:11.919Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/utilities.mdc:0-0
Timestamp: 2025-11-25T14:42:11.919Z
Learning: Applies to utils/**/*.{js,ts,jsx,tsx} : The `utils` folder contains core app logic such as Next.js Server Actions and Gmail API requests

Applied to files:

  • apps/web/utils/ai/choose-rule/run-rules.test.ts
📚 Learning: 2026-01-01T10:42:29.767Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/testing.mdc:0-0
Timestamp: 2026-01-01T10:42:29.767Z
Learning: Applies to **/*.test.{ts,tsx} : Mock `server-only` using `vi.mock("server-only", () => ({}))`

Applied to files:

  • apps/web/utils/ai/choose-rule/run-rules.test.ts
📚 Learning: 2025-11-25T14:39:49.448Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/server-actions.mdc:0-0
Timestamp: 2025-11-25T14:39:49.448Z
Learning: Applies to apps/web/utils/actions/*.ts : Use `actionClient` when both authenticated user context and a specific emailAccountId are needed, with emailAccountId bound when calling from the client

Applied to files:

  • apps/web/utils/ai/choose-rule/run-rules.test.ts
📚 Learning: 2025-11-25T14:37:56.430Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/llm-test.mdc:0-0
Timestamp: 2025-11-25T14:37:56.430Z
Learning: Applies to apps/web/__tests__/**/*.test.ts : Mock 'server-only' module with empty object in LLM test files: `vi.mock("server-only", () => ({}))`

Applied to files:

  • apps/web/utils/ai/choose-rule/run-rules.test.ts
📚 Learning: 2025-11-25T14:38:07.606Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/llm.mdc:0-0
Timestamp: 2025-11-25T14:38:07.606Z
Learning: Applies to apps/web/utils/ai/**/*.ts : Use TypeScript types for all LLM function parameters and return values, and define clear interfaces for complex input/output structures

Applied to files:

  • apps/web/utils/ai/choose-rule/run-rules.ts
📚 Learning: 2025-11-25T14:38:07.606Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/llm.mdc:0-0
Timestamp: 2025-11-25T14:38:07.606Z
Learning: Applies to apps/web/utils/ai/**/*.ts : Use descriptive scoped loggers for each LLM feature, log inputs and outputs with appropriate log levels, and include relevant context in log messages

Applied to files:

  • apps/web/utils/ai/choose-rule/run-rules.ts
  • apps/web/utils/meeting-briefs/process.ts
📚 Learning: 2025-11-25T14:37:22.660Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/gmail-api.mdc:0-0
Timestamp: 2025-11-25T14:37:22.660Z
Learning: Applies to **/*.{ts,tsx} : Use wrapper functions for Gmail thread operations from @/utils/gmail/thread.ts instead of direct API calls

Applied to files:

  • apps/web/utils/meeting-briefs/gather-context.ts
🧬 Code graph analysis (6)
apps/web/utils/user/delete.ts (1)
apps/web/utils/redis/research-cache.ts (1)
  • clearCachedResearchForUser (39-80)
apps/web/__tests__/ai-meeting-briefing.test.ts (4)
apps/web/utils/logger.ts (1)
  • createScopedLogger (18-82)
apps/web/utils/meeting-briefs/gather-context.ts (2)
  • CalendarEvent (19-19)
  • MeetingBriefingData (26-31)
apps/web/utils/ai/meeting-briefs/generate-briefing.ts (3)
  • buildPrompt (345-397)
  • formatMeetingForContext (498-509)
  • BriefingContent (41-41)
apps/web/__tests__/helpers.ts (2)
  • getMockMessage (139-179)
  • getEmailAccount (26-46)
apps/web/utils/ai/meeting-briefs/generate-briefing.ts (8)
packages/resend/emails/meeting-briefing.tsx (1)
  • BriefingContent (19-21)
apps/web/utils/meeting-briefs/gather-context.ts (1)
  • MeetingBriefingData (26-31)
apps/web/utils/llms/types.ts (1)
  • EmailAccountWithAI (10-32)
apps/web/utils/logger.ts (1)
  • Logger (6-6)
apps/web/utils/llms/index.ts (1)
  • createGenerateText (46-137)
apps/web/env.ts (1)
  • env (17-258)
apps/web/utils/redis/research-cache.ts (2)
  • getCachedResearch (82-98)
  • setCachedResearch (100-129)
apps/web/utils/llms/config.ts (1)
  • Provider (3-12)
apps/web/utils/ai/choose-rule/run-rules.test.ts (1)
apps/web/utils/ai/choose-rule/run-rules.ts (1)
  • limitDraftEmailActions (587-638)
apps/web/utils/redis/research-cache.ts (2)
apps/web/utils/logger.ts (1)
  • createScopedLogger (18-82)
apps/web/env.ts (1)
  • env (17-258)
packages/resend/emails/meeting-briefing.tsx (1)
apps/web/components/new-landing/common/Section.tsx (1)
  • Section (9-15)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Baz Reviewer
  • GitHub Check: cubic · AI code reviewer
  • GitHub Check: Macroscope - Correctness Check
  • GitHub Check: test
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (23)
apps/web/utils/ai/choose-rule/run-rules.test.ts (4)

24-47: LGTM! Comprehensive mocking for integration tests.

The mocks are properly configured for testing runRules in isolation. The after mock executing the callback synchronously is appropriate for testing purposes.


457-520: Well-structured test for the core bug scenario.

This test directly validates the fix—ensuring that when both a custom rule and a resolved TO_REPLY rule have DRAFT_EMAIL actions, the static draft (with content) is retained while the dynamic draft (null content) is removed. The assertions also verify that isConversationRule and resolvedReason flags are preserved through the limiting process.


522-553: Good coverage for the fallback case.

This test ensures deterministic behavior when multiple AI-generated drafts (both with null content) exist—the first one encountered is retained.


556-675: Excellent integration test for the double draft prevention fix.

This test validates the complete flow end-to-end:

  1. Sets up both a custom rule (with static draft) and a TO_REPLY rule (with dynamic draft)
  2. Mocks findMatchingRules to return the custom rule + meta rule
  3. Mocks determineConversationStatus to resolve to TO_REPLY with draft
  4. Tracks what drafts are actually executed
  5. Asserts exactly one draft is executed with the expected content

This would have caught the original bug where limitDraftEmailActions ran before the conversation meta-rule was resolved. As per coding guidelines, the test is co-located with the source file and uses the proper helper functions from @/__tests__/helpers.

apps/web/utils/ai/choose-rule/run-rules.ts (4)

108-114: Clean separation of match types.

Using filter and find instead of a complex loop improves readability. The separation allows the conversation meta-rule to be resolved independently before draft limiting.


116-144: Core fix implemented correctly.

The resolution flow ensures limitDraftEmailActions (called at line 146) can now observe all DRAFT_EMAIL actions, including those from the resolved conversation rule. The isConversationRule and resolvedReason flags provide useful metadata for downstream processing.


181-193: Simplified execution loop.

The refactor removes in-loop re-evaluation of conversation status. Using result.isConversationRule for conditional logic is cleaner than re-checking rule IDs or system types.


587-589: Type-safe generic signature.

The generic constraint specifies the minimum required properties while preserving all additional properties (like isConversationRule and resolvedReason) through the spread operator in the return statement. This maintains type safety while allowing the function to work with extended match types.

apps/web/utils/email.ts (1)

145-145: Approved — mail.com is a legitimate public email provider.

The addition of mail.com to PUBLIC_EMAIL_DOMAINS is appropriate. mail.com is a long-established public email service (founded 1995, operated by United Internet AG) offering free webmail with ads and alternative domain options, making it consistent with the other major public providers already in the set.

apps/web/utils/meeting-briefs/process.ts (1)

203-207: LGTM! Logger integration enhances traceability.

The addition of the event-scoped logger parameter to aiGenerateMeetingBriefing enables better diagnostic traceability for AI briefing generation. The logger is correctly scoped to the specific event context.

Based on learnings, this follows the pattern of passing loggers as parameters to utility functions rather than creating logger instances within them.

apps/web/utils/user/delete.ts (1)

12-12: LGTM! Consistent refactoring to generalized research cache.

The changes correctly migrate from Perplexity-specific research caching to a source-agnostic research cache system. Calling clearCachedResearchForUser(userId) without a source parameter appropriately clears all research sources for the user being deleted.

Also applies to: 71-74

packages/resend/emails/meeting-briefing.tsx (1)

100-109: LGTM! Disclaimer appropriately manages user expectations.

The addition of the AI-generated content disclaimer is good practice and helps manage user expectations. The specific caveat about "common names" demonstrates awareness of entity disambiguation challenges in AI-generated briefings.

apps/web/utils/meeting-briefs/gather-context.ts (1)

88-96: LGTM! Guest mapping correctly aligned with interface.

The simplified mapping of external guests correctly matches the ExternalGuest interface definition (lines 21-24). The removal of the aiResearch field is consistent with the PR's broader refactoring away from Perplexity-specific research workflows.

apps/web/__tests__/ai-meeting-briefing.test.ts (1)

1-492: LGTM! Comprehensive test coverage for AI briefing generation.

This test file provides excellent coverage of the AI meeting briefing feature with well-structured tests across three sections:

  1. buildPrompt tests (6 tests): Cover meeting details, guest context, email history, meeting history, and multiple guests
  2. formatMeetingForContext tests (3 tests): Verify formatting, description inclusion, and truncation
  3. aiGenerateMeetingBriefing tests (6 conditional AI tests): Cover new contacts, email history, multiple guests, past meetings, edge cases, and rich context scenarios

The test file correctly follows all coding guidelines:

  • ✓ Uses vitest imports appropriately
  • ✓ Mocks server-only module
  • ✓ Conditionally runs AI tests with describe.runIf(isAiTest)
  • ✓ Uses console.debug() for LLM output
  • ✓ Placed in __tests__/ directory (correct for AI tests)
  • ✓ Uses helper functions from @/__tests__/helpers
  • ✓ Descriptive test names
  • ✓ Appropriate timeout (60s) for agentic AI workflow

The AI tests wisely provide email context to avoid sole reliance on external research APIs, making the tests more robust and predictable.

apps/web/utils/ai/meeting-briefs/generate-briefing.ts (5)

1-21: LGTM!

Imports are well-organized, using the appropriate AI SDK packages and project utilities. Logger type imported correctly for dependency injection pattern.


23-26: LGTM!

Good use of named constants for agent limits. These provide clear configuration points and prevent magic numbers throughout the code.


43-73: LGTM!

The agentic system prompt is well-structured with clear workflow steps, search tips, and briefing guidelines. The searchInputSchema correctly captures the required fields for caching.


274-342: LGTM!

The createWebSearchTool function follows a clean pattern:

  • Checks cache first
  • Falls back to actual search on cache miss
  • Fire-and-forget caching with error logging
  • Graceful error handling returning a user-friendly message

145-155: LGTM!

The fallback briefing provides a sensible default when the agent fails, and formatGuestContext correctly handles the no-prior-context case with guidance to use search tools.

Also applies to: 407-420

apps/web/utils/redis/research-cache.ts (4)

6-12: LGTM!

Good refactoring to support multiple research sources. The ResearchSource type provides type safety for the source parameter, and the updated logger scope accurately reflects the module's purpose.


18-37: LGTM!

The key structure research:{source}:{userId}:{hash} is well-designed:

  • Source-specific caching prevents collisions between Perplexity and web search results
  • The wildcard patterns in getUserKeyPattern correctly support both source-specific and all-source clearing

39-80: LGTM!

The clearCachedResearchForUser function properly handles:

  • Redis configuration check
  • Cursor-based scanning for large key sets
  • Batch deletion with count tracking
  • Comprehensive logging including source context

82-129: LGTM!

Both getCachedResearch and setCachedResearch properly:

  • Check Redis configuration before operations
  • Include source in cache keys and logging
  • Validate content before caching (empty check, size limit)
  • Handle errors gracefully with appropriate logging

Comment on lines +127 to +133
if (conversationMatch) {
const { rule, reason } = await determineConversationStatus({
conversationRules,
message,
emailAccount,
provider,
modelType,
Copy link
Contributor

Choose a reason for hiding this comment

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

When the conversation meta-rule is present we now call determineConversationStatus and only push the resolved match into matchesWithFlags when rule is truthy. If determineConversationStatus returns undefined there is no branch that records the SKIPPED result with the provided reason, so the conversation match is silently dropped and, if there are no other matches, we fall through to the generic "No rules matched" branch without surfacing the actual conversation skip reason. Could we keep emitting the SKIPPED result/reason when the conversation rule resolves to nothing so we still know why nothing executed?


Finding type: Logical Bugs

Copy link
Contributor

Choose a reason for hiding this comment

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

Commit e1c7c88 addressed this comment by introducing a skippedConversationReason variable that captures the reason when determineConversationStatus returns a falsy rule. The code now prioritizes this specific conversation skip reason over generic fallback messages when no matches are found, ensuring the actual conversation skip reason is surfaced rather than being silently dropped.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Fixed in e1c7c88. Now tracking skippedConversationReason when the conversation rule resolves to nothing, and using it in the SKIPPED result if no other matches exist.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

3 issues found across 11 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="apps/web/utils/ai/choose-rule/run-rules.ts">

<violation number="1" location="apps/web/utils/ai/choose-rule/run-rules.ts:136">
P2: When `determineConversationStatus` returns no rule but provides a skip reason, that reason is silently discarded. Consider recording a SKIPPED result with the reason so debugging can surface why the conversation rule resolved to nothing instead of falling through to a generic &quot;No rules matched&quot; message.</violation>
</file>

<file name="apps/web/utils/user/delete.ts">

<violation number="1" location="apps/web/utils/user/delete.ts:71">
P2: `clearCachedResearchForUser` catches and returns on errors internally, so this `.catch(...)` (and `captureException`) likely never runs. Either remove the `.catch` block and rely on the internal logger, or change `clearCachedResearchForUser` to rethrow so callers can handle failures.</violation>
</file>

<file name="apps/web/utils/ai/meeting-briefs/generate-briefing.ts">

<violation number="1" location="apps/web/utils/ai/meeting-briefs/generate-briefing.ts:117">
P3: Off-by-one in agent step limit: `&gt; MAX_AGENT_STEPS` allows one extra step. Use `&gt;=` to enforce the configured max.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

modelType,
isTest,
});
if (rule) {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 1, 2026

Choose a reason for hiding this comment

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

P2: When determineConversationStatus returns no rule but provides a skip reason, that reason is silently discarded. Consider recording a SKIPPED result with the reason so debugging can surface why the conversation rule resolved to nothing instead of falling through to a generic "No rules matched" message.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/utils/ai/choose-rule/run-rules.ts, line 136:

<comment>When `determineConversationStatus` returns no rule but provides a skip reason, that reason is silently discarded. Consider recording a SKIPPED result with the reason so debugging can surface why the conversation rule resolved to nothing instead of falling through to a generic &quot;No rules matched&quot; message.</comment>

<file context>
@@ -105,7 +105,45 @@ export async function runRules({
+      modelType,
+      isTest,
+    });
+    if (rule) {
+      matchesWithFlags.push({
+        rule,
</file context>

✅ Addressed in e1c7c88

Copy link
Contributor

Choose a reason for hiding this comment

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

Commit e1c7c88 addressed this comment by capturing the skip reason from determineConversationStatus in a new variable skippedConversationReason and prioritizing it when creating SKIPPED results. This ensures the specific reason why a conversation rule resolved to nothing is preserved for debugging instead of being silently discarded.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Fixed in e1c7c88. Added skippedConversationReason tracking - when the conversation rule resolves to nothing, we now preserve the skip reason and use it in the SKIPPED result.


clearCachedPerplexityResearchForUser(userId).catch((error) => {
logger.error("Error clearing cached Perplexity research", { error });
clearCachedResearchForUser(userId).catch((error) => {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 1, 2026

Choose a reason for hiding this comment

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

P2: clearCachedResearchForUser catches and returns on errors internally, so this .catch(...) (and captureException) likely never runs. Either remove the .catch block and rely on the internal logger, or change clearCachedResearchForUser to rethrow so callers can handle failures.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/utils/user/delete.ts, line 71:

<comment>`clearCachedResearchForUser` catches and returns on errors internally, so this `.catch(...)` (and `captureException`) likely never runs. Either remove the `.catch` block and rely on the internal logger, or change `clearCachedResearchForUser` to rethrow so callers can handle failures.</comment>

<file context>
@@ -68,8 +68,8 @@ export async function deleteUser({
 
-    clearCachedPerplexityResearchForUser(userId).catch((error) =&gt; {
-      logger.error(&quot;Error clearing cached Perplexity research&quot;, { error });
+    clearCachedResearchForUser(userId).catch((error) =&gt; {
+      logger.error(&quot;Error clearing cached research&quot;, { error });
       captureException(error);
</file context>
Fix with Cubic

stopWhen: (stepResult) =>
stepResult.steps.some((step) =>
step.toolCalls?.some((call) => call.toolName === "finalizeBriefing"),
) || stepResult.steps.length > MAX_AGENT_STEPS,
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 1, 2026

Choose a reason for hiding this comment

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

P3: Off-by-one in agent step limit: > MAX_AGENT_STEPS allows one extra step. Use >= to enforce the configured max.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/utils/ai/meeting-briefs/generate-briefing.ts, line 117:

<comment>Off-by-one in agent step limit: `&gt; MAX_AGENT_STEPS` allows one extra step. Use `&gt;=` to enforce the configured max.</comment>

<file context>
@@ -22,55 +38,307 @@ const briefingSchema = z.object({
+    stopWhen: (stepResult) =&gt;
+      stepResult.steps.some((step) =&gt;
+        step.toolCalls?.some((call) =&gt; call.toolName === &quot;finalizeBriefing&quot;),
+      ) || stepResult.steps.length &gt; MAX_AGENT_STEPS,
+    tools: {
+      ...tools,
</file context>
Suggested change
) || stepResult.steps.length > MAX_AGENT_STEPS,
) || stepResult.steps.length >= MAX_AGENT_STEPS,
Fix with Cubic

@@ -141,35 +185,10 @@ export async function runRules({
const executedRules: RunRulesResult[] = [];
Copy link
Contributor

Choose a reason for hiding this comment

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

When a conversation meta-rule matches but determineConversationStatus returns no rule, the skip isn’t recorded if other rules also matched. Consider adding a SKIPPED result (and DB record) for the conversation rule even when other rules apply, so the skip is captured consistently.

🚀 Want me to fix this? Reply ex: "fix it for me".

Copy link
Owner Author

Choose a reason for hiding this comment

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

Not addressing this. When other rules match and execute, they get stored in the database - the user can see what happened. Recording a SKIPPED result for the conversation rule alongside executed rules would add noise. The skip reason is primarily useful when nothing at all matched, which is now handled with skippedConversationReason.

@elie222 elie222 merged commit 883ea6a into main Jan 1, 2026
32 of 33 checks passed
@elie222 elie222 deleted the fix/double-draft-prevention branch January 1, 2026 15:49
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.

1 participant