A converted reading's bounds move with its value, but the printed range text
stays in the unit the report used, and every reader preferred that text: the
list showed "Report range 3.9 to 5.5" beside 100.9 mg/dL, and the doctor
report and the FHIR referenceRange.text did the same. The text is still
stored verbatim. The range resolver now takes the reading's sourceUnit (as a
required field, so no reader can forget to select it) and, for a converted
reading, leaves the window's text empty so every surface states the converted
bounds in the marker's unit, and hands the printed text over separately with
its unit. The lab DTO carries it as printedReferenceText, and the note beside
a reading shows it as "as printed in mmol/L: ...".
The token ledger keyed every day on UTC while the daily briefing and the
other per-day generations run on the account's local day, so east of UTC
the first generations after local midnight were refused against the
previous day's spent ceiling. The budget day is now the calendar day in
the account's time zone; each request still resolves it once and books
its reservation and reconcile on the same row. The medication extraction
reads relative phrases against the same local day, the operator's spend
card shows the operator's own day, and the limit copy says the budget
resets at midnight in the person's time zone.
Every raw-SQL clock read against a timestamp column without a zone now
writes NOW() AT TIME ZONE 'UTC', so passkey and login challenges, rate
limits, provider pauses, coach usage stamps, tombstone and measurement
windows no longer pass through the session time zone even where the
per-connection pin cannot apply. The regression-window bounds stay naive
UTC timestamps instead of being turned into timestamptz. A structural
guard keeps bare NOW() / CURRENT_TIMESTAMP out of raw SQL outside a short
list of timestamptz targets. The integration test sets the database
default to Europe/Berlin and drives challenge claims, session expiry, the
native sign-in handoff freshness check and the rate-limit window through
the app's client, plus the challenge claim through an unpinned client.
Self-hosting docs describe the pin for an existing or shared Postgres.
A dash between two parts of a sentence ("{what} — tomorrow", "Could not
record {name} — try again", "End {name} — …") stands in for a comma, a
colon or a full stop the sentence should say. Every such string in the
seven bundles now says it in plain words. A range keeps its unspaced en
dash ("70–100 mmHg", "{start}–{end}"); that is how a range is written,
not a pause, and the spaced range forms became unspaced ones.
Labels the components put together themselves follow the same rule:
aria-labels read "Move up: Ramipril", a medication and its dose read
"Ramipril 5 mg", page titles use "|", Coach prefills are two sentences,
and the API's user-facing error messages lost their dashes too.
The bundle guard now refuses " — " and " – " beside " · ", with an empty
allowlist, and a sibling guard walks src/components and src/app for a
string or template literal that joins its parts with a spaced dash.
The day view gains a scores section below the values: the health score as
recorded that day, the readiness the nightly job persisted for the night
that ended that morning, a device's own recovery, the sleep score of that
night and strain (computed, or the device's day strain on its 0 to 21
scale). Each tile carries the person's usual range over the 30 days before
and opens the score's own page; a score the day does not hold has no tile,
and a day without any has no section.
Every value comes from where its page reads its history: the stored health
score record, the persisted RECOVERY_SCORE and STRAIN_SCORE rows read on
the day they describe, the canonical recovery ladder, and the sleep score
computed over one bounded read of the 30 nights up to the day. The stored
score rows leave the values list of the day they are stamped on, so a
readiness no longer appears a day early among the readings.
Scores follow their own module (recovery, sleep; the health score is core)
and map to the whole record for sharing: a scoped grant sees them named as
not shared, a whole-record grant sees them. get_day sends them to the
Coach and the MCP endpoint by name and number, and the Coach exclusions
take out every score built on excluded data.
A running cycle now states its cycle day and phase ("Cycle day 12,
follicular phase"), computed the way the cycle calendar does. Only logged
cycles count, the next logged start closes a cycle, and an open cycle
stops counting past its typical length plus the grace window instead of
reading a day count nobody recorded.
Every bundle value that chained facts with " · " now reads as a phrase,
a comma list or a parenthesis, in all seven locales, and the em-dash
separators in the day, documents, correlation, sharing, environment,
vaccination and Apple Health import strings become sentences or clauses.
The day view's running list names the record first, with the dose in
the name and a life event's category as a chip, and puts a quiet day
count below: "Day 4, since 31 December" instead of a dot chain. Document
meta, location periods, vaccination rows and mood crosstab rows drop
their composed dots too.
A guard walks every bundle and fails on any " · " outside a reasoned
allowlist, and on an allowlist entry that no longer needs its place.
A tab outlives a logout, another login and a switch into a managed
profile. A delete re-sent in another scope met a 404 that read as
"already gone" and dropped the entry, or tripped the record fence and
toasted a failure for a row the person could not see. Entries now carry
their owner, only the current owner's are re-sent, and a session end
clears the journal with the rest of the session state.
A find-then-delete pair let two deletes of the same conversation both
find the row, and the slower one failed with a 500. A client that
re-sends a delete it could not confirm now gets 404 for the second
request, which it reads as already gone.
The integration test covers the thread, its messages and its document
links going with it, the document itself staying, the list no longer
carrying it, a foreign account getting 404, and the concurrent pair.
The "Running that day" list carried the profile facts (smoking status,
alcohol pattern, shift schedule) as running items, titled by their raw
codes and counted "day n" from the date the fact was filed. A person who
quit smoking years ago and entered it last week read "day 13". Those facts
describe the person, hold no start the person gave, and are not part of a
day; the lifestyle section and its running kind are gone from the day, its
index and the model-facing get_day.
The same false count reached a medication filed without a start date: its
creation date stood in as the start. `since` is now null there, with no
day count, and the entry's date only bounds which days show it. A life
event's category is worded in the reader's language instead of its code.
get_day read through the day loader rather than the snapshot, so it
ignored the person's Coach exclusions: a person who kept mood or
medications from the Coach had both sent to the model as soon as it
asked for a day. Each exclusion token now maps to the day sections and
measurement types it covers; the sections are not loaded and the types
are dropped from the projection. A guard runs every token through the
executor.
A dated document reached the model under its title or file name, and a
visit under its reason or the practitioner's name. Both now carry their
kind. The remaining titles are fenced as the person's own text in the
Coach, as the MCP endpoint already did.
Replace the day, timeline and life-event stubs with their implementations
(#613) and drop the not-implemented helper, its error code and its OpenAPI
response with the last stub.
loadDay reads one local day for every reader: the readings live inside the
record's own day window (never the UTC rollups), through the canonical-source
engine the mood sheet already uses (resolveDayReadings in linked-context.ts),
with the person's usual range over the 30 days before; the record sections
from their own tables, each in the storage form it speaks (an instant in the
local window, a date string with its own zone, a calendar date). A switched-off
module's section is not read; a section the grant does not cover is named
not_shared and not read.
The day, the day index and the new visit preparation (GET /api/day/notable)
are admitted on the readings and narrowed per section through
actingDomainVisibility, so a scoped grant sees exactly its sections. The
timeline and its readiness stay whole-record reads and narrow their lanes the
same way.
Notable days are deterministic keys: a three-month high or low with history
behind it, a first reading, and in the visit preparation a regular reading
that stopped. The timeline lays out spans and points from the owning tables,
standing items without a date, and series by zoom: months from the rollup
tier inside its five-year window and from one cached live query before it.
The readiness inventory rates each lane with one link per gap.
Life events are created, edited and soft-deleted as MANAGE verbs with audit
rows, encrypted at rest, and join the sharing guard and the handler matrix.
get_day reads a day for the Coach and the MCP endpoint without life events,
notes or document names; its step carries the date it read.
A date is now a door to its day (#613). `?day=YYYY-MM-DD` opens the day
over whichever page is open: docked beside it from 1280 px, the way the
Coach's conversations panel sits, a sheet from the right below that, and a
bottom sheet on a phone that opens at a little over half the height and
grows to full height from its handle. On /coach it is always a sheet, so
there is never more than one docked panel. Back and Escape close it, the
previous and next day replace the history entry instead of adding one,
Alt with an arrow steps a day, focus moves to the day's heading on open and
returns to whatever opened it on close, and a polite region names the day.
A malformed or future date is dropped from the URL.
The day shows the value it was opened from with its comparison, what ran
through the day, the readings against the person's own usual range on a
short number line (eight curated, the rest behind "All values"), and what
happened in order of the clock, each row linking to its record. Loading,
failure, an empty day and areas a grant does not cover each read as such.
The footer offers the timeline when that module is on and capture for the
day, with the capture forms starting on that date.
Doors into it: a click on a daily point of a metric chart (a tap shows the
value and the tooltip offers the day on touch screens), the dates in the
chart's data table, a quiet row of dots under the axis for days with
entries and a ring for notable ones, the lab chart and lab readings, the
measurement, mood and intake lists, symptom and illness day headings,
episode onset and recovery, document dates, workout dates, the workout
detail, the mood and adherence heatmaps, and the days a Coach answer's
get_day calls read, shown as chips. The next planned visit carries what
happened since the last one, each date opening its day, with a doctor
report for that window.
Not here, and frozen by a guard: the dashboard's today area, mini charts
in tiles, the cycle calendar, model-written text, future dates, the
clinician share link and the doctor-report PDF.
Fetch the Open-Meteo air-quality feed (CAMS, domains=auto) with the weather:
17 hourly variables folded into local days by fixed rules (means and maxima
only with 18 of 24 hours, ozone as the highest 8-hour running mean, UV only
with the midday hours), a value the feed did not cover stored as null, never
as zero. Requests are cut into 90-day chunks, newest first, and chunks before
mid 2022 ask for the eight pollutant and index variables only. An air-quality
failure never costs the weather row; the day stays unfetched and the nightly
gap fill (up to 90 days per account, newest first) asks for it again.
Every Open-Meteo request asks an instance-wide budget kept in Postgres first
(500 a minute, 4000 an hour, 8000 a day, weighted by the hosted pricing
rule). A refused request is not sent and the run ends as done with
budget_blocked. OPENMETEO_AIR_QUALITY_URL and ENVIRONMENT_AIR_QUALITY_DISABLED
are honoured; the account switch lives on PATCH /api/environment/preferences.
The home, each dated location period and each stored day keep their coarse
location sealed as one value under healthlog/environment-location/v1. The
writers write the sealed value only, the readers open it and fall back to the
readable columns for rows the free-text encryption backfill has not reached,
and the backfill now seals all three and clears the readable columns. The
backup carries the location readable in a portable file and sealed in a
disaster-recovery file; a restore seals either under the receiving key.
Three correlation channels join the environment family (PM2.5, the ozone
8-hour high, the day's highest pollen count), read through the averaged
same-day window and left empty while air quality is off. The Coach and MCP
gain get_environment: module-gated, clamped to the lookback limit, no
coordinate or place name anywhere, with provenance and the attribution
lines. The settings section gets the switch, the coverage and the newest
day's values; the dashboard shows a quiet chip on a day with high pollen,
a warm night or very poor air. Copernicus and Open-Meteo are credited next
to the values.
iOS and watchOS 27 add `HKQuantityTypeIdentifierHeartRateVariabilityRMSSD`
(Apple's symbol `c:@HKQuantityTypeIdentifierHeartRateVariabilityRMSSD`,
available from 27.0). The live batch and the export.zip import now store it
as `HRV_RMSSD`, the type WHOOP, Oura and Polar already write, in
milliseconds, mean-aggregated and flagged privacy-sensitive like SDNN. The
accepted range is 1 to 300 ms.
RMSSD ranks on its own `hrvRmssd` ladder in the SQL source collapse, so a
day holding it from Apple Health and a strap resolves to one source
instead of a name tiebreak.
SDNN and RMSSD are different statistics and are kept apart once one
account holds both. The HRV page charts each on its own, titled with its
measure, instead of hiding RMSSD behind SDNN. The Coach's HRV section
carries one timeline per measure under `byMeasure` (same section key, so
every reader still finds it) rather than one daily series averaging the
two; the budget pass condenses and sheds each measure's timeline like a
glucose context.
A catch that drops what it caught is where the silent nightly failures
started. Every `catch` and `.catch()` under src/lib/jobs now rethrows,
fails the job, writes to a ledger an operator reads, counts into the run's
failure facts, hands the error back to its caller, or logs at warn under a
stable action. The enqueue helpers that swallowed a failed send (briefing,
status, narrative, morning digest, document index / summary / thumbnail,
workout insight, boot-time sweeps), the Coach nudge's provider fallback,
the workout and reaction-line cleanups, the advisory-lock release, the
backup attempt bookkeeping and the cycle reminder's dedupe reads now leave
a warn line instead of nothing or an info annotation.
`error-level-discipline-guard` holds the rule. The catches that stay
silent are listed with their reason (the pgboss readers' documented null on
a web-only deployment, the key-ring trial, the restore drill's per-account
result, an already-deleted Telegram message); a new silent catch fails, an
entry that no longer matches fails, and each entry is shown to be
load-bearing by removing it.
A Coach turn cut short by its token, time or round budget writes
`coach.turn.budget_stop` at warn.
Two daily series that both follow the season (temperature and blood
pressure, daylight and sleep) correlated strongly with nothing between
them. In a seeded simulation of such a pair the engine surfaced a
finding in 58 % of 400-day runs and 30 % of 180-day runs, and a real
next-day effect under the season came back with the wrong sign.
Every daily pair now goes through seasonallyAdjustedPearson: a linear
trend and one annual harmonic are removed from both series (trend only
under 120 paired days), the residuals are correlated, and the p-value
uses the Pyper-Peterman effective sample size. Shrinkage and the
phrasing tier read the effective size too. BH-FDR and the effect floor
are unchanged. Pairs too short to fit the basis are not tested.
The weather channels are tested once over the mean of the day before
and the day itself against the same day's outcome (lagDays 0), which
pairs the overnight low with the night it shaped now that sleep is
keyed on the wake day. Discovered pairs carry an additive nEff, the
same-day templates are wired, and the matrix skips the weather read
when the environment module is off.
scripts/compare-correlation-findings.ts counts findings per account
before and after, read-only, for the release-candidate dry run.
Own-plan turns get 165k tokens, 200 s and 16 rounds, operator turns 55k,
120 s and 8; the final-answer reserve, the concurrent-turn slot and the
step cap move with them.
Codex requests carry a per-conversation prompt_cache_key and a stable
session id, api.openai.com gets the same key, Anthropic usage counts
cache reads and writes in the gross, and local servers report cached
prompt tokens, so the turn budget discounts cached input on every
provider.
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.
The Coach loop's final round sent toolChoice "none" without the tool
definitions. With tool_use/tool_result blocks in the history Anthropic
refuses such a request (400), and OpenAI Chat Completions refuses
tool_choice without tools, so every budget, time, cap and no-progress stop
ended in an error on those wires. The final round now carries the tools
with toolChoice "none", which every wire accepts, thinking included.
Codex kept reasoning items as provider state even when the request asked
for no encrypted reasoning; replaying such an item under store:false is a
400. Items are kept only when the request included
reasoning.encrypted_content and the item carries it.
The reasoning dialect cache learned from any 4xx that mentioned
reasoning or thinking (auth, rate limits, model names such as
deepseek-reasoner) and from replay errors, and kept the downgrade for the
life of the process. Learning now requires an HTTP 400 that refuses a
sent parameter, ignores replay errors and the model name, and a learned
downgrade expires after an hour.
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 dose-change class demanded a dosage-form noun next to every skip, stop
or extra imperative, and a drug name never counted. "Skip the amlodipine",
"then take an extra ramipril", "take one more tablet of ramipril", "Nimm
abends eine Tablette mehr", "Lass das Ramipril morgen weg" and "Stop your
blood thinner before the run" all passed.
A medication noun is now a dosage-form word, a drug-class word per locale
(blood thinner, Blutverdünner, anticoagulant, statin, insulin, ...), a
common generic-name stem (-pril, -sartan, -olol, -dipine, -formin, ...),
or one of the person's own medication names. The Coach loads the names
with the turn context and passes them to the reply screen and the trail
screen; a scheduled brand name is only known that way.
Add the extra / another / one more shape in en, de, fr, es, it, pl and ko,
spelled-out counts as a dose target, and stop / leave out / go without a
named medication. The number-free shapes use a two-word slot that a
preposition ends, so "take an extra minute to log your medication" and
"stop worrying about your medication" pass. A sentence that only names a
drug, a schedule description, a past change from the log, a negated
warning and a referral ("ask your doctor whether you should stop ...")
still pass.
"Increase your metformin to 2000 mg daily" passed the outbound dose
screen. The English change pattern accepted only "it" or "your dose"
between the verb and "to", so a drug name in that slot walked past it,
and no pattern covered an imperative to take a stated amount, a count
unit (tablets, capsules, puffs), or an imperative to skip or stop a
medication. Each locale bank had the same narrow object slot.
Add a per-locale dose-change class (en, de, fr, es, it, pl, ko) with
three shapes: an imperative or infinitive change verb with up to four
words of object and a target dose; an imperative to take or inject a
stated amount; and an imperative to halve, double, skip, pause or stop
a medication. Negated imperatives, past-tense log restatements,
present-tense descriptions ("you take 1000 mg"), clinician referrals
without a target, and nutrition advice (sodium, water, steps) still pass.
A unit now ends on an explicit lookahead instead of \b, so a lab
concentration ("under 140 mg/dL") no longer trips the dose bank while a
rate ("2000 mg/day") still does. "as usual" and "wie gewohnt" join the
continuation anchors, still gated on the user's schedule.
Build the memory block for the providers the turn sends to, send it on
both paths, keep the facts it recalled in the owner's trail, and offer a
health fact the background found as the answer's one note. Every turn
enqueues the memory refresh; the job's debounce and singleton key decide
when it runs. A remembered fact points back to the stored user message.
Unit tests for the turn budget and the no-progress brake, the budgeted
loop (every stop reason, the round-by-round ledger, repeated calls,
questions, comparisons, reasoning and provider state), the trail recorder
and its screen, clarifications through the tool and the brake, the
comparison table and chart, the trail column and its owner-only read, the
reply frames, and the turn's frames end to end through the route. An
integration test proves the trail against a real database. Eval
scenarios cover a multi-round why question, a period comparison with its
chart, a question asked through the tool, and keep the charts the
existing table scenarios already showed.
Take the effective reasoning level from the route rather than resolving
it in the turn; the turn only caps it when the operator pays.
Give the Coach one statement of how an answer is shaped, in the German and
English bodies and on every other locale: the first sentence answers, one
carrying number with its unit and window, the person's own baseline, an
assumption in one line, at most one next step and no habitual closing
question, 40 to 120 words, and a closed list of filler it never writes.
- Brief is 30 to 70 words in one paragraph, detailed up to 220 in three.
- Motivational-interviewing moves only when the person opens a change topic,
at most one, never as a closing question; the confidence ruler only on an
explicit plan request.
- The few-shot examples are rewritten to model the voice.
- Result tables: show the numbers whenever they carry the answer and span
more than one value, a table for exact values or up to twelve like rows,
one figure alone only for a moment.
- Follow-up chips are the answer's only "what next".
- A voice grader for the eval (length, filler, emoji, next steps, closing
question, warm-up opening) plugs into the golden set as criteria; five
voice cases in German and English.
- A guard holds both prompt bodies to the rules and every example answer to
the grader, each check broken once on purpose.
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.
Replace the fixed three tool rounds with a budget fixed by who pays:
120k tokens, 150 s and 12 rounds on the person's own plan, key or local
model, 40k, 90 s and 6 rounds when the operator pays. Cached input counts
at a tenth. The final answer is reserved from the start, so a turn whose
budget, clock or round cap runs out answers from what it read. A
no-progress brake stops a turn that repeats calls or reads nothing new,
and a repeated call is never run again.
The day's ledger is booked round by round: round one and the final
reserve when the turn starts, each further round right before it runs,
every round settled as it returns, the unused reserve given back at the
end. A round the daily ceiling refuses ends the turn with an answer.
Every phase goes out live as an activity frame (thinking, fetching,
summarising, asking, remembering, planning, the stop, the answer), with
screened reasoning titles and summaries; the structure rides the
provenance and the text is sealed in trail_encrypted, served by
GET /api/insights/chat/{id}/messages/{messageId}/trail. Tables go out as
interim results as soon as they are read.
Add ask_clarification with a server brake, remember_fact and
propose_plan over the memory contract, model-free decision turns,
compare_series with the compare chart, eight tables per message,
assumptions with change-assumption chips, a reasoning level per turn and
at most two concurrent turns per person.
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.
The summary of a condensed block was taken over the block's mix of daily
values and weekly means: over a year the newest fourteen days weighed as
much as fourteen weeks, min and max were extremes of means, and change was
one day minus a weekly mean.
Each series block now hands the budget pass its daily values over the
whole read, computed only when the block is condensed. The summary states
the first and last day, the lowest and highest day, the mean of the days,
and the change from the first calendar week to the latest one. A block
without daily values gets no summary rather than one of what is left. The
coverage note says how the summary is taken.
The condensing step read every block as a value series: daily rows under
value, weekly rows under mean and count. Sleep nights carry minutes, so a
condensed sleep summary ended on the last weekly mean weeks before the
newest night; adherence rates fold by doses, not readings; glucose keeps a
series per measurement context; workouts are a sessions list.
Condensing now knows each block's shape: sleep reads minutes, adherence
folds rate weighted by total doses, glucose condenses every context, and a
workouts block keeps its newest sessions beside the whole-window rollup.
The Coach's figure check ran before the MCP branch, so an MCP client was
told retrieval_failed for a block it used to receive, and the plain-text
renderer then said no data was on file. Any single-source build also
condensed its own block, which reached MCP through the source snapshot's
fallback to the full builder.
The figure check now runs after the MCP branch, and condensing is an
explicit option of the snapshot build that only the Coach tools set (it is
part of the cache key). MCP output is what it was before.
A block trimmed to its unit and freshness stamp passed the figure check,
because asOf.daysAgo is a number. The check now skips the asOf and scope
fields, so that shape answers retrieval_failed like any other block
without a reading.
Inside a chat turn every tool read lands on the turn's shared full-source
snapshot when its window matches. On an account with many synced series
that build passes the prompt budget, and the budget pass collapses blocks
to a bare unit and then to an `{ omitted }` marker. get_metric_series
sliced the marker out and answered `present: true` with it, so the model
was told the readings exist and was given no figure. The same held for
sleep, glucose, compliance and workouts.
- The snapshot result names the blocks the budget pass cut. A tool whose
block was cut in the shared build reads it again from its own source at
the same window.
- A read scoped to one source condenses its own blocks last and never
swaps them for the marker: it keeps a summary (first, last, min, max,
mean, change) taken before anything is cut, the newest daily values,
weekly means folded into monthly means, and names each cut.
- get_metric_series never answers present with a block holding no figure;
that read reports retrieval_failed instead.
- A chat series read carries the readings it counted, so the step shows
them, and on a long or condensed window a note on what it covers. The
tool description and a new tool-mode rule tell the model to call
get_metric_table itself for longer history instead of asking.
- The screen-reader comma between step parts is unselectable, so a copied
row no longer reads "Weight · ,all time".
MCP output is unchanged: the source-snapshot equality suite stays green.
Reading a stored reply back drops a view chip whose table the answer
already shows with its toggle, but the resolver reads the raw stored
chips. A tap from a client that still shows such a chip is answered, by
design: the chip was a valid offer when it was stored. The test pins both
halves for "as a table" and "as a chart".
A table the answer displays with a chart renders its own chart/table
toggle, and the "Show as a chart" / "Show as a table" chips under it
repeated that control. The server no longer derives a view chip for such
a table; a table the answer only used, folded under "Data used", still
gets "Show as a chart", since that brings it into the answer. A reuse
turn shows its table with the toggle and so offers no view chip either.
The rule is one predicate in a leaf module, applied where chips are
derived and where a stored reply is read back, so a reply stored before
the change offers the same chips as a new one on every client.
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.
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.