;
}): typeof generateObject {
@@ -206,6 +208,7 @@ export function createGenerateObject({
} catch (error) {
await handleError(
error,
+ emailAccount.userId,
emailAccount.email,
emailAccount.id,
label,
@@ -295,6 +298,7 @@ export async function chatCompletionStream({
async function handleError(
error: unknown,
+ userId: string,
userEmail: string,
emailAccountId: string,
label: string,
@@ -302,6 +306,7 @@ async function handleError(
) {
logger.error("Error in LLM call", {
error,
+ userId,
userEmail,
emailAccountId,
label,
@@ -311,7 +316,7 @@ async function handleError(
if (APICallError.isInstance(error)) {
if (isIncorrectOpenAIAPIKeyError(error)) {
return await addUserErrorMessage(
- userEmail,
+ userId,
ErrorType.INCORRECT_OPENAI_API_KEY,
error.message,
);
@@ -319,7 +324,7 @@ async function handleError(
if (isInvalidOpenAIModelError(error)) {
return await addUserErrorMessage(
- userEmail,
+ userId,
ErrorType.INVALID_OPENAI_MODEL,
error.message,
);
@@ -327,7 +332,7 @@ async function handleError(
if (isOpenAIAPIKeyDeactivatedError(error)) {
return await addUserErrorMessage(
- userEmail,
+ userId,
ErrorType.OPENAI_API_KEY_DEACTIVATED,
error.message,
);
@@ -335,7 +340,7 @@ async function handleError(
if (RetryError.isInstance(error) && isOpenAIRetryError(error)) {
return await addUserErrorMessage(
- userEmail,
+ userId,
ErrorType.OPENAI_RETRY_ERROR,
error.message,
);
@@ -343,7 +348,7 @@ async function handleError(
if (isAnthropicInsufficientBalanceError(error)) {
return await addUserErrorMessage(
- userEmail,
+ userId,
ErrorType.ANTHROPIC_INSUFFICIENT_BALANCE,
error.message,
);
diff --git a/apps/web/utils/meeting-briefs/recipient-context.test.ts b/apps/web/utils/meeting-briefs/recipient-context.test.ts
index 3200e5384b..5c8a688060 100644
--- a/apps/web/utils/meeting-briefs/recipient-context.test.ts
+++ b/apps/web/utils/meeting-briefs/recipient-context.test.ts
@@ -12,7 +12,7 @@ import type { Logger } from "@/utils/logger";
vi.mock("@/utils/calendar/event-provider");
vi.mock("@/utils/date", () => ({
formatInUserTimezone: vi.fn((date: Date) => {
- // Simple mock that returns a predictable format for testing
+ // Simple mock that returns a predictable format for testing in UTC
const d = new Date(date);
const dayNames = [
"Sunday",
@@ -37,11 +37,11 @@ vi.mock("@/utils/date", () => ({
"November",
"December",
];
- const dayName = dayNames[d.getDay()];
- const month = monthNames[d.getMonth()];
- const day = d.getDate();
- const hours = d.getHours();
- const minutes = d.getMinutes();
+ const dayName = dayNames[d.getUTCDay()];
+ const month = monthNames[d.getUTCMonth()];
+ const day = d.getUTCDate();
+ const hours = d.getUTCHours();
+ const minutes = d.getUTCMinutes();
const ampm = hours >= 12 ? "PM" : "AM";
const displayHours = hours % 12 || 12;
const displayMinutes = minutes.toString().padStart(2, "0");
@@ -100,9 +100,9 @@ describe("recipient-context", () => {
expect(result).toBe(`You have meeting history with this person:
-- "Q1 Planning Meeting" on Monday, January 15 at 12:00 PM (Conference Room A)
+- "Q1 Planning Meeting" on Monday, January 15 at 10:00 AM (Conference Room A)
Description: Discuss Q1 goals and objectives
-- "Team Standup" on Wednesday, January 10 at 11:00 AM
+- "Team Standup" on Wednesday, January 10 at 9:00 AM
Use this context naturally if relevant. For past meetings, you might reference topics discussed.`);
@@ -128,7 +128,7 @@ Use this context naturally if relevant. For past meetings, you might reference t
expect(result).toBe(`You have meeting history with this person:
-- "Product Review" on Thursday, February 1 at 4:00 PM (Zoom)
+- "Product Review" on Thursday, February 1 at 2:00 PM (Zoom)
Description: Review product roadmap and features
@@ -160,12 +160,12 @@ Use this context naturally if relevant. For upcoming meetings, you might say "Lo
expect(result).toBe(`You have meeting history with this person:
-- "Past Meeting" on Monday, January 15 at 12:00 PM
+- "Past Meeting" on Monday, January 15 at 10:00 AM
Description: This is a past meeting description
-- "Upcoming Meeting" on Thursday, February 1 at 4:00 PM (Office)
+- "Upcoming Meeting" on Thursday, February 1 at 2:00 PM (Office)
Description: This is an upcoming meeting description
@@ -231,7 +231,7 @@ Use this context naturally if relevant. For past meetings, you might reference t
expect(result).toBe(`You have meeting history with this person:
-- "Simple Meeting" on Monday, January 15 at 12:00 PM
+- "Simple Meeting" on Monday, January 15 at 10:00 AM
Use this context naturally if relevant. For past meetings, you might reference topics discussed.`);
diff --git a/packages/resend/emails/reconnection.tsx b/packages/resend/emails/reconnection.tsx
index 9df8be649c..44b29e81f1 100644
--- a/packages/resend/emails/reconnection.tsx
+++ b/packages/resend/emails/reconnection.tsx
@@ -119,7 +119,7 @@ function Footer({
Unsubscribe
From 289fceb02e80e3eb80fce8d561e9399ee8943eb7 Mon Sep 17 00:00:00 2001
From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com>
Date: Fri, 2 Jan 2026 01:18:30 +0200
Subject: [PATCH 3/3] fixes
---
.../ai/meeting-briefs/generate-briefing.test.ts | 6 +++++-
.../web/utils/auth/cleanup-invalid-tokens.test.ts | 13 ++++++++-----
apps/web/utils/auth/cleanup-invalid-tokens.ts | 15 ++++++++++++---
3 files changed, 25 insertions(+), 9 deletions(-)
diff --git a/apps/web/utils/ai/meeting-briefs/generate-briefing.test.ts b/apps/web/utils/ai/meeting-briefs/generate-briefing.test.ts
index 5b803d1adc..3415af4a36 100644
--- a/apps/web/utils/ai/meeting-briefs/generate-briefing.test.ts
+++ b/apps/web/utils/ai/meeting-briefs/generate-briefing.test.ts
@@ -1,4 +1,4 @@
-import { describe, it, expect, vi } from "vitest";
+import { describe, it, expect, vi, beforeEach } from "vitest";
import type { MeetingBriefingData } from "@/utils/meeting-briefs/gather-context";
vi.mock("server-only", () => ({}));
@@ -28,6 +28,10 @@ vi.doUnmock("@/utils/date");
import { buildPrompt } from "./generate-briefing";
+beforeEach(() => {
+ vi.clearAllMocks();
+});
+
describe("buildPrompt timezone handling", () => {
it("formats past meeting times in the user's timezone (not UTC)", () => {
// This test documents the timezone bug fix:
diff --git a/apps/web/utils/auth/cleanup-invalid-tokens.test.ts b/apps/web/utils/auth/cleanup-invalid-tokens.test.ts
index 0403a804cf..ef8a1b8aa2 100644
--- a/apps/web/utils/auth/cleanup-invalid-tokens.test.ts
+++ b/apps/web/utils/auth/cleanup-invalid-tokens.test.ts
@@ -37,6 +37,7 @@ describe("cleanupInvalidTokens", () => {
it("marks account as disconnected and sends email on invalid_grant when account is watched", async () => {
prisma.emailAccount.findUnique.mockResolvedValue(mockEmailAccount as any);
+ prisma.account.updateMany.mockResolvedValue({ count: 1 });
await cleanupInvalidTokens({
emailAccountId: "ea_1",
@@ -44,9 +45,9 @@ describe("cleanupInvalidTokens", () => {
logger,
});
- expect(prisma.account.update).toHaveBeenCalledWith(
+ expect(prisma.account.updateMany).toHaveBeenCalledWith(
expect.objectContaining({
- where: { id: "acc_1" },
+ where: { id: "acc_1", disconnectedAt: null },
data: expect.objectContaining({
disconnectedAt: expect.any(Date),
}),
@@ -65,6 +66,7 @@ describe("cleanupInvalidTokens", () => {
...mockEmailAccount,
watchEmailsExpirationDate: null,
} as any);
+ prisma.account.updateMany.mockResolvedValue({ count: 1 });
await cleanupInvalidTokens({
emailAccountId: "ea_1",
@@ -72,7 +74,7 @@ describe("cleanupInvalidTokens", () => {
logger,
});
- expect(prisma.account.update).toHaveBeenCalled();
+ expect(prisma.account.updateMany).toHaveBeenCalled();
expect(sendReconnectionEmail).not.toHaveBeenCalled();
expect(addUserErrorMessage).toHaveBeenCalledWith(
"user_1",
@@ -93,12 +95,13 @@ describe("cleanupInvalidTokens", () => {
logger,
});
- expect(prisma.account.update).not.toHaveBeenCalled();
+ expect(prisma.account.updateMany).not.toHaveBeenCalled();
expect(sendReconnectionEmail).not.toHaveBeenCalled();
});
it("does not send email for insufficient_permissions", async () => {
prisma.emailAccount.findUnique.mockResolvedValue(mockEmailAccount as any);
+ prisma.account.updateMany.mockResolvedValue({ count: 1 });
await cleanupInvalidTokens({
emailAccountId: "ea_1",
@@ -106,7 +109,7 @@ describe("cleanupInvalidTokens", () => {
logger,
});
- expect(prisma.account.update).toHaveBeenCalled();
+ expect(prisma.account.updateMany).toHaveBeenCalled();
expect(sendReconnectionEmail).not.toHaveBeenCalled();
expect(addUserErrorMessage).not.toHaveBeenCalled();
});
diff --git a/apps/web/utils/auth/cleanup-invalid-tokens.ts b/apps/web/utils/auth/cleanup-invalid-tokens.ts
index 0940d00bcc..f14c739dd0 100644
--- a/apps/web/utils/auth/cleanup-invalid-tokens.ts
+++ b/apps/web/utils/auth/cleanup-invalid-tokens.ts
@@ -48,8 +48,8 @@ export async function cleanupInvalidTokens({
return;
}
- await prisma.account.update({
- where: { id: emailAccount.accountId },
+ const updated = await prisma.account.updateMany({
+ where: { id: emailAccount.accountId, disconnectedAt: null },
data: {
access_token: null,
refresh_token: null,
@@ -58,8 +58,17 @@ export async function cleanupInvalidTokens({
},
});
+ if (updated.count === 0) {
+ logger.info(
+ "Account already marked as disconnected (via concurrent update)",
+ );
+ return;
+ }
+
if (reason === "invalid_grant") {
- const isWatched = !!emailAccount.watchEmailsExpirationDate;
+ const isWatched =
+ !!emailAccount.watchEmailsExpirationDate &&
+ emailAccount.watchEmailsExpirationDate > new Date();
if (isWatched) {
try {