While a turn runs the Coach now shows a single row: a brain icon, the
current step in the answer's text size with a shimmer passing through it
and fading dots, and a chevron once there are steps to open. There is no
running clock any more, and the interim table preview with its sparkline
no longer renders under the row; interim tables stay in state until the
answer's own results show them.
When the answer is done the row reads "Thought process · N steps" with
proper plurals, and "Answer needed" while a clarifying question waits.
It never opens by itself, and a row opened during the run closes when
the turn ends. Opened, the steps form a vertical list with an icon per
kind of work on a thin connector line, and a reasoning round shows its
summary under its title.
The shimmer and the dots are a `text-shimmer` utility and a reusable
WaitingText component, both still under reduced motion.
The demo's write block now carries `meta.errorCode: "demo.readOnly"`,
listed in the error-code catalogue, and the Coach answers it with an
honest line instead of the provider-outage copy.
Removes the interim preview component, the elapsed-seconds and summary
helpers, and their message keys in all seven locales.
remember_fact and propose_plan wrote their rows during the tool round. An
answer that was then blocked, failed or abandoned left the fact in memory
without its note and Undo, and left the plan listed, activatable and fed
into later turns and the briefing. Both are now staged under an id minted
up front and written in the transaction that stores the assistant message,
only when its provenance carries them. A background proposal counts as
offered only once an answer carrying it is stored.
Plan text is model text: ifCue, thenAction and target pass the outbound
dose and risk screen in the person's locale, with their scheduled doses and
medication names, and the injection screen; a trip declines as "unsafe".
The trail route serves recalled facts and the proposal's fact to the owner
whatever the AI capability (only the model-written entries are gated), and
withholds medication facts while the medications module is off.
Accepting a fact proposal is decided under a per-message advisory lock, so
concurrent taps write one fact. Editing a fact re-reads its wording: text
naming a medication moves to "medication", other health wording moves a
non-health fact to "condition", and a health fact the person edited counts
as confirmed.
The signals of the day held today's newest single reading against the
7- and 30-day mean of every reading. For glucose, whose level climbs
with each meal, a fasting morning read as low against an all-day mean
and fed that into the briefing and status prompts. A type in
SAME_HOURS_TYPES now compares today's mean so far with the earlier
days cut at the local time of today's latest reading, the basis the
coincident-deviation flag and the metric strip already use. Below a
week of like-for-like days there is no signal. The signal carries
basis "sameHours" and both briefing prompts say what it means. Other
metrics keep the reading-against-window comparison.
The usual-range strip called the newest reading today's on every
metric page, even when it was days old. CoachReadBaseline now carries
latestDate (the reading's day in the account's zone) and latestIsToday
(isFromToday), on the REST response and the get_metric_baseline MCP
output. The strip names the date when the reading is not from today,
in all seven locales.
The usual-range strip on a metric page placed the latest single reading
against a band built from whole-day means. For glucose the day mean climbs
with every meal, so at nine in the morning a fasting reading sat under a
band of whole days and the strip said "below your range" on an ordinary
morning; the MCP baseline read reported the same.
Glucose is now placed by its day mean. A day still in progress, in the
reader's own zone, is held against the earlier days cut at the local time
of today's latest reading, the comparison the health status already makes;
with fewer than seven earlier days that have readings in those hours there
is no verdict. The baseline carries `basis: "sameHours"` on the coach-read
route and the MCP read, and the strip names the range as the one for this
time of day, in all seven locales. Other metrics keep their placement.
GET /api/auth/me gains coachReasoning: the level a turn runs at, the
person's own pick, the highest level the settings may offer, whether
reasoning can run at all, whether the lowest option is a real off, and
the layer that decided. Resolved on the server so the web settings and
the native client read the same answer.
The web reads it through useCoachReasoning. The Coach preferences route
names the stored level on its wide event, and a unit test round-trips
the level through the account-settings backup section.
What a person told the Coach never reached a tool-mode turn: facts and plans
rode the snapshot's memory block, which tool mode does not send, and the
extraction ran only past twenty turns and failed to parse on every
non-Codex chain, because the status chain runs in JSON-object mode while the
prompt asked for a bare array.
- Memory block: WHAT YOU KNOW ABOUT THIS PERSON, built per turn behind the
wire egress check (nothing is read on a refusal), fenced as data, at most
1,500 characters, never trimmed before the inventory. Confirmed facts
first, then the newest; active plans with a server-computed progress line;
reminders the person asked for. Medication facts stay out while the
medications module is off. Building it stamps last_used_at without moving
updated_at. buildCoachToolRequest and buildCoachProviderPrompts take it as
an optional memoryBlock; the snapshot memory keeps only the narrative and
trend recall.
- remember_fact: preferences, goals and context are saved at once
(source coach, source_message_id set); anything that names a condition,
limitation or medication, by category or by wording, is only proposed.
A fact must come from the person's current message; one note per answer.
- The person's answer reads the proposed fact from the encrypted trail on
the assistant message, never from the request.
- propose_plan writes a proposed plan; the tap activates it with its review
window counted from the tap. One per answer, three open at most.
- Progress: one sentence per active plan from the person's own day series
and units, also offered to the briefing (two lines at most, behind the
briefing capability). The plan review and the progress line read a step
plan as daily totals instead of per-sample means.
- Background extraction asks for {"facts":[]} / {"plans":[]} and reads
either shape, runs once a conversation has been quiet for 30 minutes, and
stores health facts as proposals offered once at the next turn.
- Unanswered plan and fact proposals lapse after 14 days in the reminder
sweep.
- Memory list API: POST saves one of the caller's own messages (the
remember button), PATCH edits a fact's wording, GET reports source, origin
and last use and never lists a waiting proposal.
- A structural guard keeps the egress check ahead of every read and the
callers of both builders to the turn and the briefing.
Add the shared contract the reasoning release builds on, with no change
to how a turn runs today.
- Schema: operator reasoning controls on app_settings (0376), the
encrypted trail text on coach_messages (0377), and source,
last_used_at and source_message_id on coach_facts (0378).
- Provider contract: CompletionParams.reasoning and onReasoning,
CompletionResult.reasoning and providerState, AiMessage.providerState.
- Reasoning levels, thinking budgets and the background-job table in
src/lib/ai/reasoning/levels.ts; coachPrefsJson.reasoning, absent
meaning medium, salvaged field by field like every other pref.
- Stream: activity, memoryNote and planProposal frames, done.stop and
done.withheldResults, result.interim, a compare chart kind, eight
table refs, three more clarification kinds with an assumed choice,
the change_assumption chip, and assumptions, activity, stop,
memoryNote and planProposal on the provenance. Zod mirror, type
parity and the native-client key rule cover every new frame.
- Chat request: memoryDecision and planDecision, parsed and not yet
acted on.
- Stub contracts for the memory functions and the activity recorder,
inert until their implementations land.
- Backup, restore and rotation carry the new columns; a fact's message
reference is resolved against the restored messages like its
conversation reference.
- Message keys for the trail, memory, plans, assumptions, replies,
interim tables, comparison charts and reasoning settings in all seven
locales, each reachable from a catalog and checked both ways.
Only the digest applied briefingForToday. The dashboard snapshot (the
web dashboard, its server prefetch and the iOS cold-launch read) and
the /insights advisor payload still served an earlier day's text and
the raw model deltas ("+33.72 bpm").
The snapshot read now resolves the briefing after the cache, beside
the capability decision, from the payload's generation moment and the
snapshot's own newest reading per type; a withheld briefing reads
"preparing", never "ready" with nothing in it. The digest takes the
resolved briefing from that read on its own clock instead of resolving
again. The advisor route resolves dailyBriefing on every branch it
returns, reading the newest reading per signal type in one grouped
query only when a reading-backed signal is present; text generated on
an earlier day no longer counts as fresh on the read GET (so the warm
is enqueued) and no longer short-circuits a provider-backed POST.
The OpenAPI descriptions of both advisor responses say so.
The same-hours comparison cut the earlier days at the current clock time.
At two in the afternoon a day holding only its fasting reading was then
held against mornings plus lunch, and read as below its range again. The
earlier days are now cut at the local time of today's latest reading.
A range read this way is the usual range for this time of day, not the
whole-day band other surfaces show. The deviation carries a basis, the
health-status API publishes it as `basis` (`day` or `sameHours`), and the
Today lead and the health-status card name the range as the one for this
time of day, in all seven locales.
The muted line under the lead repeated what the lead had just said: a lead
about blood pressure sat above "Your latest blood pressure is sitting in
the optimal band · ↓ ~10 mmHg…". The web client only dropped the headline
when the lead quoted it word for word, and other clients had no rule at
all. The digest now carries `signalLine`, built next to the lead: when the
lead already talks about the top signal's metric (matched in every shipped
language) or says its headline, only the delta remains, and with no delta
there is no line. The hero renders the field verbatim.
Recapture the dashboard, Insights, resting heart rate, Medications and
Coach after a fresh demo seed. The dashboard image now shows the Today
overview and the worth-a-look rail; the Coach image shows the history
grouped by day and a full answer with its steps and action row. Frames
match the previous refresh (2552x1308; the dashboard keeps 2434x1247).
The digest line is the body of the morning push and lands on a locked
phone. It took the briefing's first sentence, the top signal's headline
or the deterministic signal sentence verbatim, so a resting heart rate or
a blood pressure average could sit on the lock screen, and its score
floor stated the score.
A candidate with a figure in it is now passed over for the next, and the
floor says the score is ready instead of what it is. The hero's lead in
the app keeps its numbers. Tests that pinned the numeric line are turned
around, and the push builder is checked on a digest whose lead carries a
reading.
The steady line compared the newest stored score with the number on
screen but not with its band, version or composition, so a score whose
band moved overnight at the same number was still called steady. A run
that reached back past the 120 days read was also told as if it started
there.
The newest stored day now has to match the ring in value, band and,
where the digest carries them, algorithm version and composition. A run
the read cannot see the start of is marked as open (steadyAtLeast on the
digest score) and the hero says "at least" in seven locales.
parseCoachPrefs parsed the stored blob as a whole and returned the
defaults on any invalid field. A lookback value this version does not
know therefore dropped the metric exclusions with it. Each field now
falls back on its own, and the exclusion and cluster lists keep their
known entries, so drift narrows what the Coach reads instead of widening
it.
The deploy runbook's rollback section notes the one case this cannot
cover: v1.40.1 and earlier still parse the blob whole, so an account that
saved the new "Last year" lookback loses its exclusions there.
The vault's filter row gains a procedure facet beside the condition
facet. It lists the record's procedures and surgeries that hold at least
one live document, headed the way the visits page heads them (the
reason, else the practice, else the kind) with the body site and date
beneath, and narrows the list through the existing `encounterId` filter.
Same control, placement and clear behaviour as the condition facet; the
choice rides the URL as `?encounter=`, and an upload dropped into the
filtered view is filed against the procedure.
The usage read publishes the choices as `linkedProcedures` (additive),
decrypting only the label columns, and returns none when a sharing
grant does not cover the visits section.
On a phone an active facet now gives up label width before the search
field does, and the search keeps room for its icon and padding.
Getting started names the APP_MEMORY_LIMIT default and how to lower it
on a 2 GB host; the scaling guide says that web and worker share one
pool in the bundled single-container setup. The rollup comment names
migration 0372, which carries the hourly-mean statistic.
The saved Coach window only seeded the snapshot and each tool's default.
A tool could still read back to all time, the availability probe reported
counts and means for history outside the window, and several blocks read
fixed windows of their own (labs 12 months, correlations 180 days, the
coarse tail, the all-time extremes) whatever the setting said.
coachPrefs.defaultWindow is now enforced as a limit through one module,
history-reach.ts: the turn context clamps the client window, the snapshot
clamps its window and gives every fixed-window block either the shorter
window or, when the block's own window cannot fit, leaves it out. Every
retrieval tool clamps its window, an earlier period or a stored table that
reaches past the limit answers outside_reach, and the probe reads only
inside the limit and names older history as outside_reach without any
figure. Follow-up chips that would read past the limit are not offered.
The default (allTime) is no limit and reads as before. MCP clients pass no
limit and keep their own window arguments.
Adds the twelve-month option, rewrites the picker text to state what is
read with the numbers the readers use, shows the same options in the
managed-profile settings, counts cluster members with a singular, and
drops the opacity on switched-off rows that put their text below WCAG AA.
The hero led with the first sentence of the cached briefing, which was
often a greeting ("Good morning.") while the sentence with content was
never shown. Without an AI provider it fell back to "Your health score
today is N.", which the hero then removed because the ring already
shows the number, leaving a ring and "Nothing needs your attention".
The daily digest now publishes the lead resolved on the server and a
short list of facts about the day:
- lead: the reaction line or the first briefing sentence that is not a
greeting and does not repeat the score while AI text is available,
otherwise a deterministic sentence about the strongest signal (a vital
outside its personal range, a night an hour off the usual, vitals in
range, last night). A fact the lead was built from leaves the list.
- today: up to five statements in priority order (Rest Mode, doses taken
today, an appointment today or tomorrow that is not already on the
rail, last night's sleep against the usual, vitals against their
personal range, cycle day). Module-gated on the server, never padded,
never asking for anything. A phone shows the first four.
- restMode: passed through while an illness episode is active.
- score.steadyWeeks: how long the score has held, read off the stored
daily scores, shown under the ring in place of a delta.
briefingLead skips greetings and score sentences the same way, so the
morning push improves with it. Visit times are rendered in the profile
timezone and hour-cycle preference; the cycle day comes from the same
verdict the cycle page uses. The ring, the rail and the charts are
unchanged. OpenAPI regenerated; additive fields only.
Node sizes its heap from the container's memory cap. The 1g default gave
524 MB, which a long-used account fills in normal operation: on a live
instance 490 to 507 MB stayed live after a full GC, garbage collection
stalled requests for up to 8.5 s, and the process ran out of heap 26 times
in a week, taking the weekly backup with it. 1536m gives 792 MB. The
example env and the backup runbook say how to lower it on a small host.
A weekly or nightly backup job whose process died (a restart, a
container out of memory) stayed active until its two- or four-hour
expiry, and its retry walked the accounts in the same order: the account
that killed it has the oldest copy, so it led again, and every account
after it went without a copy for as long as that record stayed too
large.
Both passes now send with a five-minute pg-boss heartbeat, so a dead run
fails within minutes and its retry picks it up. Each pass records when it
started and finished every account (backup_pass_attempts, migration
0373); an account whose last attempt never finished goes behind all the
others. The backups page shows when the run in progress started and names
an account a run died under, for the weekly and the off-host card, and a
heartbeat timeout reads as what it means.
The drill read only the alphabetically last object of the newest date, so
one account stood in for all of them. It now checks every object of that
date one at a time, records a failure against the account it belongs to,
keeps going, and pages once per failing object with its key and how many
accounts were checked.
Recapture Insights, resting heart rate, Medications and Coach at the
same 2552x1308 frame. The resting heart rate shot no longer shows the
raw metric enum in the chart legend. Images are palette-quantised, about
a fifth of their previous size.
Reasoning models on Ollama and similar servers think by default and can
spend the whole max_tokens budget before the visible answer starts, so
the call comes back empty or times out. The Local and OpenAI-compatible
chain entries now carry an optional reasoning setting: Default sends
nothing, Off sends reasoning_effort "none", Low / Medium / High send
their own value. It is stored on the entry in aiProviderChain, so a
chain can mix a thinking model with one that does not know the field.
- Every exported resolver binds the entry's value onto the client, the
connection test included, keyed on the instance type so the operator
provider, OpenAI, Anthropic and Codex never carry it.
- PATCH /api/user/ai-provider accepts localReasoningEffort and
compatReasoningEffort and GET reports them. A provider absent from
the chain holds its value on a disabled entry; restating Default on a
chain that was never customised writes nothing.
- PUT /api/insights/provider-chain accepts reasoningEffort on local and
openai-compatible entries, refuses it on any other, and keeps the
stored value when the body leaves it out. Its schema is now shared
with the OpenAPI registry.
- A Default request body is byte-identical to the one sent before.
Refs #1126
A practitioner's note, a visit's reason, outcome and body site and a
vaccination's note travelled as stored ciphertext in a portable file, the
same defect the cycle and mood sections had. They now go through the
shared sealed-text helpers: readable in a portable file, sealed under the
receiving host's key on restore, ciphertext kept only where this host
opens it and otherwise reported as visitCiphertext or
vaccinationCiphertext. The practitioner address and phone ciphertext of a
disaster-recovery file gets the same check, since the key preflight now
leaves these sections to it.
The replay cell was keyed on (user, key, method, path). A client that sends
two different writes under one key got the first write's cached response for
the second, and the second was never saved. Native clients up to 1.1.0 post
the two halves of a manual blood pressure exactly that way, so the diastolic
value was silently dropped.
Each cell now stores a SHA-256 fingerprint of the request body (JSON with
recursively sorted keys, otherwise the raw bytes; scoped by method and path;
at most the first MiB is read). Lookup replays, or refuses as in flight, only
when the fingerprint matches. A different body, or a row written before the
column existed, runs the handler without claiming or caching and leaves the
existing cell for its own retries; a lost claim race re-checks the winner's
fingerprint the same way. The body itself is never stored.
Migration 0371 adds the nullable idempotency_keys.request_fingerprint column.
The OpenAPI parameter description, the client notes and the Home Assistant
guide say what a replay now requires. docs/ops gains a read-only query that
lists systolic readings left without their diastolic half, held to the
schema by an integration test.
The labels of the mood tags and mood categories an account created
travelled as the stored ciphertext in a portable file, so a restore onto
a host with another key refused the file or wrote labels nobody could
open. They now follow the cycle note: readable in a portable file, sealed
under the receiving host's key on restore, ciphertext kept only where
this host opens it and otherwise reported as moodLabelCiphertext.
The sealing helpers move to one module both sections share.
A portable file carried the day-log note, the sensitive-category envelope
and custom symptom labels as the stored ciphertext, so a restore onto a
host with another key either refused the file or wrote values nobody
could open. The portable file now carries them readable and the restore
seals them under the receiving host's key; a disaster-recovery file keeps
the ciphertext.
A file written before this still restores: each ciphertext value is
opened with this host's keys first, and one that does not open is kept
out of the row and reported in the skip summary as cycleCiphertext. The
key preflight leaves the cycle sections to that per-value check.