The dedicated conversation-history page gates on the operator master
flag and the per-user opt-out like the main Coach route, so add it to
both grep-based gate-site allowlists.
Cover the new composer settings link, the seeded-opener dismiss control,
and the recency group headers (today, yesterday, this week, earlier) on
the dedicated conversation-history page, across all six locales.
Add a Settings entry to the composer's "+" actions menu and remove the
page-toolbar gear; the drawer header keeps its own gear. Let the seeded
"worth a look" opener fill the composer column and give it a per-day
dismiss control. Route the Conversations action to /coach/conversations
and drop the in-page history drawer.
Replace the left slide-in conversations drawer with a standalone
/coach/conversations page: a search field above a recency-grouped list
(Today / Yesterday / This week / Earlier) built on the existing
conversations query. Selecting a row routes to /coach?c=<id>, reusing the
deep-link open mechanism — no new API, no new state. The drawer's
Conversations affordance now navigates to the new route, and the Coach
page drops its ?view=conversations in-page drawer handling.
Mirror the about-me management routes: the PATCH (confirm/update) and DELETE
on /api/coach/plans/[id] now apply a per-user mutation limit, closing the
consistency gap with the rest of the Coach management stack. Owner-scoped and
cheap, so the limit only caps a runaway client loop.
Adds the Coach goal / if-then-plan memory model (propose-then-confirm,
encrypted, migration 0194) and a standing Coach quality harness (graded
golden set + red-team in CI, optional nightly model judge off by default).
Caches the dashboard snapshot per language. No breaking changes.
Drop the unused extractCoachClaims helper (+ its CoachClaim types) and the
unused CoachPlanStatus type — the deterministic grader uses the boolean claim
helpers directly, and the plan status enum is consumed via the const array.
Update the dashboard-snapshot suite for the locale-keyed cell: stub the i18n
context and add getQueriesData to the fake query client so the locale-aware
preload probe is exercised.
Hotfix: the Coach and daily briefing failed for ChatGPT-provider accounts —
a provider tool-call rejection surfaced as a 500. Provider failures now
degrade gracefully, the Codex tool-call wire format is corrected, and the
provider error body is captured for diagnosis. Also: the thinking bubble
shows only the animated dots.
The pre-stream thinking bubble dropped the visible "Thinking…" word and now
renders just the three bouncing dots. The label stays in an sr-only span so
the thinking state is still announced to screen readers.
A Codex backend 400 on the richer Coach tool-call request (and the daily
briefing) propagated out of the provider chain runner unwrapped: a 4xx other
than 401/403/429 was not classified as a hard provider failure, so it bypassed
AllProvidersFailedError and rethrew from the chat route as an HTTP 500. Plain
status-card completions, which never send a tools array, were unaffected.
Harden the failure path so a provider error can never surface as a 500:
- Treat any 4xx a provider wire returns as a hard failure so it cascades and is
wrapped in AllProvidersFailedError. InsightSchemaError (the strict-JSON 422
surface) stays the only non-hard class, so that diagnostic is preserved.
- The chat route now also classifies a tagged provider error that reaches its
catch unwrapped and emits the graceful coach.provider.* SSE frame instead of
rethrowing. Only an untagged error keeps the 500 + error-reporter path.
Make the Codex 400 diagnosable: fold the redacted upstream response body into
the thrown error message and the chain wide-event, so the field the backend
rejects is visible in logs (it was previously dropped).
Fix the Codex request shape behind the tool-call 400:
- Send the required strict flag on each Responses-API function tool.
- Replay prior assistant turns as output_text, not input_text, per the
Responses API multi-turn contract — the violation only surfaced on round two
of the tool loop, which is the path that broke.
Codex-only; the OpenAI, Anthropic, local and mock providers are untouched.
A deterministic golden-set + red-team floor that gates every Coach change
offline, plus an opt-in live-judge layer that scores warmth and groundedness
with a pinned model only when COACH_EVAL_API_KEY is set.
- 42 golden cases across grounding, cross-metric, data-honesty, provider-parity
and own-baseline taxonomies, graded by behaviour-anchored binary criteria.
- Structured-claim grounding helpers extend the numeric verifier from numbers
to threshold / own-baseline / confident-verdict claims; the numeric API is
unchanged so the existing grounding tests stay green.
- Red-team battery asserts the existing inbound refusal, outbound dose screen,
acute red-flag escalation and learn-link guards fire.
- Nightly + manual workflow runs the deterministic floor always; the live judge
step runs only when the secret is present and is non-blocking.
Add a CoachPlan model: durable, user-scoped if-then implementation plans
the Coach proposes and the user confirms. The background memory-refresh
pass extracts plans the user agreed to and writes them as "proposed"; only
the user-facing PATCH on /api/coach/plans/[id] activates one. The free-text
cue / action / target are encrypted at rest with the shared Bytes codec;
metric, status and dates stay plain so the injection picker ranks without a
per-row decrypt. Active plans fold into the Coach snapshot's memory block
and survive the prompt-budget drop alongside facts. List, confirm and
soft-delete routes are Coach-gated and narrow the owner from the session.
Key the dashboard-snapshot query cell by locale so a locale switch reads
freshly localised tile copy on its own cell instead of serving the prior
locale's prose until the slot goes stale. The zero-arg key stays
byte-identical, so every prefix-based invalidation keeps reaching the
locale-keyed cells; the offline-banner and preloader match on the prefix.
Migration 0194 (additive CREATE TABLE). Encryption-rotation registry and
script cover the three new Bytes columns.
The A1–A3 source files carried v1.22.0 docstrings from before the work was
re-cut as the v1.21.2 point release. Comment-only sweep so blame and the
in-file version tags read consistently with the shipped version.
The "the Coach is already on …" pill resolved its metric label through the
brand-free English domain vocabulary, so it read in English on every
non-English locale. It now prefers the existing localised measurements.type*
name for each measurement-backed source, keeping the English phrase only for
the gait/audio long tail that rarely becomes a scoped launch. Also rewords the
English return-to-baseline line so the day count reads as the in-range run
length rather than the time since.
Surfaces the Coach's existing intelligence as ambient presence: a per-metric
"Coach read", a visible scope handoff and a seeded opener, briefing callbacks,
a tension verdict and return-to-baseline note on the score, two motivational-
interviewing moves, and grounding parity on the local model path. No
migrations, no breaking changes.
The builder is server-only by construction — only the metric-page route
imports it and it reads Prisma — and no other module in the tree depends on
the `server-only` package, so the import was both redundant and an undeclared
dependency. Removing it keeps the dead-code gate clean without weakening the
boundary.
The metric-page "Coach read" strip and the unscoped-hero seeded opener are
Coach surfaces, so both routes now call requireAssistantSurface("coach")
alongside the insights module gate. With the Coach turned off they return
the same refusal every other Coach route does, instead of lingering as a
"Coach read" line while the assistant is disabled. Restores the coach
route-gate inventory invariant.
The hero score card and the daily briefing accept tension, returnToBand, and
memory props, but nothing populated them server-side, so the lines never rendered.
Resolve all three server-authoritatively on the dashboard snapshot DTO (iOS rides
the same payload):
- tension — fires when the readiness composite's contributors disagree (a strongly
favourable one alongside a strongly unfavourable one), reusing the readiness
components the wellness ring grades and the shared disagreement detector.
Suppressed under a clinical red-flag (a coincident-deviation that no illness
explains) so the red-flag path is never reconciled into a calm verdict.
- returnToBand — runs the streak detector across the salient vitals and surfaces
at most one metric that has come back inside its own personal range after a
genuine prior out-of-band run.
- memory — maps the machine-derived coach memory block to the briefing's recall +
forward-look.
The blocks are locale-agnostic (contributor keys + a metric type); the insights
hero localises them through the existing readiness-contributor and metric-name
labels before handing the cards their resolved strings. Each value omits rather
than guesses. The snapshot cache key now carries the locale for the recall prose.
Add a deterministic narrative layer the Coach narrates rather than invents:
- Briefing callbacks + forward-looks: the daily briefing recalls the prior
period and points ahead, anchored to a figure, one callback per surface.
- Tension Verdict: when the readiness composite's contributors disagree
(good sleep but a rising resting pulse), surface a brief honest internal
read on the health-score card. A real clinical red-flag is never reconciled
away into a calm verdict.
- Streaks + return-to-baseline: a pure detector over a DAY-bucket series
closes a worry once a metric returns inside the personal MAD band after a
genuine out-of-band run; a gap day breaks a streak.
- Coach prompt: add develop-discrepancy, roll-with-resistance, and an
anti-persuasion clause; ratchet PROMPT_VERSION.
The tension flag and streak event are computed server-side; the card lines
render resolved fields and the model narrates them.
Replace the hidden composer prefill with a visible scope affordance: when
the Coach is launched narrowed to a metric, the hero shows a scope pill
naming that metric plus a tappable seed question, and the drawer sources
rail surfaces the same scope line. When the Coach opens unscoped, resolve
today's most notable derived signal server-side and offer it as a tappable
opener; fall back to the neutral greeting when no signal crosses the gate,
so the blank box is never cold and never fabricated.
Add an internal GET /api/insights/coach/seeded-question that reuses the
briefing's derived-signal detector to select the opener server-side; the
client renders the resolved DTO.
Render a compact two-line "Coach read" above each metric sub-page chart:
an own-baseline line ("Your usual range is X–Y; today's Z sits within /
above / below") and one top lagged-association line from discovered
correlations, framed descriptively. It makes a metric read seen-in-context
rather than isolated.
The strip is server-authoritative: a new GET /api/insights/coach-read
resolves the band via the median ± k·MAD baseline engine (with its 7-day
history floor — below it the strip says "still learning your range" rather
than inventing a range) and the strongest on-metric driver from the
existing FDR-controlled, effect-size-floored correlation discovery (no
lowered floor; the line is omitted when nothing clears it). The client
component only renders the resolved DTO, so web and iOS read identical
numbers. DTO shapes and the pure selection logic live in a client-safe
sibling shared by the component and the unit test.
Wired through the HealthKit metric scaffold (every generic metric page) and
the bespoke weight, blood-pressure, and pulse pages.
The post-hoc numeric verifier only graded the tool path, so local /
no-tools providers got no grounding net. Grade the no-tools prose
against the snapshot the model was shown — its structured sections,
which already carry the correlations-snapshot block — and flag any
cited number that traces to no snapshot leaf. Non-blocking: the count
is annotated under coach.prose.number_unverified and the unverified
token soft-stripped, never a stream-killer. Only runs when the full
figures were delivered this turn; a cheap follow-up has no fresh set
to grade and no-ops. Tool-path behaviour and tolerance unchanged.
Patch release. Pins the action row in the centred dialog so the primary
button stays reachable without scrolling under long forms, browser zoom, or
display scaling. No migrations, no breaking changes.
The desktop branch of ResponsiveSheet rendered DialogContent as a single
overflow-y-auto grid, so a tall form — or one made tall by browser zoom,
display scaling, or a longer-language label — scrolled its header, body,
and footer together and could push the primary button below the fold. The
branch now runs as a flex column capped at max-h-[calc(100dvh-2rem)], holds
the header and footer shrink-0, and scrolls only the body, matching the
bottom-sheet branch that already pinned its footer. Every form mounted
through the shared surface inherits the fix.
Add a desktop end-to-end guard at a short viewport asserting the body
scrolls and the footer's bottom edge stays within the viewport.
The DateField/DateTimeField overlay input sits inside the wrapper's
border (~42px); the 44px tap target is the wrapper. Measure the closest
date-field wrapper so the sweep reflects the real affordance.
DateField and DateTimeField front an sr-only native date input with a
formatted overlay carrying the 44px target, matching the file-upload
pattern the sweep already exempts. Generalise the exemption to any
visually-hidden or zero-size input.
The daily cap is now provider-aware; the seeded chain resolves to a
user-egress provider, so the refusal must be seeded at USER_PLAN_CAP
rather than the smaller operator cap to trip the gate.
The three intake dialogs (log, edit, dose-history add) still used a raw
datetime-local input, so they ignored the user's date-format preference.
Route them through DateTimeField like every other date surface. Also
correct a stale diastolic-floor comment in the dashboard verdict.