**Reasoning effort leaves the provider cards entirely.** It is a per-MODEL
capability and the models under one provider disagree about which levels
they accept: setting `anthropic` to `max` made six of its eight models
throw UNSUPPORTED_REASONING_EFFORT, and because the catalog build catches
per provider, the whole provider vanished from the picker behind one error
row. A provider-scoped control can only ever be set to a value some of its
models reject.
The composer's model picker already offers each model its own levels, and
a switch there now records provider, model, and effort together as the
next session's default — so the setting has a better home at the right
granularity. The profile field stays in `settings.yaml` for a deployment
that knows its route; only the control is gone, from both cards and both
adapter families. Two `components.spec` cases used the control as the
vehicle for their op assertions and now use `baseURL`, which is what they
were actually testing.
**A rejected Provider ID now reads as a fault.** It shared the neutral
hint paragraph with the field's guidance, so the copy telling the user
what they got wrong looked like advice. Reuses the existing `.error`
style, matching the split the key field already makes.
The line under the create form names the one blocked gate worth naming,
and its fallback arm reads "no models yet". An unmet Provider ID gate fell
through to that arm, so a card with two models listed right above it was
told it needed one. The key gate was already excluded for this reason; the
route gate was assumed excluded because its field explains itself, and was
not.
Tightening the route rule in the previous commit is what made this easy to
hit — a digit-leading id now fails the gate — but the fallthrough predates
it and fires for an empty or taken id just the same.
**A hand-declared route must not offer a reasoning effort.** The earlier
commit read the create card's missing control as drift and added one. It
is the other way round: such a model has no reasoning capability — pi-ai's
installed catalog is what supplies one, and it ships nothing under the
route — so `resolveModel` throws UNSUPPORTED_REASONING_EFFORT for every
model on it and the whole provider drops out of the picker. Verified
against the adapter, not inferred. The create card no longer offers it and
the editor withholds it on the directory's `declared` bit, which is the
real bug: that control has always been wrong for these routes.
**A blocked composer locked the way out of the block.** Reusing the
no-workspace inert posture disabled the model seat along with everything
else, so the bar asked for a model while preventing the one control that
picks one. A block now rides its own `blocked` owner prop: the textarea,
send, commands, plan seat, and access chip all lock, and the model seat
alone stays live.
**A Provider ID could derive an illegal credential reference.** The card
accepted a digit-leading id, whose derived `123_API_KEY` then failed at
the credential seam with a raw regular expression the user cannot act on.
The id must now start with a letter, and a test pins the relation between
the two rules rather than the regex.
Carries two edits beyond conflict resolution, both forced by what master
brought in:
- `CustomProviderCard`: master added front-end key validation and a
component-level `keyValue` (already trimmed) while still writing
`apiKeyEnv` unconditionally. Kept this branch's blank-key rule and its
committed-profile retry gate, and adopted master's single `keyValue` so
the component has one spelling of the key rather than two.
- `docs/user/guide/providers`: master merged #1810, whose default-model
section still taught overriding the `api-gateway` row in
`$DSH_HOME/config.yaml` — the behavior this branch replaced. Rewritten
for the settings section the picker now writes, plus the review fix from
#1810 replacing the colloquial 挂着 in the opener.
A default naming a route the Models page has since removed left the
composer saying 选择模型 while the input still accepted a message, which
then failed inside the adapter mid-turn.
`session.prompt` now refuses with `model-unavailable` before opening a
turn. That is the enforcement boundary: the method stays callable no
matter what a client disables. `session.models` reports the same fact as
`routable`, and ui-model pushes a block through the new
`ctx.conversation.blocks` registry so the bar renders the disabled
textarea it already renders without a workspace, carrying the blocker's
own reason. The push direction is forced — ui-model already depends on
ui-conversation, so ui-conversation cannot read it back.
The gate is `routable`, not "matches no advertised group": catalog
membership is advisory, so a route serving a model it stopped advertising
is missing from the groups yet perfectly usable, and `null` before the
first load never blocks so a slow Host cannot lock a working composer.
The scaffold gains a route-only adapter for fixture-less keyless
scenarios. Registering zero providers is a test artifact — every product
composition mounts one — and the goldens that froze the seat's fallback
label now show the model those scenarios actually route to.
Master's credential-lifecycle work taught the editor card that a pi-ai
profile names `apiKeyEnv` only when a key is actually stored, so a route
left blank keeps its provider-native auth path. The create card kept
writing the derived reference unconditionally, so a route declared for a
credential chain or ADC was born pointing at a reference nothing sets —
and now rendered a red missing-key dot for it.
Both cards apply one rule. The obsolete assertion moves with the behavior
(the with-key case is covered by the neighbouring test), and the merged
Models e2e golden shows the declared route unmarked rather than flagged.
A row's stored profile could not tell a hand-declared gateway from a
shipped provider whose models someone narrowed — both look identical from
outside the adapter — so the Models page had no way to mark the routes a
deployment added itself.
The directory entry now carries `declared`, answered by the owning adapter
against its own installed catalog, and the page renders a Custom tag from
it. Absence stays "this adapter draws no such distinction" rather than
"shipped", so a route no adapter claims is labelled neither way.
Also records the default-route work's Agent Note and the e2e evidence for
all three changes: the composer switch writing the section, and the Models
page declaring a route with its own reasoning effort.
ChatView's bottom-follow recognized only wheel gestures as reader input,
so touch panning, scrollbar dragging, and keyboard paging could not leave
the bottom of a streaming transcript. Replace the wheel listener with
device-agnostic attribution: a scroll position deviating from the
observed-top ledger of the last delivered or written scrollTop is reader
input. Adds keyboard-paging and touch-style fling e2e scenarios (red under
the old implementation) and the bilingual Agent Note triplet.
The create card omitted the provider-level effort the editor card offers
for the same namespace, so a route declared through 添加自定义提供方 gained
a setting the moment it was reopened for editing — one the creating user
was never shown.
Both cards now render one shared control. The field, its vocabulary, and
the inherit-means-absent rule live with the control rather than in the
editor, which is what stops the two from drifting apart again.
The route a new session starts from was frozen into the gateway's
composition entry, so switching models in a conversation reached only that
conversation and every later session went back to the shipped default.
The gateway now owns an `api-gateway` settings section: the entry is the
base layer and the user document layers over it, so `session.selectModel`
records an accepted switch as the default for the next session. The write
is wholesale rather than a merge — switching to a model with no reasoning
effort has to clear a stored one — and a storage failure is reported
without undoing the switch, which already applies to its own session.
`targetFor` now resolves its tiers on every read instead of seeding once:
an explicit selection, else the session's own logged request header, else
the live default. That is what keeps a session that has run a turn deriving
its route from its log forever after, while a session still blank — New
Session reuses one rather than minting another — starts from a default
saved after it was created.
The inline comment called every dunder entry "the Python backend's" slot,
but `__debug__` is a CPython compile-time constant, not a seeded slot;
align the comment with the seam declaration, which is the single home for
why each entry is reserved.
- index.ts: RESERVED_ERROR_MEMBERS JSDoc now says `__x__` (non-empty
middle), matching DUNDER_MEMBER and the derivative docs; and
PORTABLE_RESERVED_WORDS is described as covering portable *target*
languages (Python is a later-PR backend, not yet shipped).
- types.ts + type-equiv docs: CodeBindingNamespace.global points to
RESERVED_BINDING_GLOBALS by name with an `e.g.` sample rather than
enumerating the set (single home), no longer implying `__debug__` is a
seeded slot.
- Agent Note: separate `__debug__` (CPython compile-time constant) from
the seeded bootstrap globals.
- README (en/zh): document the four exported exclusion sets and the
portable identifier/naming contract — no cross-package link needed.
- Regenerate cordis services catalog for the shifted source line.
- Agent Note: correct the stale worker-adoption paragraph — the worker
consumes the seam constants directly by name (no RESERVED_WORDS /
RESERVED_ERROR_PROPERTIES re-alias) — and describe DUNDER_MEMBER as
`__x__` (non-empty middle).
- types.ts: document RESERVED_BINDING_GLOBALS on CodeBindingNamespace.global
(names like `__dsh_main__` pass the identifier rule but are still refused)
and the non-empty-middle dunder rule on memberNameProperty; propagate to
the type-equiv derivative docs (both languages, re-recorded).
`____` has an empty middle between the two `__` pairs and does not match
`/^__.+__$/`; assert that (not a match) and add `__x__` as the shortest
real dunder form that does.
- Worker consumes PORTABLE_RESERVED_WORDS / RESERVED_ERROR_MEMBERS by
their seam names directly, dropping the local re-alias (symmetry with
the other two imported constants).
- Split the reserved-vs-duplicate diagnostics: a backend-owned global now
reports "reserved binding global", not the misleading "duplicate".
- DUNDER_MEMBER uses `__.+__` so a bare `__` (empty middle, not a real
CPython dunder) is not matched; add coverage.
- Worker misuse tests add `a$b` (second-char `$`) and `lambda` (Python
keyword) so the identifier narrowing and reserved-word adoption are
each pinned directly, not only transitively.
- Clarify the seam JSDoc (dunder-vs-explicit-set wording, Python backend
is a later stack PR) and record in the Agent Note the obligation to
widen RESERVED_BINDING_GLOBALS when the bootstrap seeds more globals.
Move the reserved-word, reserved-global, reserved-error-member, and
dunder exclusion sets from the worker backend up to the code-runtime
seam package, and narrow the portable identifier subset to drop the
JS-only `$`. Every backend now imports one contract so a binding
namespace list valid on one backend is valid on all.
Delivers only the seam extension and the worker's adoption; the Python
backend, py-types renderer, and Code Mode language dispatch are later
PRs in the stack that depend on these exports.
- subagents.history computes its projections best-effort on both arms
(a hostile unit's fold rejection serves the page without the block,
matching the session-list precedent) with dual-arm coverage
- the lifecycle-witness spec probes every field of the seven-key
witness, protecting the key list itself
- list-children's own module docs catch up with the seq-gate contract,
and the design note records the rung-two later-event window as an
accepted, self-healing deviation of the corruption class
- the cached-identity rung gains a finality gate: the identity value
carries its descriptor seq and a cached row is served only when that
seq lands in the child's own suffix, so a fork seed's replayed
ancestor identity can never outrank the authoritative refold
(stateVersion bumped for the state-shape change)
- the cold preparation validates the inspected header against the
enumerated candidate's lifecycle witness; a republished id degrades to
that child's corrupt diagnostic instead of leaking the new owner's log
- the new projection registration proves HMR disposal; companion notes
qualify the superseded decision text and record the deliberate
error-face asymmetry
subagents.history drops its session-query read: a live child serves its
in-memory events with the registry watermark snapshot, a cold child goes
through inspectServable with a detached fold, and the retired
SESSION_QUERY_* arms disappear while every wire face stays put. The
design note records the shared read source.
- the subagent projection's empty state becomes a serializable null
sentinel (undefined fields vanish in JSON push frames, leaving stale
identities in clients); consumers treat null and a missing key alike,
and cached sentinels defer to the authoritative refold
- the tool catalog generator mounts the projection registry for
list_agents; the shared unavailable-error copy goes catalog-neutral
- the design note's verification section records the new
subagent-diagnostic snapshot honestly, the 2026-07-22 note's test
inventory is rewritten to the current mechanism, and the session-store
error code is documented
Cold children consult the optional session-projection-cache checkpoint
before paying a preparation recompute: the identity is immutable once
appended, so a cached value is definitive regardless of its watermark.
The cache stays a read-only accelerator (absent service or any rung-two
fault falls through silently; verdicts stay with the authoritative
refold), and the note plus core-data-structures pages describe the
three-rung ladder.
- listChildren contains per-child projection faults on both ladder rungs
(any registered unit's fold/schema rejection maps to that child's corrupt
diagnostic) and pins the whole-enumeration listing-failure rethrow
- the base bundle mounts session-projection (web-app's own insert retired
to avoid the double mount); stale composition comment updated
- the shared projections-unavailable wire face is pinned across
list/history/prompt; retired session-query arms removed from the catalog
paths
- the design note records the unknown-parent semantics shift and the
fold-fault isolation rule
The subagent API surface change staled the committed catalog artifacts;
the snapshot fixture agent is referenced only from its cordis.snapshot.yml,
so knip learns it as an entry like its siblings.
- listChildren reads the session store via strict ctx.get (property proxy
is caller-scoped), orders candidates branchlessly, narrows the cold-read
return type, and pins the cost model and store/registry composition gaps
with tests; per-file coverage restored
- acp-agent and headless-agent compositions mount session-projection; a
keyless snapshot pins the descriptor-less diagnostic row
- api-proxy cold spec pins header-origin ownership and the legacy
descriptor-only opt-out
- design note ships as implemented with its English pairing; companion
notes and core-data-structures pages synced