diff --git a/src/lib/ai/coach/__tests__/clarify-tool.test.ts b/src/lib/ai/coach/__tests__/clarify-tool.test.ts index af7c6f27e..136350387 100644 --- a/src/lib/ai/coach/__tests__/clarify-tool.test.ts +++ b/src/lib/ai/coach/__tests__/clarify-tool.test.ts @@ -229,6 +229,32 @@ describe("clarificationAllowed", () => { ).resolves.toBe(false); }); + it("counts the day in the person's zone, not from UTC midnight", async () => { + // 23:30 UTC on 1 March is already 2 March in Auckland and still 1 March + // in Los Angeles; each day starts at its own local midnight. + const now = new Date("2026-03-01T23:30:00.000Z"); + await clarificationAllowed({ + userId: "u1", + conversationId: "c1", + now, + tz: "Pacific/Auckland", + }); + await clarificationAllowed({ + userId: "u1", + conversationId: "c1", + now, + tz: "America/Los_Angeles", + }); + const since = db.messageCount.mock.calls.map( + ([q]) => + (q as { where: { createdAt: { gte: Date } } }).where.createdAt.gte, + ); + expect(since).toEqual([ + new Date("2026-03-01T11:00:00.000Z"), + new Date("2026-03-01T08:00:00.000Z"), + ]); + }); + it("refuses when the count cannot be read", async () => { db.messageFindMany.mockRejectedValue(new Error("down")); await expect( diff --git a/src/lib/ai/coach/clarify.ts b/src/lib/ai/coach/clarify.ts index 3b12191c9..25ea236f0 100644 --- a/src/lib/ai/coach/clarify.ts +++ b/src/lib/ai/coach/clarify.ts @@ -30,6 +30,9 @@ import { type LatestMessagesLoader, } from "@/lib/ai/coach/latest-messages"; import { prisma } from "@/lib/db"; +import { startOfLocalDayKey } from "@/lib/tz/local-day"; +import { userDayKey } from "@/lib/tz/format"; +import { resolveUserTimezone } from "@/lib/tz/resolver"; import { annotate } from "@/lib/logging/context"; import type { Locale } from "@/lib/i18n/config"; import { resolveIntlLocale } from "@/lib/format-locale"; @@ -704,6 +707,8 @@ export async function clarificationAllowed(args: { userId: string; conversationId: string; now?: Date; + /** The person's zone; resolved from the account when absent. */ + tz?: string; }): Promise { const now = args.now ?? new Date(); try { @@ -726,9 +731,11 @@ export async function clarificationAllowed(args: { ) { return false; } - const dayStart = new Date( - Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()), - ); + // The person's day, not the server's: a limit that reset at UTC + // midnight reset at one or two in the morning in Berlin and in the + // afternoon in the Pacific. + const tz = args.tz ?? (await resolveUserTimezone(args.userId)); + const dayStart = startOfLocalDayKey(userDayKey(now, tz), tz); const today = await prisma.coachMessage.count({ where: { conversation: { userId: args.userId },