-
Notifications
You must be signed in to change notification settings - Fork 74
fix: delete local drafts when email is reclassified or thread is archived #72
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
base: main
Are you sure you want to change the base?
Changes from all commits
bab1f1a
022c800
904952d
083b5bf
89a9b4d
0444b46
d8a3f6f
af350f8
65ee4d5
eb6465d
815fcf6
2534f8c
5683ffa
6a3645f
5644e10
477af70
a4f864b
d14762b
bd6eb30
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
|
ankitvgupta marked this conversation as resolved.
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,7 @@ import { | |
| learnFromPriorityOverrideInferred, | ||
| } from "../services/analysis-edit-learner"; | ||
| import { stripQuotedContent } from "../services/strip-quoted-content"; | ||
| import { deleteGmailDraftById } from "../services/gmail-draft-sync"; | ||
| import { createLogger } from "../services/logger"; | ||
|
|
||
| const log = createLogger("analysis-ipc"); | ||
|
|
@@ -110,8 +111,20 @@ export function registerAnalysisIpc(): void { | |
|
|
||
| const result = await analyzerInstance.analyze(emailForAnalysis, userEmail, email.accountId); | ||
|
|
||
| // Save analysis to database | ||
| saveAnalysis(emailId, result.needs_reply, result.reason, result.priority); | ||
| // Save analysis to database (also cleans up thread drafts if reclassified as skip) | ||
| const draftCleanup = saveAnalysis( | ||
| emailId, | ||
| result.needs_reply, | ||
| result.reason, | ||
| result.priority, | ||
| ); | ||
| if (draftCleanup) { | ||
| for (const cleanup of draftCleanup) { | ||
| if (cleanup.gmailDraftId && cleanup.accountId) { | ||
| deleteGmailDraftById(cleanup.accountId, cleanup.gmailDraftId).catch(() => {}); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Return updated email with analysis | ||
| const updatedEmail = getEmail(emailId); | ||
|
|
@@ -188,7 +201,19 @@ export function registerAnalysisIpc(): void { | |
| userEmail, | ||
| email.accountId, | ||
| ); | ||
| saveAnalysis(emailId, result.needs_reply, result.reason, result.priority); | ||
| const batchCleanup = saveAnalysis( | ||
| emailId, | ||
| result.needs_reply, | ||
| result.reason, | ||
| result.priority, | ||
| ); | ||
| if (batchCleanup) { | ||
| for (const cleanup of batchCleanup) { | ||
| if (cleanup.gmailDraftId && cleanup.accountId) { | ||
| deleteGmailDraftById(cleanup.accountId, cleanup.gmailDraftId).catch(() => {}); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const updatedEmail = getEmail(emailId); | ||
| if (updatedEmail) { | ||
|
|
@@ -239,18 +264,45 @@ export function registerAnalysisIpc(): void { | |
| const originalNeedsReply = originalAnalysis?.needsReply ?? false; | ||
| const originalPriority = originalAnalysis?.priority ?? null; | ||
|
|
||
| // Update the analysis in DB | ||
| saveAnalysis( | ||
| // Normalize: the UI represents "skip" as priority=null + needsReply=false, | ||
| // but saveAnalysis checks for the literal string "skip" to trigger draft cleanup. | ||
| const normalizedPriority = | ||
| newPriority === null && !newNeedsReply ? "skip" : (newPriority ?? undefined); | ||
|
|
||
| // Update the analysis in DB (also deletes local draft + trace if reclassified as skip) | ||
| const draftCleanup = saveAnalysis( | ||
|
ankitvgupta marked this conversation as resolved.
|
||
| emailId, | ||
| newNeedsReply, | ||
| originalAnalysis?.reason ?? "User override", | ||
| newPriority ?? undefined, | ||
| normalizedPriority, | ||
| ); | ||
|
|
||
| log.info( | ||
| `[Analysis] Priority overridden for ${emailId}: ${originalPriority ?? "skip"} → ${newPriority ?? "skip"}`, | ||
| ); | ||
|
|
||
| // If reclassified as skip and thread had drafts, clean up Gmail drafts and cancel agents | ||
| if (draftCleanup) { | ||
| log.info( | ||
| `[Analysis] Cleaning up ${draftCleanup.length} thread draft(s) for ${emailId} after skip reclassification`, | ||
| ); | ||
| const { agentCoordinator } = await import("../agents/agent-coordinator"); | ||
| for (const cleanup of draftCleanup) { | ||
| if (cleanup.gmailDraftId && cleanup.accountId) { | ||
| deleteGmailDraftById(cleanup.accountId, cleanup.gmailDraftId).catch(() => {}); | ||
| } | ||
| if (cleanup.agentTaskId) { | ||
| agentCoordinator.cancel(cleanup.agentTaskId); | ||
| } | ||
| } | ||
| // Cancel any in-flight auto-draft agent | ||
| const { prefetchService } = await import("../services/prefetch-service"); | ||
| const activeTaskId = prefetchService.getActiveAgentTaskId(emailId); | ||
| if (activeTaskId) { | ||
| agentCoordinator.cancel(activeTaskId); | ||
| } | ||
|
Comment on lines
+298
to
+303
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. 🟡 Agent cancellation on skip reclassification only checks a single emailId, missing in-flight agents on other thread emails When the user reclassifies an email as "skip" via Concrete scenario
Prompt for agentsWas this helpful? React with 👍 or 👎 to provide feedback. |
||
| } | ||
|
|
||
| // Learn from the override in the background (don't block the UI) | ||
| const accountId = email.accountId ?? "default"; | ||
| const senderMatch = email.from.match(/<([^>]+)>/) ?? email.from.match(/([^\s<]+@[^\s>]+)/); | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -16,11 +16,14 @@ import { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| getSendAsAliases, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| getSendAsAliasFetchedAt, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| upsertSendAsAliases, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| deleteThreadDrafts, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| getThreadDraftBody, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } from "../db"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { networkMonitor } from "../services/network-monitor"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { outboxService } from "../services/outbox-service"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { prefetchService } from "../services/prefetch-service"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { isNetworkError } from "../services/network-errors"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { deleteGmailDraftById } from "../services/gmail-draft-sync"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { learnFromDraftEdit } from "../services/draft-edit-learner"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| IpcResponse, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -302,11 +305,21 @@ export function registerComposeIpc(): void { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await new Promise((resolve) => setTimeout(resolve, 500)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Still trigger draft-edit learning in demo mode so we can test it | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (options.threadId && !options.isForward) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const draftSnapshot = getThreadDraftBody(options.threadId, options.accountId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const demoCleanups = deleteThreadDrafts(options.threadId, options.accountId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Cancel in-flight agents (agent drafts now run in demo mode) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const cleanup of demoCleanups) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (cleanup.agentTaskId) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { agentCoordinator } = await import("../agents/agent-coordinator"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| agentCoordinator.cancel(cleanup.agentTaskId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| learnFromDraftEdit({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| threadId: options.threadId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| accountId: options.accountId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sentBodyHtml: options.bodyHtml || "", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sentBodyText: options.bodyText, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| draftSnapshot: draftSnapshot ?? undefined, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .then((result) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (result && (result.promoted.length > 0 || result.draftMemoriesCreated > 0)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -356,20 +369,45 @@ export function registerComposeIpc(): void { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { success: true, data: result }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Snapshot draft body BEFORE send so it's available for learning even if | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // the draft is deleted (e.g. user archives the thread immediately after send). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const draftSnapshot = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| options.threadId && !options.isForward | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? getThreadDraftBody(options.threadId, options.accountId) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = await client.sendMessage(options); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // After sending a reply, mark the thread as read and re-queue analysis | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Skip for forwards — forwarding doesn't mean the user addressed the original conversation | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // After sending a reply, clean up thread drafts and re-queue analysis. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Skip for forwards — forwarding doesn't mean the user addressed the original conversation. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (options.threadId && !options.isForward) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| triggerThreadReanalysis(options.threadId, options.accountId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Fire-and-forget: mark thread read so Gmail shows it as read | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| markThreadAsReadAfterSend(client, options.threadId, options.accountId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Clean up the AI draft now that the reply has been sent. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Must happen after snapshotting the draft body (for learning) but before | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // returning to the renderer — prevents a race where the user archives the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // thread immediately and deleteThreadDrafts tries to delete the Gmail draft | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // that was already consumed or is no longer relevant. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const draftCleanups = deleteThreadDrafts(options.threadId, options.accountId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const cleanup of draftCleanups) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (cleanup.gmailDraftId) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| deleteGmailDraftById(options.accountId, cleanup.gmailDraftId).catch(() => {}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (cleanup.agentTaskId) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { agentCoordinator } = await import("../agents/agent-coordinator"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| agentCoordinator.cancel(cleanup.agentTaskId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
ankitvgupta marked this conversation as resolved.
Comment on lines
+388
to
+402
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. 🟡 Inconsistent indentation in draft cleanup block makes code misleading The draft cleanup code block at lines 388-402 is indented at a different level (10 spaces) compared to the surrounding code inside the same
Suggested change
Was this helpful? React with 👍 or 👎 to provide feedback. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Fire-and-forget: learn from draft edits (compare AI draft vs what was sent) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| learnFromDraftEdit({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| threadId: options.threadId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| accountId: options.accountId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sentBodyHtml: options.bodyHtml || "", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sentBodyText: options.bodyText, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| draftSnapshot: draftSnapshot ?? undefined, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .then((result) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (result && (result.promoted.length > 0 || result.draftMemoriesCreated > 0)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.