diff --git a/apps/web/__tests__/determine-thread-status.test.ts b/apps/web/__tests__/determine-thread-status.test.ts index 67cdaf0a06..8153bb6c50 100644 --- a/apps/web/__tests__/determine-thread-status.test.ts +++ b/apps/web/__tests__/determine-thread-status.test.ts @@ -1,6 +1,11 @@ import { describe, expect, test, vi, beforeEach } from "vitest"; import { aiDetermineThreadStatus } from "@/utils/ai/reply/determine-thread-status"; -import { getEmailAccount, getEmail } from "@/__tests__/helpers"; +import { + getEmailAccount, + getEmail, + generateSequentialDates, +} from "@/__tests__/helpers"; +import { SystemType } from "@prisma/client"; // Run with: pnpm test-ai determine-thread-status @@ -16,47 +21,53 @@ describe.runIf(isAiTest)("aiDetermineThreadStatus", () => { vi.clearAllMocks(); }); - // Helper for multi-person thread tests - const getProjectThread = () => [ - getEmail({ - from: "bob@company.com", - to: "alice@company.com, carol@company.com", - subject: "Re: Q4 Project Timeline", - content: "Alice, can you send me the final design mockups by Friday?", - }), - getEmail({ - from: "alice@company.com", - to: "bob@company.com, carol@company.com", - subject: "Re: Q4 Project Timeline", - content: "I'm working on them. Should have v1 by Thursday.", - }), - getEmail({ - from: "bob@company.com", - to: "alice@company.com, carol@company.com", - subject: "Re: Q4 Project Timeline", - content: "Great! Carol, can you check the API endpoints?", - }), - getEmail({ - from: "carol@company.com", - to: "bob@company.com, alice@company.com", - subject: "Re: Q4 Project Timeline", - content: "Sure, I'll review them today and let you know.", - }), - getEmail({ - from: "alice@company.com", - to: "bob@company.com, carol@company.com", - subject: "Re: Q4 Project Timeline", - content: - "Bob, quick question - do you need mobile mockups too or just desktop?", - }), - getEmail({ - from: "bob@company.com", - to: "alice@company.com, carol@company.com", - subject: "Re: Q4 Project Timeline", - content: - "Yes please include mobile mockups. That would be really helpful.", - }), - ]; + // Helper for multi-person thread tests (chronological order with dates) + const getProjectThread = () => { + const emailData = [ + { + from: "bob@company.com", + to: "alice@company.com, carol@company.com", + subject: "Re: Q4 Project Timeline", + content: "Alice, can you send me the final design mockups by Friday?", + }, + { + from: "alice@company.com", + to: "bob@company.com, carol@company.com", + subject: "Re: Q4 Project Timeline", + content: "I'm working on them. Should have v1 by Thursday.", + }, + { + from: "bob@company.com", + to: "alice@company.com, carol@company.com", + subject: "Re: Q4 Project Timeline", + content: "Great! Carol, can you check the API endpoints?", + }, + { + from: "carol@company.com", + to: "bob@company.com, alice@company.com", + subject: "Re: Q4 Project Timeline", + content: "Sure, I'll review them today and let you know.", + }, + { + from: "alice@company.com", + to: "bob@company.com, carol@company.com", + subject: "Re: Q4 Project Timeline", + content: + "Bob, quick question - do you need mobile mockups too or just desktop?", + }, + { + from: "bob@company.com", + to: "alice@company.com, carol@company.com", + subject: "Re: Q4 Project Timeline", + content: + "Yes please include mobile mockups. That would be really helpful.", + }, + ]; + const dates = generateSequentialDates(emailData.length, 2); // 2 hours apart + return emailData.map((email, index) => + getEmail({ ...email, date: dates[index] }), + ); + }; test( "identifies TO_REPLY when receiving a question", @@ -75,7 +86,7 @@ describe.runIf(isAiTest)("aiDetermineThreadStatus", () => { }); console.debug("Result:", result); - expect(result.status).toBe("TO_REPLY"); + expect(result.status).toBe(SystemType.TO_REPLY); expect(result.rationale).toBeDefined(); }, TIMEOUT, @@ -98,7 +109,7 @@ describe.runIf(isAiTest)("aiDetermineThreadStatus", () => { }); console.debug("Result:", result); - expect(result.status).toBe("FYI"); + expect(result.status).toBe(SystemType.FYI); expect(result.rationale).toBeDefined(); }, TIMEOUT, @@ -121,7 +132,7 @@ describe.runIf(isAiTest)("aiDetermineThreadStatus", () => { }); console.debug("Result:", result); - expect(result.status).toBe("AWAITING_REPLY"); + expect(result.status).toBe(SystemType.AWAITING_REPLY); expect(result.rationale).toBeDefined(); }, TIMEOUT, @@ -132,18 +143,18 @@ describe.runIf(isAiTest)("aiDetermineThreadStatus", () => { async () => { const emailAccount = getEmailAccount(); const messages = [ - getEmail({ - from: "recipient@example.com", - to: emailAccount.email, - subject: "Re: Report request", - content: "I'll get this for you tomorrow.", - }), getEmail({ from: emailAccount.email, to: "recipient@example.com", subject: "Report request", content: "Could you send me the Q3 report?", }), + getEmail({ + from: "recipient@example.com", + to: emailAccount.email, + subject: "Re: Report request", + content: "I'll get this for you tomorrow.", + }), ]; const result = await aiDetermineThreadStatus({ @@ -152,7 +163,7 @@ describe.runIf(isAiTest)("aiDetermineThreadStatus", () => { }); console.debug("Result:", result); - expect(result.status).toBe("AWAITING_REPLY"); + expect(result.status).toBe(SystemType.AWAITING_REPLY); expect(result.rationale).toBeDefined(); }, TIMEOUT, @@ -166,8 +177,8 @@ describe.runIf(isAiTest)("aiDetermineThreadStatus", () => { getEmail({ from: "recipient@example.com", to: emailAccount.email, - subject: "Re: Question", - content: "Perfect, thanks!", + subject: "Question", + content: "Can you send me the report?", }), getEmail({ from: emailAccount.email, @@ -178,8 +189,8 @@ describe.runIf(isAiTest)("aiDetermineThreadStatus", () => { getEmail({ from: "recipient@example.com", to: emailAccount.email, - subject: "Question", - content: "Can you send me the report?", + subject: "Re: Question", + content: "Perfect, thanks!", }), ]; @@ -189,7 +200,7 @@ describe.runIf(isAiTest)("aiDetermineThreadStatus", () => { }); console.debug("Result:", result); - expect(result.status).toBe("ACTIONED"); + expect(result.status).toBe(SystemType.ACTIONED); expect(result.rationale).toBeDefined(); }, TIMEOUT, @@ -203,14 +214,14 @@ describe.runIf(isAiTest)("aiDetermineThreadStatus", () => { getEmail({ from: "sender@example.com", to: emailAccount.email, - subject: "Re: Two things", - content: "Also, FYI the meeting moved to 3pm.", + subject: "Two things", + content: "Can you send me the Q3 report?", }), getEmail({ from: "sender@example.com", to: emailAccount.email, - subject: "Two things", - content: "Can you send me the Q3 report?", + subject: "Re: Two things", + content: "Also, FYI the meeting moved to 3pm.", }), ]; @@ -220,7 +231,7 @@ describe.runIf(isAiTest)("aiDetermineThreadStatus", () => { }); console.debug("Result:", result); - expect(result.status).toBe("TO_REPLY"); + expect(result.status).toBe(SystemType.TO_REPLY); expect(result.rationale).toBeDefined(); }, TIMEOUT, @@ -231,18 +242,18 @@ describe.runIf(isAiTest)("aiDetermineThreadStatus", () => { async () => { const emailAccount = getEmailAccount(); const messages = [ - getEmail({ - from: emailAccount.email, - to: "recipient@example.com", - subject: "Re: Quick question", - content: "Yes, 3pm works. See you then.", - }), getEmail({ from: "recipient@example.com", to: emailAccount.email, subject: "Quick question", content: "Can you confirm the meeting time?", }), + getEmail({ + from: emailAccount.email, + to: "recipient@example.com", + subject: "Re: Quick question", + content: "Yes, 3pm works. See you then.", + }), ]; const result = await aiDetermineThreadStatus({ @@ -251,7 +262,9 @@ describe.runIf(isAiTest)("aiDetermineThreadStatus", () => { }); console.debug("Result:", result); - expect(["ACTIONED", "AWAITING_REPLY"]).toContain(result.status); + expect([SystemType.ACTIONED, SystemType.AWAITING_REPLY]).toContain( + result.status, + ); expect(result.rationale).toBeDefined(); }, TIMEOUT, @@ -285,11 +298,11 @@ describe.runIf(isAiTest)("aiDetermineThreadStatus", () => { const result = await aiDetermineThreadStatus({ emailAccount, - threadMessages: messages.reverse(), // Most recent first + threadMessages: messages, }); console.debug("Result:", result); - expect(result.status).toBe("TO_REPLY"); + expect(result.status).toBe(SystemType.TO_REPLY); expect(result.rationale).toBeDefined(); }, TIMEOUT, @@ -312,7 +325,7 @@ describe.runIf(isAiTest)("aiDetermineThreadStatus", () => { }); console.debug("Result:", result); - expect(result.status).toBe("FYI"); + expect(result.status).toBe(SystemType.FYI); expect(result.rationale).toBeDefined(); }, TIMEOUT, @@ -330,7 +343,7 @@ describe.runIf(isAiTest)("aiDetermineThreadStatus", () => { console.debug("Alice's perspective:", result); // Alice asked about mobile mockups, Bob said "Yes please include" - Alice should acknowledge - expect(result.status).toBe("TO_REPLY"); + expect(result.status).toBe(SystemType.TO_REPLY); expect(result.rationale).toBeDefined(); }, TIMEOUT, @@ -348,14 +361,14 @@ describe.runIf(isAiTest)("aiDetermineThreadStatus", () => { console.debug("Bob's perspective:", result); // Bob is waiting for Alice to deliver mockups and Carol to report on API review - expect(result.status).toBe("AWAITING_REPLY"); + expect(result.status).toBe(SystemType.AWAITING_REPLY); expect(result.rationale).toBeDefined(); }, TIMEOUT, ); test( - "handles complex multi-person thread - Carol's perspective (AWAITING_REPLY)", + "handles complex multi-person thread - Carol's perspective (TO_REPLY)", async () => { const carol = getEmailAccount({ email: "carol@company.com" }); @@ -365,63 +378,69 @@ describe.runIf(isAiTest)("aiDetermineThreadStatus", () => { }); console.debug("Carol's perspective:", result); - // Carol committed to reviewing API endpoints and reporting back - expect(result.status).toBe("AWAITING_REPLY"); + // Carol committed to reviewing API endpoints and reporting back - she needs to follow through + expect(result.status).toBe(SystemType.TO_REPLY); expect(result.rationale).toBeDefined(); }, TIMEOUT, ); - // Helper for lunch scheduling thread tests + // Helper for lunch scheduling thread tests (chronological order with dates) const getLunchSchedulingThread = ( person1Email: string, person2Email: string, - ) => [ - getEmail({ - from: person1Email, - to: person2Email, - subject: "Re: free for lunch tomorrow?", - content: "I'll get back to you soon!", - }), - getEmail({ - from: person2Email, - to: person1Email, - subject: "Re: free for lunch tomorrow?", - content: "Ok. 5pm work tomorrow?", - }), - getEmail({ - from: person1Email, - to: person2Email, - subject: "Re: free for lunch tomorrow?", - content: "Sounds good, let me know.", - }), - getEmail({ - from: person2Email, - to: person1Email, - subject: "Re: free for lunch tomorrow?", - content: "Let me get back to you about that soon!", - }), - getEmail({ - from: person1Email, - to: person2Email, - subject: "Re: free for lunch tomorrow?", - content: - "Great, does 12pm work for you? Let me know and I can book a table somewhere.", - }), - getEmail({ - from: person2Email, - to: person1Email, - subject: "Re: free for lunch tomorrow?", - content: - "Yes, I'd love to. I'm free from 11 am to 1 pm tomorrow, would any time then work for you?", - }), - getEmail({ - from: person1Email, - to: person2Email, - subject: "free for lunch tomorrow?", - content: "Lmk if you're free", - }), - ]; + ) => { + const emailData = [ + { + from: person1Email, + to: person2Email, + subject: "free for lunch tomorrow?", + content: "Lmk if you're free", + }, + { + from: person2Email, + to: person1Email, + subject: "Re: free for lunch tomorrow?", + content: + "Yes, I'd love to. I'm free from 11 am to 1 pm tomorrow, would any time then work for you?", + }, + { + from: person1Email, + to: person2Email, + subject: "Re: free for lunch tomorrow?", + content: + "Great, does 12pm work for you? Let me know and I can book a table somewhere.", + }, + { + from: person2Email, + to: person1Email, + subject: "Re: free for lunch tomorrow?", + content: "Let me get back to you about that soon!", + }, + { + from: person1Email, + to: person2Email, + subject: "Re: free for lunch tomorrow?", + content: "Sounds good, let me know.", + }, + { + from: person2Email, + to: person1Email, + subject: "Re: free for lunch tomorrow?", + content: "Ok. 5pm work tomorrow?", + }, + { + from: person1Email, + to: person2Email, + subject: "Re: free for lunch tomorrow?", + content: "I'll get back to you soon!", + }, + ]; + const dates = generateSequentialDates(emailData.length, 3); // 3 hours apart + return emailData.map((email, index) => + getEmail({ ...email, date: dates[index] }), + ); + }; test( "identifies AWAITING_REPLY when other person says they'll get back to you (lunch scheduling)", @@ -438,7 +457,7 @@ describe.runIf(isAiTest)("aiDetermineThreadStatus", () => { console.debug("Result:", result); // Oliver said "I'll get back to you soon!" so Alice should be awaiting his reply - expect(result.status).toBe("AWAITING_REPLY"); + expect(result.status).toBe(SystemType.AWAITING_REPLY); expect(result.rationale).toBeDefined(); }, TIMEOUT, @@ -459,7 +478,72 @@ describe.runIf(isAiTest)("aiDetermineThreadStatus", () => { console.debug("Result:", result); // Oliver committed to getting back to Alice about the 5pm time, so he needs to reply - expect(result.status).toBe("TO_REPLY"); + expect(result.status).toBe(SystemType.TO_REPLY); + expect(result.rationale).toBeDefined(); + }, + TIMEOUT, + ); + + test( + "identifies FYI when receiving instructions after offering help (not awaiting reply)", + async () => { + const emailAccount = getEmailAccount(); + const messages = [ + // Original message asking what platform can do + getEmail({ + from: "team@platform.com", + to: emailAccount.email, + subject: "Platform Weekly Update", + content: `We send these personalized updates to help our community grow. Let us know what else we can do to help you grow! + +[... rest of newsletter content ...]`, + }), + // User offered to help platform users + getEmail({ + from: emailAccount.email, + to: "team@platform.com", + subject: "Re: Platform Weekly Update", + content: `Hey, I'd be happy to offer platform users a special discount if anyone is interested. +Let me know!`, + }), + // Latest message: Platform Support provides instructions + getEmail({ + from: "support@platform.com", + to: emailAccount.email, + subject: "Re: Platform Weekly Update", + content: `Hi, + +Here's how to get your product listed on our platform: + +If your product is not listed yet: + +1. Go to our registration page +2. Add your product name +3. Select your company +4. Choose relevant categories +5. Complete your product page with description, screenshots, and pricing + +To get more visibility: + +- Get at least 3 user reviews +- Complete your company profile fully +- Add detailed product information + +Best regards, +Platform Support`, + }), + ]; + + const result = await aiDetermineThreadStatus({ + emailAccount, + threadMessages: messages, + }); + + console.debug("Result:", result); + // ABC provided the help/instructions. User is not waiting for ABC to do something. + // The ball is in the user's court to act on the information if they want to. + // This should be FYI (informational) or TO_REPLY (if user wants to act), but NOT AWAITING_REPLY + expect([SystemType.FYI, SystemType.TO_REPLY]).toContain(result.status); expect(result.rationale).toBeDefined(); }, TIMEOUT, diff --git a/apps/web/__tests__/helpers.ts b/apps/web/__tests__/helpers.ts index 9c418220fd..659e530332 100644 --- a/apps/web/__tests__/helpers.ts +++ b/apps/web/__tests__/helpers.ts @@ -11,6 +11,7 @@ export function getEmailAccount( userId: "user1", email: overrides.email || "user@test.com", about: null, + multiRuleSelectionEnabled: false, user: { aiModel: null, aiProvider: null, @@ -22,6 +23,25 @@ export function getEmailAccount( }; } +/** + * Helper to generate sequential dates for email threads. + * Each date is hoursApart hours after the previous one. + * @param count - Number of dates to generate + * @param hoursApart - Hours between each message (default: 1) + * @param startDate - Starting date (default: 7 days ago) + */ +export function generateSequentialDates( + count: number, + hoursApart = 1, + startDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), +): Date[] { + return Array.from({ length: count }, (_, i) => { + const date = new Date(startDate); + date.setHours(date.getHours() + i * hoursApart); + return date; + }); +} + export function getEmail({ from = "user@test.com", to = "user2@test.com", @@ -29,6 +49,7 @@ export function getEmail({ content = "Test content", replyTo, cc, + date, }: Partial = {}): EmailForLLM { return { id: "email-id", @@ -38,6 +59,7 @@ export function getEmail({ content, ...(replyTo && { replyTo }), ...(cc && { cc }), + ...(date && { date }), }; } diff --git a/apps/web/utils/ai/reply/determine-thread-status.ts b/apps/web/utils/ai/reply/determine-thread-status.ts index 5152e083b0..92d25aaedc 100644 --- a/apps/web/utils/ai/reply/determine-thread-status.ts +++ b/apps/web/utils/ai/reply/determine-thread-status.ts @@ -20,26 +20,34 @@ export async function aiDetermineThreadStatus({ Your task is to determine the current status of an email thread from the user's perspective. The thread can be in ONE of these mutually exclusive states: +* TO_REPLY - We need to reply +* AWAITING_REPLY - We're waiting for them to reply +* FYI - No reply needed +* ACTIONED - Thread is complete + +DETAILED CRITERIA: + **TO_REPLY**: The user has received email(s) that require a response. Use this when: - Someone asks the user a direct question - Someone requests information or action from the user - The user needs to provide specific input - Someone follows up on a conversation requiring the user's response - There are ANY unanswered questions/requests in the thread that the user hasn't addressed yet +- The user made a promise to get back to someone and hasn't followed through yet + +**AWAITING_REPLY**: Waiting for the other person to take action or respond. Use this when: +- The user asked a question and is still waiting for an answer +- The user requested information/action and is still waiting for it to be delivered +- Someone ELSE promised to do something and hasn't done it yet (e.g., "I'll get back to you tomorrow") +- The ball is in their court - it's THEIR turn to respond or act +- The user is NOT the one who needs to reply next -**FYI**: The user received important email(s) that don't require a response. Use this when: +**FYI**: The thread no longer needs any response, but the content in the last messages needs the user's attention. Use this when: - Important updates, announcements, or information the user should be aware of - CC'd on important matters for awareness only - Status updates that are valuable to know but don't need acknowledgment - NO questions or requests exist anywhere in the thread -**AWAITING_REPLY**: Waiting for the other person to take action or respond. Use this when: -- The user asked a question and is waiting for an answer -- The user requested information/action and is waiting for it to be delivered -- Someone promised to do something (e.g., "I'll get this for you tomorrow", "Let me check and get back to you") -- It's the other person's turn to deliver something or take action -- The ball is in their court - **ACTIONED**: The thread is complete/done. No further action needed from anyone. Use this when: - All questions have been answered - All requests have been fulfilled @@ -49,7 +57,9 @@ Your task is to determine the current status of an email thread from the user's CRITICAL RULES - READ CAREFULLY: 1. **CHECK EVERY MESSAGE**: Don't just look at the latest message. Scan the ENTIRE thread for unanswered questions or pending requests 2. **Unanswered questions persist**: If message #1 asks "Can you send me the report?" and message #2 says "FYI, meeting moved to 3pm", the status is still TO_REPLY because the report request is unanswered -3. **Promises = AWAITING_REPLY**: If someone says "I'll get back to you", "Let me check", "I'll send that tomorrow" → AWAITING_REPLY (not FYI) +3. **Promises from different perspectives**: + - If SOMEONE ELSE says "I'll get back to you" → AWAITING_REPLY (waiting for them) + - If YOU said "I'll get back to you" → TO_REPLY (you need to follow through on your promise) 4. **Latest message context matters**: If the latest message is purely FYI but there are unresolved items earlier in the thread, prioritize the unresolved items 5. **FYI is only when nothing is pending**: Use FYI ONLY when there are absolutely no questions, requests, or pending actions in the entire thread @@ -59,7 +69,7 @@ Respond with a JSON object with: const prompt = `${getUserInfoPrompt({ emailAccount })} -Email thread (most recent message first): +Email thread (in chronological order, oldest to newest): ${getEmailListPrompt({ diff --git a/apps/web/utils/date.ts b/apps/web/utils/date.ts index b03fb9cd14..6768b95250 100644 --- a/apps/web/utils/date.ts +++ b/apps/web/utils/date.ts @@ -86,3 +86,17 @@ export function formatDateSimple(date: Date) { year: "numeric", }); } + +/** + * Comparator function for sorting messages by internalDate + * @param direction - 'asc' for oldest first (default, chronological), 'desc' for newest first + */ +export function sortByInternalDate( + direction: "asc" | "desc" = "asc", +) { + return (a: T, b: T): number => { + const aTime = internalDateToDate(a.internalDate).getTime() || 0; + const bTime = internalDateToDate(b.internalDate).getTime() || 0; + return direction === "asc" ? aTime - bTime : bTime - aTime; + }; +} diff --git a/apps/web/utils/reply-tracker/handle-conversation-status.ts b/apps/web/utils/reply-tracker/handle-conversation-status.ts index 1fae963a0a..d894d0e342 100644 --- a/apps/web/utils/reply-tracker/handle-conversation-status.ts +++ b/apps/web/utils/reply-tracker/handle-conversation-status.ts @@ -7,7 +7,7 @@ import { getEmailForLLM } from "@/utils/get-email-from-message"; import { createScopedLogger } from "@/utils/logger"; import { SystemType, ThreadTrackerType } from "@prisma/client"; import prisma from "@/utils/prisma"; -import { internalDateToDate } from "@/utils/date"; +import { sortByInternalDate } from "@/utils/date"; const logger = createScopedLogger("conversation-status-handler"); @@ -47,16 +47,11 @@ export async function determineConversationStatus({ }; } - // Sort messages (most recent first) - const sortedMessages = [...threadMessages].sort( - (a, b) => - (internalDateToDate(b.internalDate).getTime() || 0) - - (internalDateToDate(a.internalDate).getTime() || 0), - ); + const sortedMessages = [...threadMessages].sort(sortByInternalDate()); const threadMessagesForLLM = sortedMessages.map((m, index) => getEmailForLLM(m, { - maxLength: index === 0 ? 2000 : 500, + maxLength: index === sortedMessages.length - 1 ? 2000 : 500, extractReply: true, removeForwarded: false, }), diff --git a/apps/web/utils/reply-tracker/outbound.ts b/apps/web/utils/reply-tracker/outbound.ts index 599fdc052c..5ba47d0d9d 100644 --- a/apps/web/utils/reply-tracker/outbound.ts +++ b/apps/web/utils/reply-tracker/outbound.ts @@ -4,7 +4,7 @@ import { aiDetermineThreadStatus } from "@/utils/ai/reply/determine-thread-statu import prisma from "@/utils/prisma"; import { createScopedLogger, type Logger } from "@/utils/logger"; import { getEmailForLLM } from "@/utils/get-email-from-message"; -import { internalDateToDate } from "@/utils/date"; +import { internalDateToDate, sortByInternalDate } from "@/utils/date"; import type { EmailProvider } from "@/utils/email/types"; import { applyThreadStatusLabel } from "./label-helpers"; import { updateThreadTrackers } from "@/utils/reply-tracker/handle-conversation-status"; @@ -53,10 +53,10 @@ export async function handleOutboundReply({ return; // Stop processing if not the latest } - // Prepare thread messages for AI analysis (most recent first) + // Prepare thread messages for AI analysis (chronological order, oldest to newest) const threadMessagesForLLM = sortedMessages.map((m, index) => getEmailForLLM(m, { - maxLength: index === 0 ? 2000 : 500, // Give more context for the latest message + maxLength: index === sortedMessages.length - 1 ? 2000 : 500, // Give more context for the latest message extractReply: true, removeForwarded: false, }), @@ -114,12 +114,8 @@ function isMessageLatestInThread( ): { isLatest: boolean; sortedMessages: ParsedMessage[] } { if (!threadMessages.length) return { isLatest: false, sortedMessages: [] }; // Should not happen if called correctly - const sortedMessages = [...threadMessages].sort( - (a, b) => - (internalDateToDate(b.internalDate).getTime() || 0) - - (internalDateToDate(a.internalDate).getTime() || 0), - ); - const actualLatestMessage = sortedMessages[0]; + const sortedMessages = [...threadMessages].sort(sortByInternalDate()); + const actualLatestMessage = sortedMessages[sortedMessages.length - 1]; if (actualLatestMessage?.id !== message.id) { logger.warn( diff --git a/version.txt b/version.txt index 0df16dcbd9..70ea2c4e0d 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -v2.17.10 +v2.17.11