Presets own the rows that decide what a session's `/` menu contains, but
both browser catalogs cache per session and had no invalidation edge for a
recompose: `commands/changed` is registry-wide and recomposing registers
nothing, so the menu kept serving the composition the session no longer ran.
The host stream now frames the logged `agent-preset/selected` commit as
`host/session-preset-changed`; the runtime bridges it to the typed
`session/preset-changed` event, `ui-command` soft-refreshes that session's
directory key and `ui-skill` invalidates its catalog entry.
Reaching the host on a second switch was a separate defect: the list-row
identity guard compared every summary field except `agentPreset`, and the
merge keeps the row's `updatedAt`, so a switched row looked unchanged and
served its cached instance forever. The hero chip compares the pick against
that row, so switching back to the creation-time preset sent no RPC at all.
Delete design-session citations (decision/audit/plan ordinals, stack
positions), change narration, review choreography, and reviewer-addressed
justification from comments, JSDoc, docs, READMEs, Agent Notes, tests, and
generator templates; restate every affected fact as current-state contract
prose. Fix generated docs at their sources and regenerate the catalogs and
cordis-surface regions; re-paste type-equiv blocks; update every bilingual
counterpart and re-record the pairs. Record the citation rule in the
committed-artifact-citations Agent Note.
A hand-damaged preset was silent until the worst moment. An unparsable
composition listed as an ordinary selectable row and failed only at the
next session start — set as default, every new session failed. A
directory whose composition file was deleted vanished from the roster
while still occupying its id: copy answered "delete the existing preset
first" while remove answered "not found", a dead end.
Discovery now owns health: every id-shaped directory is a roster slot,
broken when its composition is missing or unloadable, checked with the
loader's own entryListSchema dialect (!!js included) so health never
rejects what the loader accepts. `broken` rides AgentPreset, the
agentPreset.list entry, and the UI row; mount/recompose/standingKeyFor
refuse broken up front with the discovery-reported reason, while
resolve/read/remove still answer. The section renders marked red cards —
unselectable, uncopyable, deletable, location kept on custom rows — and
both pickers drop broken rows entirely.
The cordis preset's persona now forbids editing the shipped install
(corrupting cordis would disable the mode itself) and points authoring
at $DSH_HOME/.agent-presets; its skill teaches preset.yml metadata, the
copy-first workflow, the one-escalation sandbox reality, and honest
verification. Exercised live: asked to edit the shipped composition the
composed agent refuses citing both rules; asked for real presets (simple
and complex) it lands them under the user root with one approved
escalation each and self-checks with the loader dialect.
The web YAML editor is gone. agentPreset.write (arbitrary composition
text) became agentPreset.copy { from, agentPreset, name? }: a host-side
whole-directory copy of ids the host resolves itself — symlinks
dereferenced, modes re-tightened to owner-only with owner-execute kept,
metadata rewritten to keep the source's description but never its name or
roster order. No composition text or path crosses the wire in either
authoring direction, and the entryListSchema/!!js concern dissolves with
assertComposition itself.
The settings section becomes: a read-only viewer over shipped
compositions, a copy dialog (id + optional display name) as the only
create entry, delete for custom rows, and a location action leading into
the preset's own files — agentPreset.openDocument { agentPreset } resolves
the directory host-side and opens it natively, or answers
{ opened: false, path } for the row to show as text where the deployment
has no desktop. agentPreset.list reports hasDocument beside authorable;
the gateway's nativeOpen config pins the capability where
canOpenNativePath platform detection would mislead. The privileged set is
now read/copy/openDocument/remove.
With files as the only composition editor, standing mounts grew
stamp-keyed generations: ensureStanding compares the composition file's
mtime+size and starts the next generation for later sessions, while every
joined session keeps the generation it runs on.
New keyless web lane (agent-preset-authoring, overlay pins
nativeOpen: false so goldens render one branch on every platform) drives
view/copy/reveal/delete end to end; the real-composition CLI e2e switches
to copy semantics.
The composer's preset seat stays removed here — this layer moved it to the
hero chip and the session-header action — so only the model seat takes the
`modelSeatLocked` narrowing master introduced.
Conflicts:
apps/web/tests/snapshots/*/*.expected.md
packages/client/connection/README.md
packages/client/connection/README.zh.md
packages/client/ui-conversation/src/client/skeleton/InputBar.tsx
packages/host/apiproxy/src/fetch/client.ts
The Client API carrier's `agentPresets` member was the one member of its class
without an `IApiClient[...]` annotation. Inferring it inlined `AgentPresetEntry`
into the emitted declaration by the specifier TS picks — the host `index.ts` —
dragging the whole gateway, and with it the host `Context` merges, into every
Client program importing the carrier. Annotated like its siblings.
`ApiRemoteAgentOptions.setup` now takes the inspected session rather than its
header alone: this layer resolves a resumed session's preset from the LOG,
because a session that switched while blank ran its turns under the newer
composition and the header is written once at creation.
Conflicts:
apps/web/tests/snapshots/*/*.expected.md
packages/client/ui-conversation/src/client/skeleton/InputBar.tsx
packages/host/apiproxy/src/api-proxy.ts
scripts/doc-budgets.manifest.json
Invocation is an ordinary session.prompt again: the pre-step gesture
boundary makes it deterministic host-side for every front end, so the
dedicated RPC (handler, wire schema, error codes, client face, fixtures)
and ui-skill's claim machinery are net deletions. The menu keeps decision
21 exactly — a pick lands literal /name text — plus the user-only marker
from skill.list's modelInvocable flag.
Deleting the preset a user default names left the setting pointed at an id
nothing will ever supply again, and every session created without an explicit
pick then failed to start — the delete dialog called it 'new sessions cannot
select it', which understates a hard creation error. `remove` now clears the
user layer when it named the preset just deleted, exposing the deployment's own
default underneath. Storing a default that does not exist YET stays deliberate:
the roster is a live directory, so a name absent now may exist by the time a
session asks, and `resolve` still reports that case.
`agentPreset.select` also leaves the loopback set. It was pinned as a real
escalation — one preset mounts the toolset that edits the live runtime — but
`session.create` already takes an `agentPreset`, so pinning only the switch left
the same capability one method over. The deeper reason is that the capability is
not the preset's to grant: the deployment's own default already carries `bash`
and the filesystem tools, so any caller that may start a session at all can
already run commands as this process. `read`/`write`/`remove` stay pinned on
their own footing — those touch files, not sessions.
Bilingual README updates for the four touched packages (ui-skill's claim
flow and deterministic-injection model experience, the apiproxy skills
domain, the shared renderSkillContent seam export, the catalog stitch
sentence), the implemented Agent Note triplet recording the decision and
its peer-product evidence, and the regenerated catalogs/graphs.
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.
The Web picker collapsed onto the directory flow (see the one-route-to-add-a-workspace
Agent Note), leaving workspace.create({ name }) with no product consumer. Delete the
whole feed line: the wire schema's name member and WorkspaceApi spelling, the gateway's
workspaceRoot config/default and the mkdir branch, the client seam that carried the name
(WorkspaceCreateInput, WorkspacesService.create, intentName), the dsh web
--workspace-root flag, and the fixture's name handling. workspace-name-conflict stays as
workspace.rename's duplicate-title error.
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.
A composition is a file, but "edit it on the filesystem" is not a browser
affordance. The roster gains `read`/`write`/`remove` beside `select`, and
the browser gains a settings section over them: the presets as rows, one
composition open in a YAML editor at a time, and per-row default, duplicate,
and delete.
All four authoring methods are loopback-pinned. A composition names the
plugins a session runs, so reading one is reconnaissance, writing one is
arbitrary capability, and selecting one can move a session onto a preset
that edits the live runtime. `agentPreset.list` deliberately stays ordinary
and now reports `authorable`, so a surface knows whether creating is
possible at all rather than offering a button whose save always fails.
Authoring starts by duplicating: a shipped preset opens read-only because
the deployment's copy is what a broken local one is compared against. Ids
are contained before they become directory names, and the text is parsed
with the loader's own schema, so a save cannot leave a file no session
could load.
Fixes a defect the real-composition test found: a preset written under the
user's home could never mount, because the loader resolves a row against the
composition's own directory and Node's `node_modules` walk from there never
reaches the installed harness. The mount now records the host base and sends
bare specifiers there, leaving relative paths resolving from the preset.
Also closes the coverage the earlier surfaces in this stack shipped without —
the General row, the composer seat, and the plugin halves now have tests.
`agentPreset.list` gives a browser the deployment's roster so it can offer a
choice when starting a session. Each row carries the id, its `trust`, and
whether it is the current default.
`trust` is on the wire deliberately: a `user` preset is exactly as privileged
as the plugins it names, so a surface that offers one alongside a shipped
preset can say which is which rather than presenting both as vetted.
The domain is read-only. A preset is a composition on disk, so authoring one
is a filesystem act rather than an RPC; and a deployment composing no presets
answers with an empty roster rather than an error, because sharing the host
composition is a valid deployment.
The RPC map made every registration site a type error, so the route, the
response-schema table, the service delegate, and the browser fixture are all
wired rather than only the ones I remembered.
Update every doc referencing base.cordis.yml/web.cordis.yml, --config, -p, or
$DSH_HOME/config.yaml to the profile vocabulary with bilingual counterparts
re-recorded; regenerate the catalogs and graphs; add the
profile-plugin-bundles Agent Note recording the design and its rejected
alternatives.