diff --git a/src/lib/ai/coach/snapshot.ts b/src/lib/ai/coach/snapshot.ts index e383f8a05..96ddd8867 100644 --- a/src/lib/ai/coach/snapshot.ts +++ b/src/lib/ai/coach/snapshot.ts @@ -25,11 +25,21 @@ export interface CoachSnapshotResult { * `includeRaw=false` because the Coach replies are conversational and * the user is asking the model — they should never depend on raw * measurement timestamps that the privacy mode controls. + * + * Bounded to the last 90 days. Coach narrates the same windows the + * Daily Briefing renders (last7 / last30 / last90); fetching every + * historical measurement on each turn was unbounded I/O for power + * users with multi-year Withings imports. The 90-day floor is the + * widest window any Coach metric currently consumes. */ +const COACH_SNAPSHOT_WINDOW_DAYS = 90; + export async function buildCoachSnapshot( userId: string, ): Promise { - const features = await extractFeatures(userId, false); + const features = await extractFeatures(userId, false, { + sinceDays: COACH_SNAPSHOT_WINDOW_DAYS, + }); // Trim down to the metrics the Coach narrates. extractFeatures // returns more (sleep, steps, etc.) — the Coach surface keeps the diff --git a/src/lib/insights/features.ts b/src/lib/insights/features.ts index 4f7594228..c802a7972 100644 --- a/src/lib/insights/features.ts +++ b/src/lib/insights/features.ts @@ -256,6 +256,7 @@ function computeHistoricalComparison( export async function extractFeatures( userId: string, includeRaw: boolean, + options: { sinceDays?: number } = {}, ): Promise { const user = await prisma.user.findUnique({ where: { id: userId }, @@ -268,9 +269,21 @@ export async function extractFeatures( const now = Date.now(); - // Fetch ALL measurements (not just 30 days) for full temporal context + // Fetch measurements. Default = ALL (full temporal context for the + // dashboard / insights generator). Callers that only consume the + // ≤90-day windows (Coach snapshot per turn) pass `sinceDays: 90` so + // the per-turn I/O stays bounded — `findMany({ where: { userId } })` + // is unbounded by user-history size and gets paid once per Coach turn + // for power users with multi-year Withings imports. + const sinceDays = options.sinceDays; + const sinceCutoff = + typeof sinceDays === "number" && sinceDays > 0 + ? new Date(now - sinceDays * 24 * 60 * 60 * 1000) + : null; const measurements = await prisma.measurement.findMany({ - where: { userId }, + where: sinceCutoff + ? { userId, measuredAt: { gte: sinceCutoff } } + : { userId }, orderBy: { measuredAt: "asc" }, });