Fixes a tool-contract bug that 1.1.14's own per-turn instrument caught on
the very first real call — which is the whole argument for having built it.
The live trace:
get_trend - error - Unknown nightly metric 'ahi'. Valid: ['central_ahi'...
get_trend - no_clear_trend <- the silent retry
Asked for an AHI trend, the model guessed the word every clinician uses
('ahi'), the resolver rejected it (the real column is 'total_ahi'), and
the turn burned an entire extra tool round recovering — ~20s of wall-clock
on a local reasoning model, spent on a guess the schema should never have
permitted.
Root cause was structural: metric_resolver.py held the real list of 25
nightly metrics while tools.py declared every metric param as a bare
{type: string}. Two independently hand-maintained lists with nothing
keeping them in sync. The resolver's own docstring had drifted too
(omitting median_epap, p995_leak, cheyne_stokes_pct, temperature_celsius)
— the same bug in a third place, which is the tell that hand-copying was
the problem rather than any one list being wrong.
Fix: tools.METRIC_VOCABULARY is BUILT from known_nightly_metrics() +
known_log_types() + known_metric_aliases() at import time, so the schema
cannot drift from the validator. Add column #26 and every metric-taking
tool advertises it on the next boot.
An enumerated description, NOT a JSON-Schema enum, deliberately: metric
legitimately accepts manual-log composites ('medication:melatonin:dose'),
and an enum of the nightly columns would make 'trend my melatonin intake'
unexpressible. Applied to all 10 metric params across 6 tools — full list
on each tool's primary param, pointer on the secondaries.
Plus a tiny ahi->total_ahi alias as a safety net. 'leak'/'pressure' are
deliberately NOT aliased: each could mean the median or p95 variant, and
silently picking one for the caller is a data-integrity bug.
Also: the load_tools chip reported 'no data' on success (its envelope has
no data key) — now reads 'activated get_trend, compare_periods'.
THE TOOL DIET QUEUED FOR THIS RELEASE WAS MEASURED AND CANCELLED. The
1.1.14 breakdown on a real call: system 1,701 / tools 666 / tool results
523 / history 23 / total 2,913. Vitals' diet was justified because
tool_results dominated a 17-19k call; URSA's is 523 tokens inside an
already-lean 2,913-token request. Rounding floats there would have
reclaimed ~100 tokens on a request using under 10% of the context —
cutting the wrong thing. Measuring first turned a pointless release into
a real one, which is exactly what the sibling note said it would.
Slices:
b65bf08 slice 1 — derived vocabulary + alias + 10-case drift guard
5161321 slice 2 — load_tools chip reports what it activated
fd29c1b slice 3 — docs + version bump
Images: brain40/ursa-oscar-{api,mcp,web,watcher}:1.1.15 (all four pushed).
Tests: 158 passed. Frontend: tsc -b + vite production build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>