The unit-system and glucose-unit selects invalidated
`userUnitPreference()` and `userGlucoseUnit()` after a save, but no
query was ever registered under either key; every surface reads the
units from the account payload, which both selects already refresh.
The card menu took an optional side-effect item for the GLP-1 card, but
no page ever passed the handler, so the item never rendered; side
effects are logged on the detail page. The slot, its message key in ten
locales and the tests that passed it by hand go.
One test pinned behaviour only the dead slot produced: a delegate who
may write but not manage kept a menu trigger because the test supplied
the handler. In the app that delegate never had one, and the test now
says so.
Four files under src had no production importer:
- the keyset measurement reader for backups: both backup writers moved
to the shared account copy and the streamed full backup, and its
header still claimed both used it;
- the HOMA-IR calculator and the mood stability label, never wired to a
surface;
- the backup verdict helper, which only the backup classification guard
called. It now lives in that guard.
The queue expiry table is what the job queue guards check the registry
against and nothing in the worker reads it, so it moves to the test
helpers. The version-stamp baseline drops the deleted files.
Someone whose doctor agreed a target of 140/90 entered it on /targets;
the chart and the dashboard tile followed it, but the BP and pulse status
texts, the general status, the comprehensive overview, the insight-card
alert and `extractFeatures` (and through it the comprehensive text, the
daily briefing and the Coach) still graded every reading against the age
default. The text under the chart could contradict the chart.
All six places now resolve the band the way the charts do since #1261:
`resolveEffectiveBpTargets` for blood pressure, the PULSE override for
resting pulse, the default only where no target is set. The resolved band
rides in each snapshot's targets block, which is part of the input hash,
so the next generation follows a changed target. The Health Score keeps
its clinical targets on purpose.
The nightly plan review read a before/after window of the plan's metric
and stored an English verdict in `outcomeEncrypted` for every plan. No
screen and no route returned it; only the Coach snapshot read it, behind
the `COACH_EXPERIMENT_VERDICT` flag that is off by default. The worker now
only moves an active plan whose review fired to `reviewed`, which is what
takes it out of the standing group. The flag, its snapshot block, the
three eval cases for that block and the compose and env entries go with
it. The Coach prompt keeps its experiment rules and now ties the
read-back to the plan's progress line instead of the removed block.
Existing stored outcomes stay in backups untouched.
The plans page and the Coach reminders card printed the metric as a raw
key in capitals (WEIGHT, BLOOD_PRESSURE) in every language. Both now show
the measurement's name from the catalogue, through the plan metric
resolver moved to `plan-metric.ts`; a key HealthLog does not read shows
as written. The unused `coach.plans.proposalTitle` string goes.
Every new account finished setup on a full AI panel: a gradient badge, a
sample briefing, three ways to connect a provider, a one-tap offer for the
operator's key and a disclaimer, the most prominent block on the screen.
It is now one muted line with a link to Settings, AI, shown under the same
rule as the Insights hint (could be set up here, nothing configured yet,
not opted out). That hint and the consent request on Insights and
Settings, AI stay the places that invite.
The sample briefing card, the `onboarding.ai.*` strings in ten locales and
the end-to-end spec for the removed offer go with it. The consent-grant
guard's floor drops to the two callers left.
The resolver tests queue their fixture rows through one helper instead of
an `as never` per row, the no-provider status case also proves the
single-provider resolver is never asked, and the size and comment
baselines take the lower numbers the change left behind.
The day view, the day index and the visit preparation each built the
UTC calendar window for a date-only column by hand (documents had a
fourth copy). `dateOnlyWindow()` beside the other date-only helpers
replaces them. The report name select becomes one spread, the share
page builds its calendar formatter outside the view, and the PDF
section reads it off the context, so the functions these fixes touched
stay within their size budget; `DocumentRecordLinks` drops under it.
personal-record-badge.tsx, intake-history-editable.tsx and
intake-history-preview.tsx had no importer outside their own tests and
the preview, so they rendered nowhere while four guard tests kept them
listed as live surfaces. They go with their tests, their guard entries
and the strings only they used.
GET /api/personal-records stays: the native app reads it. The
resolved-field guard now records its benchmark band as a native-only
field instead of naming the deleted badge as its reader.
A mood, symptom, lab value, custom metric value, unscheduled dose,
allergy, vaccination, visit or measurement saved on a weak signal could
be stored twice: the POST committed, the answer missed the 15 s
timeout, the form said it could not save, and the second tap was a new
call with a new key, so the server ran it again.
useIdempotentPost keeps one key until a save lands and renews it for the
next entry, so the retry replays the first answer. The create forms and
the three create hooks post through it; the comment on the per-call key
in api-fetch now says what that key does and does not cover.
LabForm and MoodForm each gain the one line that mounts the key, so
their function-size baselines go up by one. The intake toast guard now
recognises a dose posted through the hook.
Inside a shared or managed record the weight page shaded the band for
the viewer's own height, and the record's own target never arrived
because `/api/user/thresholds` refuses under a switch: a parent saw
their own healthy range behind a child's weights. The BMI page decided
from the viewer's height whether there was anything to show, and the
target gear opened an editor that could neither load nor save there.
The weight page now reads the record's target and height from
`/api/insights/targets`, the payload the reference panel under the
chart already uses, and paints the same wings as before. The BMI page
reads the record's height there inside somebody else's record. The
gear stays hidden in another person's record, as the BMI chart
already draws nothing there rather than a wrong figure.
With the Medications module switched off, dose reminders, missed-dose
rows and low-stock alerts kept coming, a tap on one landed on
"Medications is switched off", and the Telegram buttons still booked
intakes. The reminder tick now drops the medications of an owner with the
module off, one module read per owner and tick, so neither a reminder nor
a missed-dose placeholder is written while it is off. The low-stock pass
skips such a user, as the mood reminder does for its module, and the bot
offers and books nothing.
A medication whose course had ended still counted as live: the low-stock
pass kept warning about its stock, and the bot's /add list offered it and
booked a dose after the end, taking a unit off the stock. The low-stock
pass and every bot path now skip a course that has ended on the person's
clock. A course that has not started yet keeps its reorder warning and
stays in the bot. A paused medication is already active: false, so the
existing filter keeps it out.
The bot's six copies of the medication lookup go through one helper.
The notification matrix listed "Unusual measurement", "Low adherence",
"Withings sync failed" and "Personal record" with a switch per channel,
three of them on by default, yet nothing dispatches any of them: a reading
far outside its range or a week of missed doses produced no message.
eventTypesForRole now leaves them out for every role, next to the
admin-only filter, and the preferences read no longer serves rows stored
for them. Their labels in all locales and their tap targets are deleted.
The types stay in EVENT_TYPES, so the preference write and the stored rows
keep parsing; no migration.
The label and tap-target tests walked EVENT_TYPES and so pinned the four
in place; they now walk the event types that are sent.
Confirming a document's facts creates medications, conditions and lab
values, and the profile take-over creates a condition or an allergy.
Both evicted only the roots of those lists, so the dashboard and the
Today digest showed the old medication set until the 120 s poll, the day
and timeline kept the old state for five minutes, and adherence and
nutrient reads were not evicted at all.
Both now spread the bundles of what they create, and the condition paths
pair them with the inactive refetch of the daily reads.
The report cover, the PDF author field, the FHIR Patient and the share
page printed `fullName ?? username`, and the collector never read the
display name. A managed record has no way to get a full name, so its
generated `managed-<hex>` login landed on every clinical document made
for it; an own account printed the login instead of its display name.
The collector now resolves the name once (`patientName()`: full name,
display name, login, never a managed record's generated handle) and the
five readers print that one field. `patient.username` and
`patient.fullName` leave the report payload; nothing else read them.
Symptoms, workouts, lab results, allergies, documents and questionnaires
evicted only their own lists. An open day, its calendar dots and the
timeline kept the pre-write answer for up to five minutes, so "Capture
for this day" saved a symptom or a workout and the day showed the old
list, and a deleted row stayed on it.
Workouts, labs, allergies and documents get a bundle beside the seven
that already carry the day and timeline roots; the six lab lists become
one (the OCR dialog's own copy of the commit hook's list goes with it).
Symptoms and questionnaires add the two roots to their list. Symptoms
stay off the illness bundle on purpose: nothing on the dashboard or the
Today digest reads them. Custom metrics need nothing: neither the day
nor the timeline reads them. invalidateKeys now drops a key listed twice,
since spread bundles share the two roots.
A rolling reminder ("every N days") is anchored on its last reading, but
the reminder tick rolled any cycle of a week or less on after a delivered
reminder, re-anchoring it on the send. A missed every-two-days blood
pressure reading therefore moved two days ahead each evening, read "in 2
days" on the card, and never showed as today or overdue.
A rolling reminder now stays due after its reminder, whatever its
interval, until a reading lands or it is done, skipped or snoozed. While
it stays open the repeat follows its interval, capped at a week, still in
its notify hour and under the per-day claim. Calendar rules that recur
within a week (a twice-daily course) keep rolling on; appointments are
unchanged.
holdsOpenAfterReminder stays the one definition for the tick and the
edit route. The daily digest asks the narrower question underneath it,
hasLongCycle, so a short rhythm still waits for its time of day on the
start page and is not pinned under the rail cap.
Refs #1369
Ketones had to live in a custom metric, with the glucose-ketone index typed
in by hand beside it, where it drifted from the readings it came from.
- Type: BLOOD_KETONES (beta-hydroxybutyrate, mmol/L, migration 0496),
plausible from 0 to 10: the blood ketone meters read 0.0 to 8.0 and print
HI above it, and a venous value off a lab sheet fits under 10. Zero is a
real reading. No band, no direction, no personal record, off the Coach
snapshot and the correlations; readable over MCP and owned by the glucose
module like glucose.
- Entry: choosing blood ketones in the measurement form also takes the
glucose from the same fingerstick, in the account's glucose unit. Both
rows go through the array arm of POST /api/measurements with one
timestamp, so they land in one transaction or not at all.
- Index: glucose (mg/dL / 18.0182, the tree's one factor) over ketones,
computed at read time and never stored. Each ketone reading above zero
pairs with the glucose reading at the same instant, else the nearest one
within 15 minutes either side; a tie goes to the same source, then to the
earlier reading. Nothing pairs across a wider gap.
- Surfaces: GET /api/measurements/series takes kind=ketones and
kind=glucoseKetoneIndex (each point names the paired readings), and the
day carries glucoseKetoneIndex when both reading types are visible. The
new Ketones page charts the readings with the index beneath; the day view
shows the index beside the value tiles; the doctor report, its PDF and the
clinician share print it in the glucose group when both leaves are chosen,
since beside the ketones it gives the glucose back.
- FHIR: ketone readings export under LOINC 104816-4 (Beta hydroxybutyrate
[Moles/volume] in Blood). The index is left out: it has no LOINC term,
and both readings it is built from are in the bundle.
- The measurement form's action row moved into its own component to keep
the form inside its size budget.
OpenAPI regenerated; all ten locales; the import unit table lists the type.
GET /api/cycle/feed/<hlf_token>[.ics] answers any calendar app with the
record's forecast as RFC 5545: all-day events for the next few periods
over their widening windows, optionally past periods, and for a
conception goal that asks for it the next fertile window. Dates and a
title only; discreet titles and the calendar name "HealthLog" by default.
The body is built live on every fetch in the record's own zone and
language, never the request's. ETag is the content hash and a matching
If-None-Match answers 304; DTSTAMP moves only when the content does.
The feed is anonymous, so every gate is read per fetch: the auth-surface
limit before any lookup, the token's shape, the HMAC lookup of a live
feed, a per-token bucket, then the record and carrier not pending
deletion, the operator's external-access switch, cycle tracking for the
record, and for a feed a delegate minted, a live MANAGE grant covering
cycle. Every miss is the same 404.
/api/cycle/feed-subscription (status, turn on, options, turn off) and
its rotate are cookie-only through requireRecordAuth("manage", "cycle"),
with a fresh proof on turning on and rotating and a per-actor mint
bucket. The URL is returned once; the status carries the prefix, the
options, the fetch statistics as a calendar-app family and the
projection state.
The hlf_ family is registered with the path and token redactors, the
secret-shape refusal, the proxy's public paths and edge headers, the
URL-credential, sharing, recent-proof, step-up and module-gate guards,
the error-code catalogue, the activity feed and the destructive-control
registry. The cycle settings tab gains the subscription card and the
predictions panel lists the later cycles.
A merge into a target without a panel now takes over the first panel
a source carries, in the order the sources were given, so the merged
marker keeps its category. The result reports it as adoptedPanel, on
a dry run as well, and the merge preview says so before the person
confirms. A target with its own panel keeps it. The ledger does not
record panels; a merge has no undo.
The integration suite also proves the clear path end to end: an
explicit null on PUT clears panel, context and both bounds.
Refs #1349
The panel field offers the catalog panels in the page language and the
person's own panels, which GET /api/biomarkers now lists as
customPanels (one spelling per folded form). The field stays free
text; a catalog panel typed in any language lands in the same category.
An edit used to omit an emptied panel, context or bound, so the stored
value survived the save. The form now sends null for a field that held
a value and was emptied. A new marker still omits empty fields, and a
field that was empty when the edit began stays out of the body, so a
context the list could not decrypt is never wiped by an unrelated save.
Refs #1349
GET /api/labs/overview now answers categories and grouped next to the
markers. A marker's category is its panel: a catalog panel in any
shipped language folds to one key, any other panel becomes the
person's own category under an opaque hash key, and a marker without
a panel falls back to the catalog panel of its name. ALIAS_PANEL
places the ten analytes that have names but no seed. Each category
carries its label in the caller's language, its members, counts per
range status and one combined status (above wins over below; no
alarm tone). grouped is true only when grouping adds something.
The list renders this as given: with groupBy set to category it shows
a disclosure header per category in both views, the person's sort
applies inside each category, and groupsOpen decides which start open.
A category opened or closed by hand stays so in this browser until the
choice changes. Settings > Arrangement > Labs gets both choices. The
overview query key carries the locale, since the labels are localised.
Refs #1349
A report that prints a text result such as "SEE NOTE" with a numeric
range beside it minted the marker without bounds: the document commit,
the scan commit, the free-text lab write and the CSV import each passed
null bounds for a qualitative reading, and the CSV row reader and the
scan review payload dropped the range before that. The marker's first
number then had nothing to be judged against.
Every mint path now passes the printed window. A range that is text
itself yields no bounds, an existing marker keeps its own, a transposed
window is not minted, and the reading itself still records no window.
The scan review shows the range on a text row that mints a new marker.
Refs #1313
A blob: document inherits the policy of the page that minted it, and
WebKit applies the inherited frame-ancestors to the blob frame's own
parent. With 'none', Safari on macOS and iOS refused the PDF preview
iframe and showed an empty box; Chromium does not apply it there and
rendered the preview. 'self' keeps every foreign origin out.
Refs #1311
The draft state for the typed fields pushed the form past its size
baseline. The primitives own only their input state and read nothing from
the form, so they move out and the baselines tighten.
Weigh-ins: Garmin's document has no samplePk and gives kilograms, the live
service sends a samplePk and grams. A weigh-in now takes its id from
samplePk or else its own GMT instant, reads each mass by magnitude (above
1000 is grams), and is written whole or not at all: a missing weight, a
missing instant or one implausible value drops every row of it, counted
on the wide event by reason.
Refused resources: a lasting 4xx on one resource (a cycle summary for an
account that tracks no cycle) is noted once on the progress column,
counts as done for the backfill, is skipped by the hourly sync and is
listed in the card's details. It no longer holds the backfill open or
turns the connection red. The cycle leg never parks the connection,
whatever it is refused with.
Backfill: weigh-ins walk back to the start of Garmin Connect on their
own, so history before the first watch day and a scale-only account
arrive. Progress and the completion stamp are written only while the
connection the slice started on is still the stored one.
Key: connect sends the key to the one service the person picks (Garmin
Connect or Garmin Connect China), never to both. A parked key is asked
about again once a day with a single token exchange and reconnected when
Garmin accepts it. The key's end is read from the token's own iat and
exp, an estimate the key outlived is cleared after an accepted exchange,
and the validity can be corrected from the card's menu (PATCH
/api/garmin/key-expiry, with the day the key was created) without
pasting it again.
Sync: an incremental run reaches back to the last successful sync, up to
a year, so an outage longer than the 30-day window leaves no gap; a night
Garmin no longer reports (deleted, or turned into a nap) is retired.
Card: connect errors are translated from meta.errorCode in every locale.
Types: BODY_BATTERY, STRESS_LEVEL, TRAINING_READINESS and
ACUTE_TRAINING_LOAD get source ladders of their own (the device or the
bridge first, then the hand-entered sources in their usual order), join
the ranked types and the provenance picker, and the recovery-empty
description names them.
Garmin Connect now offers a Personal API: a person creates a key for
their own account and any client can buy short-lived tokens with it.
HealthLog takes that key in a new Garmin card, checks it with Garmin
before storing it sealed on the user row, and polls every hour.
- Transport: key exchange with a per-process token cache keyed on the
key, one re-exchange on a refused token and then reauth_required, the
429 Retry-After carried to the caller, the China host tried when the
global one does not know the key, answers read with a byte cap, all
egress through safeFetch to the two fixed hosts.
- Sync: daily summaries, sleep summaries, HRV, weigh-ins and the
activity search, chunked to each resource's documented range, read one
after another and stopped at the first rate limit; today's menstrual
cycle summary adds logged period days the person has not logged.
- Backfill: a resumable walk through the shared admission lane that
learns where the history starts from the daily summaries and the
activities, waits out Garmin's rate limit and stamps its marker.
- Types: GARMIN source; BODY_BATTERY and STRESS_LEVEL for Garmin's own
day scores, kept apart from the computed STRESS_SCORE; TRAINING_READINESS
and ACUTE_TRAINING_LOAD for figures a bridge pushes through the ingest
token. All four chart on the recovery page and sit on the ladders,
units, ranges, labels and report groups like the device-native scores.
- Key expiry: learned from a token capped short of a day, else estimated
from the validity picked at connect; the card warns a week ahead and
asks for a new key once Garmin refuses the old one.
- Ledger, health, freshness, watchdog, backup classes, wipe plan,
encrypted-column registry, rotation script, OpenAPI and all ten
locales carry the new integration. The old "no Garmin connector" note
is gone.
The stored bytes are already a Buffer and the MIME literal already fits
its union, so neither needs a cast; the external pick goes through one
helper. The as-never ratchet tightens accordingly.
ensureDbCompatibility ran ALTER TABLE / CREATE TABLE IF NOT EXISTS on the
login, register, token and session paths, mirroring columns every
migration under prisma/migrations already creates. The entrypoint applies
those migrations before the server listens and stops the boot when one
fails, so the mirror never had work to do, and DDL on a request path
needs the table's owner: a connection that may only read and write rows
fails every login on it. Drop the module, its call sites and the mocks
that stubbed it out.
The desktop table and the mobile cards carried the same status badge
twice. Pulling it into one component makes room for the injection-site
chip without growing the list function past its size baseline, and
the baseline tightens to the new, lower count.
- baselines: take main's entries, keep the two ru-driven line counts (coach-prose-grounding 1091, insight-generator 1115) and regenerate the function baseline.
- messages/ru.json: translate the five unit-less markers keys the release added (labs.unit.none/noneHint, labs.ocr.errorUnitMismatchNoUnit, labs.ocr.unitDiffersBadgeNoUnit, recordImport.unitlessHint) — parity is 11 159 keys again.
knip 6.44 follows the members a dynamic import reads, and the Insights
page reads two of the derived barrel's: the barrel now exports those two,
whose building blocks are imported from their own modules. BAND_VAR had
no reader, and the drawer re-exported useResettableValue for a test that
only imports nextResettableValue. The size and version-stamp baselines
take the lower numbers.
Dose bank
- DRUG_UNIT learns мг / мкг / мл / ЕД / МЕ, so "2,4 мг" anchors a dose again.
- The bank matches verb-first Russian word order too ("принимайте 2 таблетки",
"пропустите дозу метформина", "начните с 5 мг"), and the Cyrillic drug names
people actually see on the box; the deferral words ("спросите врача",
"как назначено") stand those shapes down.
Cyrillic-safe regexes
- \w and \b are ASCII in JavaScript: every stem in the ru bank and in
population-norms/ru.ts is [а-яё]*, and the word edges are lookarounds
(?<![а-яёa-z]), so «высокий риск» and «нормальный диапазон» match again.
- coach-prose-grounding: Russian month names in the genitive, a space as the
thousands separator in parseLocaleNumber and numFrag, and Cyrillic-safe
edges around the written-date pattern.
Registered everywhere a locale has to be
- targetLanguageName() returns "Russian", so the coach, the period narrative
and the document descriptions stop saying "write in English".
- i18n-key-tool.mjs, split-description.mjs, LOINC name normalisation (it
stripped Cyrillic, so no Russian lab name resolved), HOST_TLDS (ru/by/kz/ua),
no-key-fallbacks (32 tables), country-label, the AUDIT-C standard drink.
- docs/api/openapi.yaml regenerated.
Questionnaires
- The PHQ-9 / GAD-7 source comments no longer claim no validated Russian
version exists (Zolotareva 2023; Pfizer's official screeners).
- WHO-5 items are gender-neutral: «чувствовал(а)».
- "body fat" and "fat mass" both read «Жировая масса»; body fat is now
«Процент жировой массы», which is what the two metrics measure.
Tests
- ru in the 13 locale tables, plus mood, onboarding, instruments, clinician
view, status summary, describe screen, citation coverage and LOINC.
- i18n-no-dot-separator: the ru bundle is exempt from the dash rule (a spaced
dash is Russian punctuation, not a fragment join); the middle dot still
fails there.
- Two 800-line baselines raised by the 12 and 5 lines this change adds.
pnpm typecheck, pnpm openapi:check and the full suite (36 316 tests) pass.
The briefing's lab input passed the stored "1" to the model, the unit
mismatch refusal read "is in 1", and the trend card left a gap where a
language puts the unit before a particle. get_labs says that a value
without a unit has an empty unit, and a lab link id is scrubbed like its
label. The review row is back at its baseline length.
The links fetch lists beside a visit, condition, document or vaccination move
out of record-search into record-links, so the lab and medication reads can
list theirs through the same builder. A linked document imported from
Paperless-ngx or Papra now names its source system and its id there, never
the instance address. fetch document: also lists the lab results read from
it and the medications filed against it, each behind its own module.
A confident lab value that Finish review refused (no unit, or a unit that disagrees with the marker) showed the reason but no way to act on it, so it could only be discarded. The Check control now also opens on a row with a commit failure, not only on a low-confidence one.
FactRow grows by one line (the data-slot the new test finds the control by), so its size baseline moves from 262 to 263.
Refs #1313
The upload creates a passwordless account before it reads the file and
deletes it again when the file is refused. The only audit row was
written after a successful store, so a refused upload whose delete also
failed left an account nobody could sign into and no record of where it
came from.
The account and its admin.user.create row (outcome "created") now
commit together, and the discard writes admin.user.delete with outcome
"deleted" or "failed", so a leftover account always has a trace. The
runbook says how to remove one by hand.
With the server naming lab markers as value lines, the page offers them:
the values menu lists the markers the record has numeric readings for,
under the groups the labs list sorts them into, and a choice is stored
and requested like any measurement line. The phone toolbar gains the same
menu as an icon button, since the chronicle shows the chosen lines too.
A lab row sits under the value lines in their rhythm, on its own scale.
Each reading is its own point on the day of the draw, joined in time
order by a thin stroke and never averaged. The reference range is painted
behind the points as the labs page paints it, a faint band with dashed
edges, and the scale holds it so the band is always in view. A reading
outside its range is a triangle pointing the way it left and is worded as
outside wherever it is written out, so colour is never the only sign.
A point reads on its own by pointer, keyboard and tap: the day, the value
with every digit it was stored with, the verdict, the range and the
report it was read from (a link once pinned). The selection bar lists
the bucket's readings with their report, a lab day's row in the phone
chronicle lists that day's readings, a screen-reader table lists them
all, and a marker with readings in a unit that does not convert says how
many were left out. Nothing else on the timeline changes.
To make room without growing the large timeline files, the legend, the
screen-reader tables and the value-line geometry move into modules of
their own, and the chart, the chronicle, the point cards and the page
each hand a part to a helper; the size baselines are written back lower.
Refs #1299
A fixed preference list goes stale at every retirement: when GPT-5.5
retired for ChatGPT sign-in on 2026-10-14, every model the shipped list
named was refused and Codex answered 503 until a release changed it. The
working model also sat in one slot for the whole process, so two sign-ins
on different plans overwrote each other's model and the vision gate read
whichever had answered last.
The client now reads the models the account is offered the way the Codex
CLI does (GET /backend-api/codex/models?client_version=..., same sign-in
as the responses call; shape from codex-rs/protocol/src/openai_models.rs)
and walks: CODEX_MODEL first; our preference order kept to what is
offered; the other offered models in the list's priority order, leaving
out hidden, specialist and frontier-tier entries; the rest of our order
last, so the walk never holds fewer models than before. With
CODEX_MODEL_FALLBACK_CHAIN set the order is the operator's and the list is
not read.
The list is a hint, never a gate. The read goes through safeFetch with a
5 s bound and a 4 MiB cap, is shared by concurrent requests, cached per
sign-in for an hour, and a failed read waits ten minutes. Any failure
leaves the static walk, one ai.codex.models_unreadable warning per sign-in
reports it, and each request that builds the walk annotates a pinned
codex.models.* shape.
The working model, refused models and the list are kept per stored
credential (a person's connection, or the operator-shared central one),
bounded by TTL and a 1000-entry LRU, with a digest of the account id so a
credential reconnected to another account starts empty. A model the list
offered that then answers model_not_found goes to the back of that
sign-in's line for six hours. When the list names a model's input
modalities, the Codex vision gate reads them; otherwise the model family
decides, as before.
Refs #1287
The reasoning-field planning and the refusal learning are a self-contained
part of the Codex wire. Moving them out of codex-client.ts leaves room for
the per-sign-in model selection that follows without growing a file the
size ratchet already holds at its limit.
Refs #1287
The dense fold deleted Google Health and Oura samples outright, on the
reasoning that no client pulls them. The change feed serves every
source, so a client mirroring it kept those samples next to the hourly
means with nothing to tell it they were gone.
The nightly increment of a server-pulled source is tombstoned again: the
feed reports each sample and the tombstone cleanup purges them after the
retention. A backlog day, such as an account's first fold, is still
deleted outright, and the account's delta cursors expire instead, since
tombstoning millions of rows would undo the point of the fold. The
compaction-tombstone class leaves these sources out, so the feed, the
backlog purge and the restore treat their tombstones like any deletion.
Apple Health and Health Connect are unchanged.