A typed apiFetch wrapper owns the envelope unwrap, the ok-check and the
error read for every client call — two hundred call sites across the
app migrate onto it and a lint rule keeps raw api fetches out for good.
The legacy timezone helpers fold into the tz module and test fixtures
drop personal names.
The composer starts as a single line with send beside it and grows only
when the text does; the shift-enter hint and the inline disclaimer are
gone. A typing indicator bubbles before the first token. The context
rail hides by default behind a toggle and stops scrolling sideways,
conversations open in the full view instead of a broken side drawer,
and the coach preferences move into Settings where the rest of the
configuration lives — context toggles and the exclusion list now derive
from one source so they can no longer contradict each other. A floating
bubble appears only while a coach-initiated nudge is unread. Clearly
stated allergies and conditions are remembered immediately through a
deterministic extractor instead of waiting for the twenty-turn
summary pass.
The pregenerate pass now warms the status cards even when the
comprehensive step is budget-blocked, failed or timed out — the gap
that left cards generating on first visit despite the nightly runs —
and the loop gains a per-user timeout so one hanging provider cannot
stall the tick. Schedule edits recompute their rollup days immediately,
the compliance cache keys on the timezone, open doses no longer count
against the nudge trigger, an invite sent against an open registration
still lands in the ledger, the self-report prompt fence uses explicit
data markers, the maintenance crons invalidate the caches they touch,
and a flaky audit assertion polls instead of racing. Six care-focused
achievements join the engine: miss-free streaks, measurement routine,
a complete self-context and a sleep-log run.
The freshly released metadata-action v6.1.0 exits without output on the
pull-request build path; both call sites pin back to the proven v6.0.0
commit. The dead-code gate also caught an unused read helper and an
unused analytics type left behind by the wave — both removed.
Invites move into their own admin section with a real ledger: status
badges, expiry, uses, a redemption history of who joined when (new
redemption table, revocation is now soft so history survives), and an
honest one-time link/QR moment. The assistant self-context grows
structured fields for conditions, allergies and focus — encrypted like
the free text — merges age and gender from the profile into one fenced
prompt block, and asks up to three targeted follow-up questions that
surface as tappable chips above the coach composer. A fourth nudge
trigger gently suggests a check-up when the self-context is stale and
the coach is actually in use.
Users can now tell the assistant about themselves in an encrypted
free-text profile that feeds the coach and briefing prompts as clearly
fenced context, with the coach allowed one targeted follow-up question
when the picture is incomplete. Operators on closed registrations can
mint expiring invite links with a QR code from the admin area; a valid
invite opens registration exactly as many times as it allows and records
who redeemed it. A nightly pass watches three deterministic signals —
low adherence, blood pressure above its target band, a falling recovery
trend — and sends at most one coach nudge per week through the existing
notification channels, gated behind the same opt-outs as every other
assistant surface.
The two-o'clock status crons generated for every account, ignoring both
the per-user coach opt-out and the operator kill switch; they now share
one gated discovery that also skips users the pregenerate pass will
rewarm anyway, and the mood card finally gets the nightly run the other
six always had. Deleted measurements no longer feed any prompt. Locales
without an assistant language fall back to English consistently instead
of German texts with English fallbacks. A rate-limited regenerate stops
claiming success, the dashboard briefing state distinguishes a missing
provider from a pending generation and serves a stale briefing with its
timestamp instead of hiding it, narrative warming actually processes
oldest first, and worker failures reach the error tracker through one
shared reporter while the insight queues gain real retries.
Blood-pressure fixtures in the in-target integration suite were copied
from live readings; they are now synthetic with the same in/out split so
every assertion holds. Hard-coded operator hostnames move behind env vars
(DEPLOY_LOGS_URL, HEALTHLOG_PROD_URL), the codex client identifies via
the public repository URL, the OpenAPI servers entry points at a neutral
example host, and remaining workflow, docs and comment references to the
operator's instance or working notes are generalised. The compose
whitelist also gains DEPLOY_WEBHOOK_SECRET, which was missing and made
the deploy webhook unusable on compose installs.
Warm the insight generator (briefing + key findings + insight prose) and
the Coach to the same premium-motivating advisor register already shipped
for the per-metric assessments. The intro and role framing drop the cold
clinical-summariser voice for a warm advisor who reads the user's data
against their own history, names earned wins, and frames an unfavourable
finding as one doable step.
Every safety and quality guard is preserved: no diagnosis, no
prescription, no platitudes, no bare number-echoing, honest about
unfavourable values, the banned positivity openers, the doctor-consult
call-to-action, and the JSON output schema + keyFindings tone enum stay
byte-identical. PROMPT_VERSION 4.27.0 -> 4.28.0 (also covers the Coach,
which imports it). Native FR/ES/IT/PL personas warmed to match.
The per-metric and wellness-score assessment prompts gain a premium-advisory
layer: name the earned win, build a little momentum, frame an unfavourable
finding as an opportunity, and close forward-looking. Every existing guard
stays in force — no platitudes, no bare number-echoing, autonomy-supporting,
never alarming, moralising or diagnostic, and the banned-opener and
forbidden-phrase lists are unchanged. The composite-score archetype warms to
a WHOOP/Oura-style recovery coach while staying descriptive, not clinical.
PROMPT_VERSION 5.0.0 -> 5.1.0 so the provenance can slice prose before/after
the tone edit. Cached accounts converge to the new voice within ~24h.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Push the cycle-reminder cron's gating into the query instead of loading
every CyclePrediction row each tick: filter on the user's
cycleTrackingEnabled / predictionEnabled relation and restrict to rows
whose nextPeriodStart falls inside a tz-generous [today-grace, today+lead]
string window. The per-tick cost now scales with the in-window cohort, not
the whole table; the exact per-user-timezone window still runs in JS.
Make the period-boundary shortcut transactional and idempotent: wrap the
close-prior + open-new (or stamp-current) cycle mutations in a single
$transaction so two concurrent taps cannot double-close a prior cycle or
compute a length against a stale read, and add withIdempotency so an exact
key replay is a no-op. The boundary day-log now ranks the incoming flow
against any existing same-day entry and never downgrades a richer flow.
Decode sync cursors additively: a live beta v1 token keeps its
measurement / mood / intake watermarks and simply fresh-scans the two
domains added in v2 (cycleDays, cycles) rather than forcing a full
re-init. Correct the stale "no live consumer" comment.
Thread the user's timezone through the Coach cycle snapshot so "today",
day-of-cycle and days-until-period derive from the user's zone — matching
the calendar and insights routes — instead of wall-clock Berlin.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the menstrual-cycle phase as a channel in the existing FDR-guarded
correlation machinery, the same way rated mood factors were folded in —
no new statistics, the same Welch + Benjamini-Hochberg pipeline, the same
day floors and q = 0.10 gate.
Two complementary mechanisms, both confined to a gender-gated surface:
- a categorical luteal-vs-follicular contrast per outcome metric (resting
heart rate, HRV, sleep, steps, weight, basal/wrist/skin temperature),
surfacing one headline finding (resting-heart-rate-by-phase, falling
back to HRV); and
- the continuous phase ordinal folded into the lagged-Pearson matrix.
Phase never reaches the general correlations route — the temperature
channels held out of the general matrix are un-blocked only here, behind
the cycle gate, so a reproductive inference can never leak onto a
non-gated surface. Every row shows n, effect size, p and q; nothing is
fabricated on thin data and nothing is framed as causal.
Surface it through a new gated GET /api/cycle/insights and a drop-in
card that mirrors the mood-factor crosstab.
Add a cycle dimension to the Coach snapshot: current phase + day-of-cycle,
the next predicted event (period range + confidence + method, fertile
window goal-gated), and the headline phase finding — present only for a
cycle-enabled account, so a non-cycle snapshot is unchanged. A new ground
rule keeps the Coach descriptive: never contraception-grade, never a
"safe day" claim.
OpenAPI regenerated; phase-correlation + snapshot-inclusion tests added;
copy localised across all six locales.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an additive, non-breaking assessment field to GET /api/insights/derived:
{ text, source, updatedAt } | null, keyed to the requested metric id for the
five per-score ids (READINESS, SLEEP_SCORE, RECOVERY_SCORE, STRAIN_SCORE,
STRESS_SCORE). The field is null when the metric is not one of those or when
its status is not "ok". A deterministic text always fills it — composed from
the score's own contributors, so provider-less accounts and the demo are never
empty — and warmer provider prose overrides it once cached, warmed out of band
so the GET never blocks on a model round-trip. de/en, following the v1.12.12
deterministic-narrative model. The batch grid read returns assessment: null;
the detail sheets use the single-metric route.
Introduce a shared buildMetricSignal() helper: a token-lean, signal-rich
descriptor (current, own baseline, signed delta, spread, normal-swing verdict,
normal-range placement, contributors) computed server-side so the model states
the comparison instead of re-deriving it from raw buckets. Thread it into the
generic per-metric assessment snapshot and lead the base prompt from it. Add a
PROMPT_VERSION constant to the base system prompt so prompt changes are
traceable.
Regenerate docs/api/openapi.yaml.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The BYOK Anthropic client fell back to the retired claude-3-5-sonnet-latest;
match the chain-runner default (claude-sonnet-4-6). The admin ai-settings GET
displayed a gpt-4o-mini fallback that disagreed with the actual provider
default (gpt-4o); align the display string so the surfaced model matches what
the provider would use.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The v1.12.1 diversity block (variety lead, data-strength line, steady-run
repetition signal, FDR-screened cross-metric relations) reached only the
generic archetype cards. The seven hand-tuned headline cards — the ones users
see most — ran at temperature 0.3 with only the previous-context block, so they
stayed prone to repetitive openers and forced filler steps. Thread the same
assessmentContextBlock through the specialized get*UserPrompt builders and
raise their temperature to 0.45 to match the archetypes.
Soften the unconditional "close with one step" across the specialized sections
and the activity / body-composition / sleep archetypes to the base prompt's
conditional form — close with a step only when the finding implies one,
otherwise affirm and name one thing to watch — so a steady metric no longer
manufactures a filler step. EN/DE parity preserved; guard computeSteadyRun
against an absent graded slice.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The reference-grounding builder (buildSystemPromptWithReferences) and its
SOURCES list shipped to no one: both live paths called the plain system prompt,
so the model was asked to cite referenceId values it was never shown — and the
parser rejects invented ids, so the citation machinery the schema and UI
footnote already support stayed dark. Wire the builder into both consumers,
passing the metric sections the snapshot actually carries (a weight-only run
never lists BP guidance). Apply the fenced-JSON parse fallback here too.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The insights ground-rules carried changelog tokens ("v1.4.16 phase B8", "W4d")
inside the prompt strings the model reads — token waste and inconsistent with
the project voice. Move the rule content to clean numbered rules; the markers
belong in code comments, not shipped prompt prose. Update the test pins to the
cleaned rule headers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The OpenAI BYOK + admin-shared fallback defaulted to a small model and
Anthropic to a 2024 Sonnet, so a user on the operator's shared key got the
weakest model for the Insights assessments. Bump the OpenAI default to gpt-4o
and the Anthropic default to claude-sonnet-4-6; both stay overridable by user
config. Sync the provider docs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The per-metric assessment prompt (base-system) and the scope-hardened
insight system prompt (insight-generator) each carried EN and DE as two
parallel template literals kept in sync by hand — a standing drift risk
flagged in the AI-quality audit.
Model each prompt as one section skeleton: a single ordered list where
every section supplies its EN and DE fragment side by side, and a builder
joins the fragments with a blank line. The section order and prose are
unchanged; the DE calibration is preserved verbatim. A section edited in
one locale but not the other now shows up as an asymmetric edit in one
list instead of two literals silently diverging.
Pure refactor — every emitted prompt string is byte-identical for both
locales across every metric, surface and reference combination. A new
locale-skeleton-parity test pins the structure (each section carries both
fragments, composed prompt equals the joined fragments); the existing
grounding, filler-ban, forbidden-phrase, refusal and exact-phrasing guards
pass unchanged.
(cherry picked from commit 12cd718d6d602a48cafc14ad06d82282fdaacb27)
The per-metric assessment cards are tightly grounded but their form had
converged: every card opened the same way, forced a "one doable step"
even when nothing was actionable, and a metric that stayed steady for
weeks produced near-identical paragraphs. Squeeze more value from data
already computed server-side, without touching the grounding floor or any
safety contract.
- Surface FDR-surviving cross-metric correlations on the per-metric card.
The discovery engine already screens behaviour×outcome pairs with a real
Pearson + exact p-value + Benjamini-Hochberg FDR control; that
intelligence only reached the period narrative. A new read-only consumer
runs the same full-matrix discovery and filters to the pairs that
involve the current metric, then feeds the engine's own conservative,
descriptive, never-causal interpretation strings into the prompt as
grounded relations. Best-effort: a correlation hiccup can never block the
generation it only decorates.
- Streak-aware repetition. Derive a steady-run length from the graded
series (consecutive recent weeks within a band of the user's own
baseline) and tell the model when it has already reported "no material
change" N times running, so it acknowledges the continuity in one clause
and pivots to a different facet instead of restating the same level.
- Stop forcing a step; add controlled variety. The closing step is now
conditional — when nothing is genuinely actionable the model affirms and
names one thing to watch rather than manufacturing the filler that lets
platitudes back in. A deterministic per-render variety token (seeded from
user + metric + day, never Math.random / Date.now) rotates the opening
angle so consecutive cards and days don't read identically, and the
phrasing temperature lifts 0.3 → 0.45 while the facts stay pinned by the
snapshot and the forbidden-phrase guards.
- Surface the computed data strength (n + recency) into the prose prompt so
it hedges on thin data instead of guessing what "few" means, matching the
UI confidence badge.
- Add locale-matched few-shot examples (a grounded assessment + a labelled
banned-filler counter-example) to the assessment system prompt, which
most helps weaker local-provider models follow the contract.
The own-baseline grounding, computed-not-hallucinated stats, schema-
enforced output, filler-phrase ban, and correlation-as-association
discipline are all preserved and re-pinned by new guard tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit c9d04cab1523e0e57a3f50c87c9f88684b01b26f)
Close the launch-blocking gap where the ConsentReceipt infrastructure was
built but never enforced as a precondition: a direct API caller with a valid
token could forward a health snapshot to a server-managed external LLM with no
receipt on file. Consent was enforced client-side only.
Add a consent guard (src/lib/ai/consent-guard.ts) and call it before the first
external-provider call on the server-managed path. The gate fires only when the
resolved chain could egress via the operator's global key (admin-openai); a
user's own BYOK key (openai/anthropic), their own ChatGPT OAuth account, and
the self-hosted local provider stay ungated — that egress is the user's own act
and the settings flow is its consent. The check fails closed: a BYOK-primary
chain with an admin-openai fallback still requires a receipt, since the runner
may cascade to the operator key. The surface maps to the consent kind (coach ->
ai_coach, insights -> ai_insights_only; master ai_full satisfies either).
- Interactive routes (insights/chat, insights/generate POST) throw
ConsentRequiredError; api-handler renders a 403 with
meta.errorCode = "consent.ai.required", mirroring assistant.disabled.* so
the client can render an inline grant-consent notice.
- Off-request pipelines fail closed quietly: comprehensive generation returns a
typed skipped:no-consent outcome, and the per-metric status family
(runStatusCompletion, gating every status card + the period-narrative warm +
the off-budget Coach memory workers) surfaces the no-key fallback instead of
egressing.
Also rate-limit POST /api/fitbit/sync (the one Fitbit route without a limiter):
a 5/60s baseline plus a tighter 1/hour bucket on the expensive fullSync walk,
keyed by user id, matching the sibling Fitbit routes.
Harden the Fitbit OAuth redirect_uri: assert it is an absolute https origin (or
http on localhost), lands on /api/fitbit/callback, and stays same-origin with
NEXT_PUBLIC_APP_URL — defence-in-depth behind Google's registered-URI check so
a Host-coerced or misconfigured app URL cannot redirect the authorization code
off-origin.
(cherry picked from commit 46b5fc2cea1b6e80bee0445bfdacf5790442c67d)
Apple Health (and some WHOOP shapes) write BOTH an unspecified ASLEEP
aggregate row AND the granular CORE/DEEP/REM breakdown for the same
sleep period. The per-night reconstruction summed both, so a real ~8 h
night surfaced as ~16 h across every sleep surface — the chart showed
~16-18 h points and the target-range 30-day average read ~16.4 h/day.
Prefer the granular partition: when any CORE/DEEP/REM stage is present
for a night/source, count only those; fall back to the bare ASLEEP (or
a stage-less legacy/manual) row only when no granular stage exists.
IN_BED and AWAKE stay excluded; dual-source nights still collapse to one
canonical source via the user's sleep priority ladder. The same rule
applies inside the per-source asleep tiebreak so a source writing both
shapes is not over-weighted.
Align the surfaces that bypassed the shared helper:
- insights/targets: SLEEP_DURATION was averaged as raw per-stage rows
(minutes) and fed into a target band expressed in hours, producing the
bogus 16.4 h/day. Reconstruct per-night time-asleep, convert to hours,
and classify + roll up consistency on the nightly hour totals.
- coach snapshot: route the sleep block through reconstructSleepNights
so the prompt narrates the same nightly numbers (was summing every
stage row, including IN_BED + AWAKE).
- sleep card chart: read /api/measurements/series?kind=sleep (night
reconstruction, hours) instead of the generic measurement
daily-average path, and label the axis in hours instead of minutes.
Add regression fixtures: a night with both a bare ASLEEP row and the
granular breakdown asserts the granular total (no double-count); a
dual-source night with both shapes per source; and a multi-night 30-day
average that lands at ~8 h, not ~16 h.
The snapshot builder ran its independent reads sequentially on a cache
miss: feature extraction, the shared measurement query, the mood,
compliance, sleep and workout table reads, and the GLP-1, derived,
trajectory and memory helper blocks each awaited in turn. None consumes
another's result, so they now fire concurrently and resolve in batched
hops — feature extraction and the measurement read first (both gate the
per-metric blocks), then the remaining reads in one Promise.all. The
source guards are unchanged, so a disabled source still issues no query,
and the synchronous block assembly keeps its original order, so the
provenance envelope is byte-for-byte the same. Cuts several round-trips
off the cold build; the warm path is cached and unaffected.
- Drop the unused `now` field from the coach `ExtractOpts` type; fact
extraction never read it (conversation-summary keeps its own `now`).
- Correct the no-ladder tiebreak docstring in measurement-read: the
collapse picks the alphabetically smallest source name, matching the
live-SQL `ORDER BY … source` tiebreak, not max count.
- Replace the hand-inlined canonical-source subquery in the summaries
rollup-path narrows query with `canonicalMeasurementsFrom(rank, "90 days")`
for live/rollup parity with its siblings.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add requirePublicHost to the three outbound safeFetch sites whose host is
user- or operator-controlled and lacked the connect-time pin: the OpenAI
and Anthropic clients (BYO base-URL override) and the IP-geo lookup
(IP_GEO_LOOKUP_URL env). A base URL resolving to a private/metadata
address is now rejected. The LOCAL AI client intentionally stays
conditional (LAN escape hatch). Redirect-follow is already closed by the
safeFetch defaults.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove the two never-wired *_PROMPT_VERSION exports the dead-code gate
flagged, and consume the summary-length target as a real backstop that
caps the stored rolling summary so a misbehaving provider can't bloat
the encrypted column or the next prompt's history window.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- conversation-summary: fault-isolate the per-message decrypt in the
fold loop so one undecryptable historic turn (e.g. mid key-rotation)
no longer permanently stalls a conversation's summary refresh.
- source collapse: unify the no-ladder tiebreak on alphabetically
smallest source across the JS reader and the all-time rollup SQL, so a
ranked vital whose day carries only off-ladder sources resolves to the
same canonical source on the warm and the live-SQL read paths.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
buildCoachMemoryBlock gains a third fault-isolated sub-source — the
top-N durable facts — folded under snapshot.memory so they ride the
existing lowest-priority registration and are shed first under the
char budget. Add an EN + DE system-prompt paragraph instructing the
Coach to use facts as the user's own stated, descriptive context —
never as a diagnosis, never invented.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fetchConversationWithMessages now returns the decrypted rolling summary
(fail-closed → null), and buildHistoryWindow substitutes it for the dead
placeholder so a long conversation keeps memory of its elided turns.
Once a conversation grows past the turn cap the chat turn fires a
fire-and-forget coach-memory-refresh so the next long turn is fresh,
without adding a provider round-trip to the interactive reply.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the coach-memory-refresh pg-boss queue: a fire-and-forget enqueue
contract (coach-memory-shared) and a worker pipeline that runs the
conversation-summary refresh then the fact extraction for one long
conversation, each fault-isolated and budget-gated. Register the queue
in allQueues + a boss.work handler in the worker, with a source-grep
inventory guard so the queue can never ship unregistered.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the two background generators behind Coach long-term memory, both
reusing the budget-aware provider chain (runStatusCompletion) with a
local fallback and encrypting their output at rest:
- conversation-summary: folds the turns elided past the history window
into a rolling natural-language summary, merging into any prior
summary; short-circuits cheaply until a conversation grows long.
- facts: extracts durable, descriptive personal facts (preferences,
stated conditions, goals, constraints, context), gated by a Zod
schema + de-dup + per-user cap; buildCoachFactsBlock ranks the top
facts for snapshot injection. Never diagnostic, never numeric.
Both are pure/injectable for unit testing and annotate counts only,
never content. Wiring into the queue, route, and snapshot follows.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extract the AES-256-GCM Bytes codec from persistence into a shared
bytes-codec module so the conversation summary and the new personal-fact
store reuse one ArrayBuffer-backed implementation. Add the CoachFact
table (migration 0116) for durable, user-scoped facts the Coach extracts
from conversations — encrypted fact text, plain category/confidence/
source for rank-without-decrypt, soft-delete for a reversible forget.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fold the deterministic short-horizon forecasting engine into the Coach
prompt as a compact, lowest-signal snapshot block. For each in-scope
metric the block carries direction + slope + the projected horizon end
with its widening prediction band, the last observed value, and the
fit's R² / server-computed confidence — read straight from the existing
projection engine, never recomputed. A metric below the fit/history/
staleness floor is omitted; the block is dropped entirely when none
qualify, and it sheds first under prompt-budget pressure.
Add a conditional-projection ground rule so the Coach narrates a
trajectory only when the block is present and only for the metric it
covers: direction and range, always conditional, never a certainty, a
risk score, a diagnosis, or a dated event; absent block means no
projection. The rule lands in the safety-contract matrix across all six
locales and inline in the EN + DE system prompts, with an overclaim
probe asserting a projection is never upgraded. Bump PROMPT_VERSION.
Assemble a zero-LLM longitudinal memory block from artefacts already
persisted: the latest period-narrative headline and a per-metric
prior-vs-current band recall. The Coach can now reference what was true
last period instead of re-deriving a cold snapshot every turn.
The block is machine-derived (labels plus numbers only), so it is
unencrypted and adds no model cost. Each sub-source is fault-isolated —
a failing narrative read or context build drops that sub-block and never
sinks the turn. It registers against the lowest-signal cluster so the
budget degrader sheds it first, before any clinical data.
Promote the volatile per-worker last-working cache to a Postgres
provider-health ledger so the fallback chain shares one health signal
across every instance, modelled on the atomic-upsert rate limiter.
The ledger holds a durable negative cache: an auth-class failure
(401/403) benches that provider for a cooldown instead of re-burning the
dead round-trip on every generation; a sustained hard failure backs off
briefly. The local model is never benched — it stays the guaranteed
floor of the chain. Every ledger interaction fails open, so a DB error
falls back to today's chain walk exactly.
When the user's primary provider fails auth-class, the Coach emits a
distinct credential_expired frame that deep-links to reconnect rather
than a generic "try again later".
Adds the provider_health table (migration 0112) and the
errorCredentialExpired copy across all locales.
The connection-test route mapped a provider 5xx to a 502, which a reverse
proxy rewrites to its own HTML error page; the browser's res.json() then
threw "Unexpected token '<'". Every provider-call failure now returns
HTTP 200 with a secret-free { ok:false, providerType, reason }. Reasons
are categorised: credential/auth (401/403, or a 5xx whose body signals an
invalidated/expired session), rate-limit (429), provider server error
(other 5xx) and network/timeout.
The empty-override "test my saved config" path now resolves through the
same provider chain generation uses, instead of the legacy codex->admin
probe that could test a different provider than the one generation runs.
The chain runner already advances past an auth-class failure to the next
enabled provider; add explicit coverage for that on the raw generation
path (401 -> next succeeds, fallback count incremented).
When a derived wellness score (readiness or recovery) crosses a notability gate
— outside the green band AND confidence at least medium — the insights
generator now appends a SYSTEM CONTEXT block so the briefing can narrate the
shift observationally. Mirrors the GLP-1 plateau detector's append mechanism:
reads the one derived contract, returns null (the common case) at zero token
cost, and instructs the model to frame the signal as an observed pattern, never
a diagnosis or a dose recommendation. Add the readiness/recovery sourceMetric
enum values plus their briefing href + icon + trend-chart mappings.
Add a "derived" block to the Coach snapshot: one tiny object per high-signal
derived metric (readiness, recovery, sleep, stress, strain, HRV balance,
cardio-fitness band, vascular-age delta) carrying value + band + confidence +
historyDays — never the raw series. A fully-populated block is ~250 tokens,
comfortably inside the snapshot budget. Insufficient metrics are omitted so the
model never sees no-data noise; each metric's compute is fault-isolated so a
transient failure on one never sinks the turn. Gated on the underlying signals
staying in scope. The system prompt now documents the block and frames the
bands as descriptive composites, not clinical findings.
The standalone /targets (Zielwerte) page is retired. Target editing
moved inline onto the Insights metric pages in v1.8.6 — each metric's
reference panel mounts the editor and writes the same
PUT /api/user/thresholds, reading the same /api/insights/targets
payload. The page carried a deprecation notice and had already dropped
out of the navigation, so removing it is a UI-only change: no data
model, API route, or inline editor moves.
Remove the page and its tests, the components reachable only from it
(target card, per-card coach button, summary header, target prompt /
scope helpers, coach-handoff hook), the now-dead /zielwerte→/targets
proxy redirect, and the orphaned targets.* i18n keys across all six
locales. Repoint the handful of code comments that pointed readers at
the page to the inline Insights panel, and prune the two coach
gate-inventory allowlists plus the queryKey factory guard root that
named the removed files.
The reference panel keeps the range bar, consistency strip, status
pill, edit sheet, and source-link helpers — those are the live editing
surface now.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit 7f842c5620087127e980fe22a56133135b7c0531)
The ChatGPT Codex backend rotated its ChatGPT-auth model slugs off the
`-codex` specialist line onto the plain `gpt-5.x` line, so the in-code
default ladder (`gpt-5.3-codex`, `gpt-5-codex`, `gpt-5`, `gpt-4o`) is
now rejected and insight generation fails with a 503 on a fresh deploy
that has no `CODEX_MODEL_FALLBACK_CHAIN` override.
Replace the default ladder with the current accepted set, in order:
`gpt-5.5` (current default across paid tiers), `gpt-5.4` and
`gpt-5.4-mini` (documented fallbacks), `gpt-5.3-codex` (late rung, still
accepted intermittently), `gpt-5.2` (legacy floor). The
fall-through-on-rejection walk, the working-slug cache, and the env
override path are unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit bced19de2d4f0788bfd6fa56a78e719347776a1f)
Extend the assessment-card system beyond the seven specialised scopes to
the ~30 HealthKit metric pages, without hand-writing a prompt per metric.
- Add a metric-metadata registry keyed by HealthKit id (display name,
unit, direction, optional normal range, archetype, scope), covering the
data-bearing HealthKit metrics. The seven specialised metrics keep
their own scopes and are excluded from the generic set; an unregistered
id gets no card.
- Add a generic data-driven generator that reuses the existing graded
snapshot, provider runner, stale-while-revalidate cache, and persist
path. One prompt template per archetype (physiological-vital,
activity-fitness, body-composition, mobility-gait,
environmental-exposure) plus a dedicated sleep template, parameterised
by metric metadata. Empty-data metrics return an insufficient marker
with no provider call. Normal ranges use age + sex where the profile
stores them; no ancestry/region ranges.
- Add GET /api/insights/metric-status?metric=<id>: apiHandler-wrapped,
Zod-validated against the closed registry id set (422 on unknown),
read-only with stale-while-revalidate. First insights route with
OpenAPI coverage; regenerate docs/api/openapi.yaml.
- Route the generic metric:<id> scope through the on-demand status queue
worker dispatch.
- Extend the nightly warm pass to the generic metric caches for a user's
data-bearing metrics only (one distinct read gates the set), keeping
de+en warm and never calling a provider for an empty metric.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit 41453fbdfee60793c16269b9a9e5f04b6c3ea450)
The hard '~5 measurement points' floor did not fit mood (counted in
entries) or medication adherence (counted in scheduled doses). Re-word
the gate as 'a few measurement points/entries/events' and note that the
unit depends on the metric. Also fix the gating-field path: the recency
field lives at dataCoverage.newestMeasurementDaysAgo, not on summary.
The mood card noted persistent low phases but offered no gentle path to
support. When mood stays clearly low for several days, the model now
offers an autonomy-preserving pointer toward talking it through with a
trusted person or a professional — never alarming, never a diagnosis, no
emergency tone. The safety-contract matrix does not cover this pathway
and does not reach the per-metric assessment prompts, so it lives here.
For an infrequent schedule such as a weekly injection, the 7-day
compliance figure is dominated by a single dose: one miss swings the
percentage sharply and reads as a collapse it is not. Tell the model to
lean on compliance30 as the load-bearing value when schedulesPerDay is
low and treat compliance7 as a rough pointer only. Output stays {summary}.
The overall-status card summarises up to six metrics, but it inherited
the single-metric 30-60 word budget, which forced the model to either go
generic or overrun. Grant the section an explicit 3-5 sentence / 50-80
word budget and cap it at naming two metrics by name, with the rest
mentioned only when they deviate notably from their own mean.
The bmi and weight cards produced near-identical trend text for a user
with both metrics, since bmi is weight scaled by a constant. Re-aim each
card so they carry distinct information: bmi now centres on WHO-band
placement (call out a band crossing or a near-boundary value against the
person's own baseline), and weight stays on the continuous trend and
pace (kg/week, plateau, milestone). Each defers the other concern to its
sibling card.
Drop the dead-field guidance (avg90/allTime, historicalComparison,
ratePressureProduct, bodyCompositionDivergence, seasonalVariation) and
the chart-token instruction. Describe the per-type measurementSeries map
and the adherence/mood/target context, pull them into one coherent
picture against the person's own baseline, and close with the single
most important doable step.
Replace the historicalComparison and moodAdherenceRisk guidance with the
actual per-medication snapshot fields (compliance7/30, streak,
taken/skipped/missed, dailySeries). Read the recent direction as
compliance7 vs compliance30 against the person's own baseline, frame
positively, and close with one blame-free doable step.
Describe the graded mood series, target band and tag list, lead on the
weekly pattern against the person's own baseline, name persistent low or
high phases calmly as an observation rather than a diagnosis, and close
with one kind doable step.