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.
Key rotation rewrote every ciphertext column through a plain Prisma update,
in the in-app corpus walk and in the rotation script alike, so a rotation
stamped the run time on every encrypted row: the Coach panel then listed
old conversations as recent, and every client re-pulled measurements and
mood entries whose sync cursor had moved. Both now read each row's
updatedAt and write it back, matching on it, so a row edited since the
read is left for the next run instead of being stamped. The models that
carry the column are listed beside the registry and checked against the
schema, and the updatedAt guard now covers the rotation writes.
The terms that decide whether the Today lead already names the top
signal's metric were bare stems: "risen" and "chosen" matched the Polish
"sen" (sleep), "impulse" matched "puls", "Passing" matched the Italian
"passi", and the Korean "잠" matched "잠시" (a moment). A lead about
weight then dropped a sleep headline. Every term now has to start a word,
whole-word terms have to end one (Unicode-aware on both sides), and the
Korean terms carry their particle.
Today's glucose mean was held against a band built from whole days. By
mid-morning the day holds a fasting reading and nothing after, so an
ordinary morning read as "below your range". A glucose day still in
progress is now compared with the earlier days cut at the same local clock
time, read in SQL so a dense stream is averaged whole rather than through
its newest fifty rows. With fewer than seven earlier days that have
readings in those hours, today gets no verdict. The day-aggregate reader
gains the local-clock cut; an integration case pins it against the
in-memory fold in a half-hour zone.
The title encryption backfill moved each Coach conversation's title into
ciphertext with a plain Prisma update, which stamps updatedAt. The Coach
panel orders and groups conversations by that column, so threads untouched
for months showed up under Today. The memory summary refresh did the same
in the background.
Storage-only rewrites now carry the row's own updatedAt and match on it, so
a turn that lands meanwhile wins and the row is retried on the next pass:
conversation titles, practitioner contacts, measurement and mood notes
(where updatedAt is the sync cursor), inventory notes, and the mood-note
script. The summary refresh writes through raw SQL. A guard freezes both
rules: every backfill write on a model with @updatedAt names the column or
is listed with the reason the stamp is wanted, and every conversation
write outside the person's own actions keeps it.
When most days in the window carry the same value, the median ± k·MAD band
has no width: the coach strip read "Your usual range is 61–61 bpm" and the
next ordinary reading of 62 counted as outside it. The shared band builder
now keeps the edges at least the measurement's own day-to-day wobble apart
(per metric, in its canonical unit, falling back to one display step), so
every surface built on it reads a real range and classifies against it.
The reported spread stays the measured dispersion.
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.
A lead past the 160-character budget was sliced at a character count, so
a long briefing sentence reached the hero as "…sleep landed r…". Model
text now leads only when it fits whole: the reaction line keeps the
complete sentences that fit, the briefing leads with its first sentence
when it fits, and otherwise the headline or the deterministic sentence
takes the slot. Only when no source fits is the first sentence shortened,
at a word boundary. The budget is unchanged, so the hero keeps its
height.
The digest is polled every 120 seconds. Its cycle line read the whole
cycle history, 90 days of day logs and wrist temperature on every poll,
outside any cache, and the coincident-deviation engine walked its eleven
vitals one after another, two queries each.
The cycle read now rides the digest extras cell, which every cycle write
already hard-evicts; the cycle preferences route evicts it too, and a
module switch does through the health-score invalidator. A record without
a cycle profile stops after one point read. The per-vital reads run
together and are folded in registry order, so the result does not depend
on which finished first.
On a seeded four-year account (1.25M measurements, 52 cycles) under a
792 MB heap, the warm digest went from 9.9 to 5.0 ms and the vitals engine
from 11.1 to 4.6 ms (medians of four interleaved rounds).
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 lead filter dropped any sentence containing the score's digits, so a
score of 52 threw out "You slept 7 h 52 min" and a resting heart rate of
52 bpm. A sentence now repeats the ring only when it names the score (in
any shipped language, or as "out of 100") and carries the number on its
own, not as part of a time, a decimal or a measurement with a unit.
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.
The derived block shortened each engine's window to the lookback limit,
so a 7-day limit produced a readiness computed over 7 days instead of the
30 the ring on the dashboard uses, under the same name. A block whose own
window does not fit is now left out, as the lookback rule says, and the
coincident-deviation flag follows its 30-day baseline the same way.
The GLP-1 block sent every dose change with its decrypted note whatever
the lookback limit said. Dose changes are entries, so only those inside
the limit reach the prompt; the dose in force stays as the current dose.
buildMedsTodayBlock ended the day at local midnight plus 24 hours. On the
day the clocks go back that stops an hour early, so a dose between 23:00
and midnight was neither projected nor counted in the doses tally. The
exclusive end is now the next local day's start, as the digest loader
already computes it.
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 earlier-tables block names each stored table with its window and row
count. The list was filtered by module and exclusion only, so a table
stored before the limit was set or narrowed still told the model how much
history it covered. It now answers to the same range test show_result
uses, and the history-reach guard checks both the filter and that the
call site passes the limit.
The provider-chain summary marked every entry available, so the Coach's
provider select offered entries without a key or sign-in. Choosing one
saved it to the front of the chain, the resolver skipped it, and the
select jumped back without a word.
Availability now comes from the presence probe, which names every chain
type whose credential is present. After a switch the picker reads the
chain back and says so when the server still answers with another
provider.
Taking the weekly preview from the writer dropped the schema check the
old scanner ran at the end, so a copy that violates the schema was
described as restorable. The collector now checks each section in its
JSON form as the writer shows it, an array section and a bulk table one
element at a time, plus the schema's one whole-document rule (a
canonical copy names every measurement by id). A copy that fails gets
no stored preview, and the preview route reads the copy itself, as it
did before. Nothing is kept beyond one element.
Measured on the preview memory test under --max-old-space-size=524:
held after the producer 3 MB with and without the check, raw peak
heapUsed 293 MB with it (335 MB without). The test now loads the
backup schema before its baseline: a module loaded once per process,
not memory that grows with the record.
A sample carrying the HealthLog origin marker was always dropped from an
export import. After a move to a new instance without a backup, the
export is how the mirrored values come back, and every one of them was
silently left out. A marked sample now counts as written by HealthLog
only when its HKExternalUUID names a row of this account of the same
kind, deleted rows included; a marked cycle sample only when the
account holds a day-log on its day. Every other marked sample is
imported, and still meets the manual-value match. A correlation's own
HKExternalUUID now covers records that carry none.
The day weight joined its source to an hour grouping of itself. With
stale or missing planner statistics, as after a new account imports a
year of pulse below the autoanalyze threshold, the planner ran that
join as a nested loop and the all-time pulse aggregate ran into the
60 s statement timeout. The weight is now two window passes over one
scan: the readings in a row's hour, and one marker per hour summed over
the day for the hour count. The weights are unchanged.
A new integration test seeds over 100k pulse rows with autovacuum off
and no ANALYZE, bounds the all-time reader and a zoned per-source mean
well under the timeout, and compares every row's weight against the
earlier self-join after ANALYZE. The ANALYZE the metric-series snapshot
test needed for the same trap is gone.
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.
An account whose last backup attempt never came back was kept behind
every other account for as long as its attempt stayed open. A pass that
regularly stops on its time budget never reached it, so it never got a
copy again. It is now held back only until a later run has started on
another account; if its record kills the process again, its attempt is
the newest once more and it goes last again. A crash loop still leaves
the other accounts their copies, and the account itself is attempted at
least every second run.
The drill now reads every account's newest off-host object but still ran
on the default fifteen-minute expiry, without a lock and with two
retries, so a large instance could see it killed, run beside itself, and
page every failed account three times. It now carries a four-hour expiry,
stops between accounts on its budget and reports the objects it did not
reach, holds a pass lock at the binding, and is not retried.
A hover prefetch excused the mount refetch even after a dose logged
elsewhere had invalidated the medication reads, so the page showed the
compliance from before the dose. An invalidated entry now always
refetches.
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.
The Coach page gets a conversations panel on the right of the thread. From
1280 px it docks beside the thread and slides open and shut; the reading
column keeps its width either way and only its centring moves. It is open
on a first visit and remembers the last choice per device. Below 1280 px it
is a sheet from the right. The toggle sits at the trailing edge of the top
bar, through a small page-owned slot the top bar now renders.
The panel lists every conversation grouped by recency, with a row menu to
rename in place (Enter saves, Escape cancels without closing the panel) and
delete with the existing undo toast. The undo window now survives the sheet
closing. New chat is the round button at the panel's bottom right, and
picking or starting a conversation keeps ?c= in the URL.
The gear in the panel header opens quick settings: a popover from md up, a
bottom sheet on a phone. It switches the answering provider (chain reordered
through the existing route), the model and reasoning level where the person
owns them, and shows what the Coach can see. Keys, the Codex sign-in and the
fallback chain stay in Settings > AI. The drawer's gear opens the same
overlay. The composer's + menu is gone.
Settings > Coach links to /coach?settings=data, which opens the overlay on
what the Coach can see instead of a rail that no longer exists.
The parity test built a fresh Intl.DateTimeFormat for each of ~36k
comparisons and crossed the 5 s timeout on CI runners. One formatter per
zone halves its time; it also gets an explicit 20 s budget, since it is
meant to be exhaustive.
backupPayloadSchema is built when validations/backup.ts is evaluated and
holds about 12 MB of heap. The restore job, the stored-backup readers and
the export writer import that module, and they sit on the boot path, so
every server process built the schema at start whether or not it ever
touched a backup.
The schema version, the compatibility check and the per-section summary
move to validations/backup-summary.ts, which never builds the schema. The
call sites that validate a file (upload, restore, the summary and download
routes, and the streamed reader's element check) load backup.ts with a
dynamic import at that moment; everything else takes only types. The
OpenAPI registry keeps its static import, since only the spec generator
loads it.
After warm-up on a production build under --max-old-space-size=524, the
used heap drops by about 29 MB, and the schema no longer appears in a heap
snapshot. A structural test refuses a new static value import of the
module outside the registry.
get_metric_series (and get_metrics, the metric resources and the review
prompts, which fan out over it) answered with one section of a Coach
snapshot, but built the whole snapshot to get it: the all-type feature
extraction, the derived wellness layer, trajectory, correlations, memory,
GLP-1, cycle, illness, labs and visits. On a seeded account with about
215k measurements a resting heart rate read ran 208 SQL statements and
blocked the event loop for 175 ms to return under 2 KB.
The MCP surface now asks the executor for a source snapshot. It runs the
same prelude (prefs, module gates, admitted sources, cutoffs, now shared in
snapshot-prelude.ts) and the same block builders over the requested
source's reads only. The core clinical aggregates come from extractFeatures
scoped to the one block, whose bulk read keeps its all-type cap so the rows
a block sees do not change. A section large enough for the full builder's
prompt-budget pass to trim, and the environment cluster, go to the full
builder, so their results are the old ones by construction.
The integration test compares the tool's serialised result with the full
builder's section and grounding for nine sources over four windows, the
get_metrics batch and the metric resource, on a seeded account dense enough
to put the feature row cap into play.
The in-memory day-aggregate fake re-filtered every row for every day,
converting each timestamp to the profile timezone again, so a 1000-day
series took about five seconds and the graded pulse-status test hit its
timeout. It now groups by day and segment in one pass (4.8 s to 85 ms).
The medications list and the compliance summary refetch on every mount,
so a take or skip logged on another device shows on return (#316). The
nav link's hover / touch / focus intent and the route-commit preload
prefetch both reads too, so every visit through a link sent each of
them twice: the prefetch, then the identical request from the mount a
few hundred milliseconds after the first answer landed.
The mount now skips its refetch only when the cached data answers a
prefetch that went to the network for this visit: within the last 10 s,
and since the route last left /medications. A direct load, a return
with an older entry, or a quick return whose prefetch found the entry
still fresh keep the refetch on mount.
A Coach snapshot, a derived-metric batch or the Insights page fans out
into a dozen readers that each probe the same account's rollup coverage
at once. In production one MCP get_metrics call held 59 of these probes
open on the database together, each taking about 320 ms under that
contention, and the same burst repeated every 15 minutes.
Concurrent probes for one account now join the query already running.
Nothing is kept after it settles, each caller gets its own map, and a
measurement write drops the in-flight entry so a probe that starts after
a write always reads afresh. The registry lives on globalThis so every
server bundle shares it.
A document whose preview render fails for good (a PDF whose first page
renders blank) was queued by the boot-time backfill, refused, and queued
again at the next boot, without end.
The thumbnail job now keeps the failure with the document
(document_thumbnail_failures, migration 0374): the reason, the attempts
since the last success and when the newest failed. The backfill, both the
boot discovery and the per-account pass, leaves a document out after one
deterministic refusal (raster-failed, empty-render, pixel-cap) or three of
any other kind, until the document changes. A direct enqueue still renders
it, and a preview that is made clears the record.
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.
Since v1.39.6 the preview scanner parsed the JSON on its way into the
pieces, kept every section but the three bulk tables, and validated the
whole document at the end: a second copy of the record beside the
writer's own, inside the weekly job. On an account with a document vault
that is most of the record.
The writer now shows each section and each bulk row to an observer just
before writing it. The preview collects key uses from that and takes its
counts from the writer's own FullBackupCounts; nothing is parsed or kept.
An upload hands over the summary and key uses it has already validated.
The round trip pins the writer's counts and key uses to a full read of
the copy on an account with a row in every model, and a memory test holds
what the pass keeps once the producer is done.
Measured on 70,000 measurements, 3,000 mood entries and 40 one-megabyte
documents under --max-old-space-size=524: held after the producer
56 MB before, 3 MB after; peak heapUsed 398 MB before, 334 MB after.
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.
A text column written before key ids existed holds the bare base64 of
nonce, tag and ciphertext. The key check read such a value as an encoded
Bytes column, decoded it once too often, and accused the v1 key of not
opening a copy it opens. The instance's stored AI key has exactly that
shape and is the shortest v1 value in every disaster-recovery copy, so it
was picked as the probe and refused every restore of an account with v1
values, even with the instance settings left out of the verdict.
A legacy value now reads as a string unless its decoded head is base64
text. Samples are kept per section and only the sections the verdict
counts are probed; a key is unreadable when none of its probes opens.
Binary values open with the label their column is sealed under, taken
from the encrypted-column registry, and are not probed when no label is
known. A preview stored by an earlier release is still read, and its
sample's layout is derived again from the value.