diff --git a/src/lib/ai/coach/__tests__/series-long-window.test.ts b/src/lib/ai/coach/__tests__/series-long-window.test.ts index bb40f918b..7bc3bd4a6 100644 --- a/src/lib/ai/coach/__tests__/series-long-window.test.ts +++ b/src/lib/ai/coach/__tests__/series-long-window.test.ts @@ -244,6 +244,12 @@ describe("get_metric_series inside a chat turn", () => { for (const empty of [ { unit: "kg" }, { omitted: "trimmed for prompt budget" }, + // The shape the budget pass really leaves: the unit and the freshness + // stamp, whose `daysAgo` is a number but not a reading. + { + unit: "kg", + asOf: { daysAgo: 3, isToday: false, currentForTodayClaims: false }, + }, ]) { buildCoachSnapshot.mockReset(); buildCoachSnapshot.mockResolvedValue(snapshot({ fatMass: empty })); diff --git a/src/lib/ai/coach/tools/executor.ts b/src/lib/ai/coach/tools/executor.ts index fde8110dc..a431530e5 100644 --- a/src/lib/ai/coach/tools/executor.ts +++ b/src/lib/ai/coach/tools/executor.ts @@ -619,11 +619,22 @@ function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } -/** Whether a payload holds at least one finite number anywhere. */ +/** + * Fields that describe a block rather than measure anything: the freshness + * stamp (`asOf.daysAgo` is a number, but no reading), and the scope echo. A + * block trimmed to `{ unit, asOf }` holds no figure. + */ +const METADATA_FIELDS: ReadonlySet = new Set(["asOf", "scope"]); + +/** Whether a payload holds at least one finite measured number anywhere. */ function carriesFigures(value: unknown): boolean { if (typeof value === "number") return Number.isFinite(value); if (Array.isArray(value)) return value.some(carriesFigures); - if (isRecord(value)) return Object.values(value).some(carriesFigures); + if (isRecord(value)) { + return Object.entries(value).some( + ([key, child]) => !METADATA_FIELDS.has(key) && carriesFigures(child), + ); + } return false; }