feat(coach): snapshot includes day-level readings with weekday labels

The Coach snapshot used to ship only aggregated statistics (mean,
slope, SD, range, count) per metric. A turn that asked
"why was BP higher on Monday?" had no day-level rows to ground in
and the model fell back to apologetic aggregate-only phrasing
("from the snapshot I cannot isolate the specific Monday").

Extend buildCoachSnapshot to fold in:
  - timeline.recent: one row per UTC day for the last 14 days, each
    tagged with its weekday so the model can answer day or weekday
    questions directly. BP rows pair systolic + diastolic; days with
    only one side measured are dropped to avoid fabricating a
    complement.
  - timeline.weekly: ISO-week buckets covering days 15..N where N is
    the configured window. Keeps the prompt budget tight on 30/90-day
    windows while still letting the Coach cite older weeks.
  - per-day medication adherence rows from MedicationIntakeEvent so
    the Coach can answer "did I miss my dose Tuesday?" without
    inventing the schedule.

Also add the optional `scope` field to coachChatRequestSchema (sources
+ window) so the upcoming sources-rail picker can narrow the snapshot
without breaking older clients — missing fields fall through to
all-source last30days.

A typical 30-day, 5-source snapshot lands around 2.4-2.8 KB JSON
(≈700-900 prompt tokens) — comfortably inside the 25k/day Coach token
budget.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-10 19:21:48 +02:00
1 parent 7921ffc8e6
commit ed61b17f57
4 files changed
+581 -24

No files matched your search

+7 -2
View File
@@ -143,7 +143,12 @@ async function handleChatRequest(request: NextRequest): Promise<Response> {
{ status: 422 },
);
}
const { conversationId, message, locale: bodyLocale } = parsed.data;
const {
conversationId,
message,
locale: bodyLocale,
scope,
} = parsed.data;
await enforceBudget(userId);
@@ -201,7 +206,7 @@ async function handleChatRequest(request: NextRequest): Promise<Response> {
// Build the prompt: system + (optional) snapshot + recent history +
// the new user message.
const snapshot = await buildCoachSnapshot(userId);
const snapshot = await buildCoachSnapshot(userId, scope);
const systemPrompt = getCoachSystemPrompt(locale);
const window = buildHistoryWindow([
...priorTurns,