diff --git a/src/app/api/labs/ocr/capability/route.ts b/src/app/api/labs/ocr/capability/route.ts index 19c848b11..87df4115c 100644 --- a/src/app/api/labs/ocr/capability/route.ts +++ b/src/app/api/labs/ocr/capability/route.ts @@ -4,7 +4,7 @@ * Cheap probe (no provider call) the Labs UI uses to decide whether to show * the "Scan a report" affordance. Returns `{ available, reason, pdfSupported }` * for the calling user's configured AI provider. The surface stays dark for - * codex-only / text-only-model users. + * users with no provider configured and no local-OCR opt-in. */ import { apiHandler, requireAuth } from "@/lib/api-handler"; import { apiSuccess } from "@/lib/api-response"; diff --git a/src/lib/ai/coach/tools/executor.ts b/src/lib/ai/coach/tools/executor.ts index 75026d815..f9502c7d9 100644 --- a/src/lib/ai/coach/tools/executor.ts +++ b/src/lib/ai/coach/tools/executor.ts @@ -20,8 +20,10 @@ * mutation or egress surface. * * The builder result is memoised by the 60s snapshot LRU keyed on - * `(userId, window, sources)`, so several tool calls in one turn (and the - * base-context inventory build) share the underlying reads. + * `(userId, window, sources)`. Each tool scopes to a different source set, so + * tool calls for distinct domains do NOT share a cache entry; sharing happens + * only when the SAME scope recurs (a repeated call, or a turn that re-enters + * the same domain) within the 60s window. */ import { z } from "zod/v4"; diff --git a/src/lib/fitbit/sync.ts b/src/lib/fitbit/sync.ts index 6abf49dcd..ae3c493cf 100644 --- a/src/lib/fitbit/sync.ts +++ b/src/lib/fitbit/sync.ts @@ -47,6 +47,14 @@ import { /** Refresh the access token this many ms before `tokenExpiresAt`. */ const TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1000; +/** + * Delimiter joining `(type, externalId)` into a single dedup map key. A + * control character keeps it unambiguous against any value a real type or + * externalId could contain, while staying a normal printable-safe constant + * (a literal NUL in the source made text tooling treat the file as binary). + */ +const DEDUP_KEY_DELIM = "\u0000"; + /** * Overlap window for the incremental sync, in ms. Fitbit finalises daily * summaries after the fact (a daily SpO2 / HRV / RHR can settle hours after the @@ -396,7 +404,10 @@ export async function upsertFitbitMeasurements( liveByKey = new Map( existing .filter((e) => e.externalId !== null) - .map((e) => [`${e.type}${e.externalId}`, { id: e.id }]), + .map((e) => [ + `${e.type}${DEDUP_KEY_DELIM}${e.externalId}`, + { id: e.id }, + ]), ); } catch (err) { // A probe failure must not strand the whole batch; fall back to treating @@ -421,7 +432,7 @@ export async function upsertFitbitMeasurements( const plannedCreateKeys = new Set(); for (const r of readings) { const type = r.type as MeasurementType; - const key = `${type}${r.externalId}`; + const key = `${type}${DEDUP_KEY_DELIM}${r.externalId}`; const live = liveByKey.get(key); if (live) { toUpdate.push({ id: live.id, r }); @@ -438,7 +449,9 @@ export async function upsertFitbitMeasurements( } else { // A duplicate fresh key inside the same batch — overwrite the planned // create's payload so last-write-wins, matching the prior upsert loop. - const idx = toCreate.findIndex((c) => `${c.type}${c.externalId}` === key); + const idx = toCreate.findIndex( + (c) => `${c.type}${DEDUP_KEY_DELIM}${c.externalId}` === key, + ); if (idx >= 0) { toCreate[idx] = { type, diff --git a/src/lib/openapi/routes/ocr.ts b/src/lib/openapi/routes/ocr.ts index 7b4286469..bb90fefcf 100644 --- a/src/lib/openapi/routes/ocr.ts +++ b/src/lib/openapi/routes/ocr.ts @@ -29,9 +29,7 @@ const capabilityResponse = z .object({ available: z.boolean(), mode: z.enum(["vision", "text"]).nullable(), - reason: z - .enum(["no-provider", "text-only-model", "enable-local-ocr"]) - .nullable(), + reason: z.enum(["no-provider", "enable-local-ocr"]).nullable(), pdfSupported: z.boolean(), }) .meta({ diff --git a/src/lib/validations/labs-ocr.ts b/src/lib/validations/labs-ocr.ts index 8f8835d53..74a6584df 100644 --- a/src/lib/validations/labs-ocr.ts +++ b/src/lib/validations/labs-ocr.ts @@ -207,7 +207,7 @@ export interface OcrCapabilityDto { */ mode: "vision" | "text" | null; /** Why scanning is unavailable, when it is. */ - reason: "no-provider" | "text-only-model" | "enable-local-ocr" | null; + reason: "no-provider" | "enable-local-ocr" | null; /** Whether PDF uploads are accepted (Anthropic vision provider only). */ pdfSupported: boolean; }