-
Notifications
You must be signed in to change notification settings - Fork 1.3k
fix: prevent double draft emails when multi-rule selection enabled #1163
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2be0caf
2de7d01
6ff89ca
bf92508
e1c7c88
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -105,15 +105,59 @@ export async function runRules({ | |
| logger, | ||
| }); | ||
|
|
||
| const finalMatches = limitDraftEmailActions(conversationAwareMatches, logger); | ||
| // Separate regular matches from conversation meta-rule | ||
| const regularMatches = conversationAwareMatches.filter( | ||
| (m) => !isConversationRule(m.rule.id), | ||
| ); | ||
| const conversationMatch = conversationAwareMatches.find((m) => | ||
| isConversationRule(m.rule.id), | ||
| ); | ||
|
|
||
| // Resolve conversation meta-rule to actual rule (e.g., TO_REPLY) | ||
| const matchesWithFlags: { | ||
| rule: RuleWithActions; | ||
| matchReasons?: MatchReason[]; | ||
| resolvedReason?: string; | ||
| isConversationRule: boolean; | ||
| }[] = regularMatches.map((m) => ({ | ||
| ...m, | ||
| isConversationRule: false, | ||
| })); | ||
|
|
||
| let skippedConversationReason: string | undefined; | ||
|
|
||
| if (conversationMatch) { | ||
| const { rule, reason } = await determineConversationStatus({ | ||
| conversationRules, | ||
| message, | ||
| emailAccount, | ||
| provider, | ||
| modelType, | ||
| isTest, | ||
| }); | ||
| if (rule) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: When Prompt for AI agents✅ Addressed in
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Commit e1c7c88 addressed this comment by capturing the skip reason from
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in e1c7c88. Added |
||
| matchesWithFlags.push({ | ||
| rule, | ||
| matchReasons: conversationMatch.matchReasons, | ||
| resolvedReason: reason, | ||
| isConversationRule: true, | ||
| }); | ||
| } else { | ||
| // Track why conversation rule was skipped (e.g., determined FYI but rule disabled) | ||
| skippedConversationReason = reason; | ||
| } | ||
| } | ||
|
|
||
| const finalMatches = limitDraftEmailActions(matchesWithFlags, logger); | ||
|
|
||
| logger.trace("Matching rule", () => ({ | ||
| module: MODULE, | ||
| results: finalMatches.map(filterNullProperties), | ||
| })); | ||
|
|
||
| if (!finalMatches.length) { | ||
| const reason = results.reasoning || "No rules matched"; | ||
| const reason = | ||
| skippedConversationReason || results.reasoning || "No rules matched"; | ||
| if (!isTest) { | ||
| await prisma.executedRule.create({ | ||
| data: { | ||
|
|
@@ -141,35 +185,10 @@ export async function runRules({ | |
| const executedRules: RunRulesResult[] = []; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When a conversation meta-rule matches but
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
|
||
| for (const result of finalMatches) { | ||
| let ruleToExecute = result.rule; | ||
| let reasonToUse = results.reasoning; | ||
|
|
||
| if (result.rule && isConversationRule(result.rule.id)) { | ||
| const { rule: statusRule, reason: statusReason } = | ||
| await determineConversationStatus({ | ||
| conversationRules, | ||
| message, | ||
| emailAccount, | ||
| provider, | ||
| modelType, | ||
| isTest, | ||
| }); | ||
|
|
||
| if (!statusRule) { | ||
| const executedRule: RunRulesResult = { | ||
| rule: null, | ||
| reason: statusReason || "No enabled conversation status rule found", | ||
| createdAt: batchTimestamp, | ||
| status: ExecutedRuleStatus.SKIPPED, | ||
| }; | ||
| const ruleToExecute = result.rule; | ||
| const reasonToUse = result.resolvedReason || results.reasoning; | ||
|
|
||
| executedRules.push(executedRule); | ||
| continue; | ||
| } | ||
|
|
||
| ruleToExecute = statusRule; | ||
| reasonToUse = statusReason; | ||
| } else { | ||
| if (!result.isConversationRule) { | ||
| analyzeSenderPatternIfAiMatch({ | ||
| isTest, | ||
| result, | ||
|
|
@@ -571,16 +590,9 @@ function isConversationRule(ruleId: string): boolean { | |
| * If there are no draft email actions, we return the matches as is. | ||
| * If there is only one draft email action, we return the matches as is. | ||
| */ | ||
| export function limitDraftEmailActions( | ||
| matches: { | ||
| rule: RuleWithActions; | ||
| matchReasons?: MatchReason[]; | ||
| }[], | ||
| logger: Logger, | ||
| ): { | ||
| rule: RuleWithActions; | ||
| matchReasons?: MatchReason[]; | ||
| }[] { | ||
| export function limitDraftEmailActions< | ||
| T extends { rule: RuleWithActions; matchReasons?: MatchReason[] }, | ||
| >(matches: T[], logger: Logger): T[] { | ||
| const draftCandidates = matches.flatMap((match) => | ||
| match.rule.actions | ||
| .filter((action) => action.type === ActionType.DRAFT_EMAIL) | ||
|
|
||
There was a problem hiding this comment.
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
determineConversationStatusand only push the resolved match intomatchesWithFlagswhenruleis truthy. IfdetermineConversationStatusreturnsundefinedthere is no branch that records the SKIPPED result with the providedreason, 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/reasonwhen the conversation rule resolves to nothing so we still know why nothing executed?Finding type:
Logical BugsThere was a problem hiding this comment.
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
skippedConversationReasonvariable that captures the reason whendetermineConversationStatusreturns 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.There was a problem hiding this comment.
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
skippedConversationReasonwhen the conversation rule resolves to nothing, and using it in the SKIPPED result if no other matches exist.