llm.discoverModels was reachable from any declared trusted host. The
method takes a caller-supplied baseURL and makes the host issue a GET to
it, then reports the status or the parsed body — so on a LAN deployment
an anonymous caller had a probe for whatever the host can reach and the
browser cannot, plus a path that carries a draft credential. The
PRIVILEGED_METHODS doc already states the rule this broke: trustedHosts
is a DNS-rebinding fence, not authentication, so the configuration plane
stays loopback-same-origin. It is in that set now, asserted both against
the hand-built fence and over real HTTP beside the catalog reads that
deliberately stay reachable.
supportsDiscovery and listModelDiscoveryNamespaces are gone. The field
was required on the wire and read by nobody: its own contract said a
surface should offer the action "instead of naming an adapter family it
would have to hardcode", while the surface hardcodes llm-pi-ai in two
places and gates the button on whether there is anything to probe. Its
shape did not fit the second caller either — the create card has no row
to read a per-row field from. Keeping a required field alive for a
consumer that may never arrive costs every producer and fixture a value
nobody consults, which is exactly how the fixtures drifted. The registry
that fed it had no other production consumer, so registration and
disposal are now observed through the offer itself.
The Agent Note claimed the key is never logged, which the wire schema
beside it already contradicts, and predated both the provider field and
the catalog-answer path. The two new public types pointed at core.md
without a type-equiv block or manifest entry, so the generated service
catalog named documentation that did not exist.
pi-ai resolves a request's apiKey override only through a provider that
declares an api-key method: resolveProviderAuth short-circuits to that
method when the override is present, and otherwise falls through to the
credential store and then to ambient discovery. A provider with no
api-key method at all therefore resolves to nothing, and the request
fails with "Provider is not configured" before any network I/O.
Two routes hit that. openai-codex ships OAuth alone, so moving off the
/compat dispatch broke a profile that names a key for it — the old path
handed the token straight to the provider. And a catalog route naming an
api was being rebuilt with the harness's own auth, so `openai: {api:
openai-completions}` stopped reading OPENAI_API_KEY, contradicting the
documented promise that omitting a credential keeps provider-native
discovery.
Auth is now one decision for both constructions. A catalog route keeps
its installed provider's auth, through an api override too: which
environment a provider reads belongs to the provider, not to the wire
format its models speak. A catalog provider with no api-key method gets
the harness method beside its own, but only when the profile names a
credential — a keyless codex profile keeps the honest refusal, since
this adapter holds no OAuth store to resolve through.
Materialization now spreads the installed entry instead of enumerating
the result, so a Model field this package does not model survives a
pi-ai upgrade; headers went missing from an nvidia route exactly that
way once already. providerInfo reports the configured displayName, which
also joins the registration facts so a rename re-registers rather than
leaving the old label in every selector. A refused registration swap
gets its own diagnostic naming the route, matching the directory swap
beside it.
The README documented endpoint interrogation this layer does not
implement, and still described unknown providers as kept-last-good after
they became legal declarations refused at the write point. The Agent
Note claimed per-model reasoning configurability the schema never had,
required capacities the route now defaults, and stated an apiKey
override that short-circuits unconditionally.
Three defects surfaced while driving the Models page.
A hand-declared model needed an explicit contextWindow and maxTokens,
but a provider listing usually returns ids and nothing else — so the
page happily wrote a profile the adapter then rejected, which took the
whole namespace down silently. Capacities now fall back to the route's
`defaultContextWindow` (262,144) and `defaultMaxTokens` (32,768). Both
are guesses by construction, which is why they are route fields a
deployment corrects once rather than constants buried in the adapter;
the fallback sizes the model and never becomes a per-request cap.
That silent failure was the second defect. A schema-valid profile the
adapter could not serve was stored and only rejected later, disabling
every route in the namespace with nothing said. `dsh-settings` gains an
optional `validate` on registration — a check for what a schema cannot
express — and `llm-pi-ai` refuses an unserviceable section at the write
that produced it. A stored section that fails keeps the namespace's last
good value, as a schema failure already did, so an externally edited
document still cannot strand the owner. The plugin's own last-good
fallback goes with it: nothing reaching it can fail any more.
Third, a model with no reasoning metadata advertised the single level
`off`, which pi-ai translates to *omitting* the reasoning option — the
same request naming no effort produces. Selecting it disabled nothing,
so a provider whose default is to think kept thinking with `off` shown
as selected. Such a model now reports no reasoning capability at all,
which is the seam's way of saying the control is unavailable, and the
per-model `reasoning` flag is gone: without a thinkingLevelMap to spell
levels it could only invent them.
The protocol table narrows to the three a hand-declared route reaches
today, most-reached first so a surface offering a choice defaults to the
one gateways actually speak.
The merge staged the `--ours` consistency records before the merged prose was
re-recorded, so the committed hashes still describe the pre-merge content and
`verify-translation-pairing` rejects the pair. Re-record all three against
what actually merged.
- `relay` resolves its sender in `contextBody` like every other form. It was
the one shape whose marker could claim a form the body did not render: an
unreadable sender fell back inside the body while the row still said relay,
contradicting the contract this PR's own note states.
- `recall` requires the retained, omitted, and truncated fields. Completeness
is what the card exists to report, so a reference that cannot state it is
not a readable recall — showing the label alone presents a confident card
over unknown loss.
- The snapshot body states the supersession its producer framing line carries.
That line is the one part of the model-facing text no section contains, and
unlike an instruction context's `<system-reminder>` it states the form's own
semantics rather than wrapping content.
- `GoalMessageSource` is a discriminated pair, so `{ form: 'notice' }` without
its account no longer compiles. The guarantee this PR claims now holds at
that seam too, not only through `ContextFormed` on plugin sources.
- Goal and tool-goal summaries are bounded by a shared `boundContextSummary`,
which tool-tasks now uses as well. A goal objective is unbounded caller text
in exactly the way a task label is.
- The runtime snapshot interpolates once per request: agent-loop renders the
sections and joins them through `joinContextSections`.
- Every form's fallback branch is pinned, not only the notice one.
The rebase carried the simple-interface block from the base branch, while
this branch turned ContextFormed into a per-form discriminated union;
ContextSnapshotSection gets its own block so the page stays self-contained.
Four values complete the vocabulary, so the opaque body is reached only by
producers that genuinely promise no shape.
`snapshot` — current state a later snapshot supersedes. system-prompt now
exposes `renderContextSections()`, the named contributions
`renderContextSnapshot()` already joins for the model, so the body attributes
each part to the subsystem that produced it instead of re-splitting joined
prose. The runtime snapshot, time-context, and tmux-context declare it.
`notice` — a one-off account of what just happened, declared by tool-tasks,
goal state changes, tool-goal wrap-up, plan-mode switches, and
repeat-tool-guard. Its `summary` rides the COLLAPSED row: these five are the
majority of shipped producers and none of them needs expanding to be read.
The task summary bounds itself because its inputs are unbounded caller text.
`relay` — a message another agent addressed to this one; both subagent
sources declare it and the body names the sender above what it said.
`recall` — material lifted from another session's log. session-reference
needed no new field: its references already record retained and omitted
counts and the truncation flag, which the body shows first, because recalled
context is bounded on the way in.
`ContextFormed` is now discriminated by `form`, so a producer cannot declare
a shape without the facts that shape is presented from — a notice without its
summary, or a snapshot without its sections, fails to compile.
Only the two hook bridges stay opaque, by design: their content is whatever
an external program printed, so no shape can be promised for it. Unknown
kinds and unreadable records land there too.
- An empty replacement catalog is a real catalog: `renderCatalogUpdate()`
publishes zero entries when the last skill disappears, and falling back
would hide that every earlier name was retired.
- The opaque fallback keeps a `form` declaration this version cannot present.
It is the one place a newer or foreign log's declared shape would otherwise
vanish from the UI entirely, since the row marker is also absent there.
- An instruction change with an unrecognized `action` disqualifies the record.
The action decides the word the row shows, so an unknown one would be
presented as loaded or updated.
- The catalog list bounds itself and reports the withheld count. Entry count
is unbounded and the scrollport bounds height, not node count.
- A catalog message keeps content blocks this version does not know, instead
of dropping model-visible content the extensible union may carry.
- `core.md` defines `ContextFormed`, the interface actually carrying the
optional field, beside `ContextForm`.
- The superseded-in-part bullet states the affected clauses as one rule rather
than enumerating them; two rounds of enumeration each missed some, which is
the shape being fragile rather than the list being wrong.
- The note records the one migration case that does not self-heal: an
old-format catalog as the only one, with an empty current view, leaves a
stale catalog nothing replaces.
- `catalogHistory` validated its durable read. `agent.session.events` is a
JSONL/SQLite seed on resume or fork, and seed validation guarantees only a
source object with a non-empty `kind`; a `skill-catalog` record with missing
or wrongly shaped `entries` threw inside the step listener, failing every
later turn of that session. It is now skipped as an unrecognizable record,
the posture the replaced content digest had, with a regression test over six
malformed shapes.
- The headless keyless smoke still filtered catalogs by the old plugin source,
so the `built-bin-smoke` gate would not have found the catalog message.
- Entries record the published description unescaped. The pseudo-XML escaping
belongs to the `<available_skills>` frame and is applied when rendering it,
so a description containing `<` no longer reaches the card as `<`.
`escapeText` is injective, so republish semantics and the model-facing text
are unchanged.
- Adjacent text blocks join with no separator, matching how provider adapters
flatten them; the body no longer shows a line break the model never saw.
- Provenance fields are bounded like the text: an unknown producer may record
an arbitrarily large value.
- Both readers are all-or-nothing, and the row's form marker reports what
rendered rather than what was declared, so a partly unreadable record cannot
present a confident but incomplete account.
- The catalog body consumes `update` as a replacement notice; the digest
canonicalizes per entry as JSON, since every separator character is itself
legal in a description.
- `core.md` documents the form axis with a `ContextForm` type-equiv block, and
both projections assert the wiring they duplicate.
Assistant footers and the stats line gain TTFT/tok-per-second readings
folded from step timings; context occupancy moves off the stats line onto
a composer ring whose panel shows a heuristic system/tools/messages
breakdown from the new token-meter contextBreakdown session projection.
Injected context reached the transcript as one anonymous shape whose
expanded body was the whole message serialized as inline JSON, so the
escaping collapsed the only readable part — the model-facing prose —
into a single line.
`MessageSource` gains an optional producer-declared `form`, a small
semantic vocabulary of information shapes independent of `kind`: kind
says who produced the context, form says what shape it is, so several
producers may share one presentation. Two values ship.
`instructions` (workspace-context) lists the reconciled files above
their text and keeps the `<system-reminder>` framing verbatim, because
the framing is part of what the model read. `catalog` moves dsh-tool-skill
off the shared plugin kind onto a `skill-catalog` source carrying the
published name/description entries, and the body lists those instead of
re-parsing `<available_skills>` out of the prose. Catalog identity moves
with it: the republish digest now covers the durable entries, deleting
the text-slicing that recovered them from a logged message.
Everything else renders the opaque body — the model-facing text with its
real line breaks, then the remaining provenance as fields. That is the
documented default, not a leftover: a resumed, forked, or foreign log
must render whether or not its producer is mounted here, which is why
the classification lives in the durable source rather than a client-side
table keyed by producer.
core-data-structures/bash.md now names dsh-bash as owner of the shared
exit-status contract parseExitStatus/ParsedExitStatus provide to both
shell tools' presentResult (mirroring the README seam paragraph); the
windows-pwsh-default roadmap no longer claims composition-first ordering
while the rendering stage ships first; the terminal-card-model JSDoc
covers the shell tools, not just bash. Re-record both bilingual pairs.