Same required field as the standard preset: master made
`allowParallelInProgress` mandatory on dsh-tool-todo, and a preset that names
the row owns the choice.
master made `allowParallelInProgress` a required field on dsh-tool-todo, so
every composition must choose it. The base patch was updated with the field,
but the Web surface takes `tool-todo` from the mounted preset instead, and
that row carried no config — so the `standard` preset failed to mount and
every session on the Web surface died at setup.
Also make the preset tree's write test reach the override. Tearing the agent
down stops in the loader's own "tree is being disposed" case before any write;
a live row reconfiguring itself is the trigger that actually gets there.
`ui-question`'s node half called `ctx.tools.register` on the host context.
`ScopedLayers.merge()` combines the global layer with the agent's exact-scope
layer, and an unscoped registration lands in the global one — so the tool
reached every agent no matter which preset composed it. `core-web`, sold as a
two-tool benchmark surface, really presented three.
Rendering a question is a host UI capability; having the tool is an agent
capability, and only a preset decides that. The node half is now empty and the
`tool-ask-user` row moved into the preset that wants it. The TUI keeps its own
row, having no presets.
The composition tests now assert the global tool layer is EMPTY, which is the
invariant that would have caught this: any tool outside a preset reaches every
agent. The browser lane's composition, seeded-history, and hermetic-skill
assertions address their registries through a composed agent for the same
reason — those services are per session now, and the host cannot resolve an
`isolate` realm by name.
Moving the agent plane behind per-session presets took five rows with it that
the host still owns, and the Web surface stopped booting: `host-apiproxy`
injects `subagents`, so with the registry disabled here the entry never
activated and `dsh web` died at plugin-tree load.
The criterion is injection, not subject matter. A host row that injects a
service resolves it before any session exists, so there is no agent to key by:
`bash-env` (which `apps/cli/src/web.ts` injects to publish `DSH_WEB_URL`), the
`subagents` registry and its spawn/fork backends (a process singleton whose
cross-session queries the api-proxy serves, and whose provider names are
globally unique), and `tool-subagent-report` (a continuable setup on that
singleton, registered once per live session by a list that is not scope-aware)
all stay host-plane. What a preset chooses is which delegation TOOLS it sees.
The browser lane needs the second half: skill roots now resolve inside a preset,
a subtree the lane's include patches cannot reach, so the row's documented
environment fallback is pinned for the whole scaffold lifetime — presets mount
when a session is created, not at boot. Without it a developer's real
~/.dsh/skills enters replay requests and goldens while CI sees none.
The browser e2e lane had been failing wholesale since this stack moved the
agent plane into presets, and nothing caught it: 34 of 48 files. Two of the
causes are product defects, not test breakage.
`bashEnv` goes back to the host plane. `apps/cli/src/web.ts` injects it to
publish `DSH_WEB_URL`/`DSH_WEB_MODE`, so the earlier note that "nothing outside
the agent plane injects bashEnv" was simply wrong — behind a preset's `shell`
realm those variables reached no shell at all, and a `dsh web` agent could not
find the address of its own interface. This is the same criterion that returned
`subagents`: a host row that injects a service resolves before any session
exists and has no agent to key by, so the service is host-plane. `tool-bash`
consumes the host registry from inside the preset, which works because an
agent context chains to the host; only the reverse is invisible.
`tool-subagent-report` goes back with it. It is not a tool this agent calls: it
registers a continuable SETUP on the host `subagents` singleton, and that list
is not scope-aware. One copy per mounted preset meant every child was handed
`report` once per live session, so the second registration threw and a cold
subagent resume failed with `subagent-not-resumable` — a diagnostic three
layers removed from the cause.
The lane's own composition facts follow. Skill roots resolve inside a preset
now, a subtree include patches cannot reach, so the scaffold pins the roots'
documented environment fallback for its whole lifetime rather than for the boot
— presets mount per session. Without it the developer's real `~/.dsh/skills`
enters replay requests and goldens while CI sees none. The `apps/cli`
composition test pins `storage-json` for the same reason: unpinned it wrote,
and then read back, the developer's own `~/.dsh/storages/`.
Three tests now address through an agent what they used to read off the root
context, because that is where the thing lives: the tool catalog, the skill
registry, and the token meter. The seeded-history projection baseline asserts
the opposite of what it did — a detached session yields a preset-plane
projection only from a durable checkpoint written while it was live, and this
seed was written straight to persistence and never ran.
Goldens re-recorded for the hero's preset chip and the settings nav entry.
The picker listed presets alphabetically by id, so the shipped set read
cordis, minimal, standard — reverse order of capability. A preset may now
declare `order` in its metadata; the shipped three declare 1/2/3 and read
standard, minimal, cordis. A preset that declares none sorts behind those
that do, then by id, so authored presets stay stable.
Editing had nowhere good to live. Inside a card it was squeezed into a
~268px column; hanging off the end of the grid it was orphaned from the card
it edits. It now replaces the list: a back link, what is being edited, and
the form at full width. One thing on screen at a time, which is what the
form's height wanted all along.
Cards in different grid rows sized independently, so a short description made
a short card. `grid-auto-rows: 1fr` makes every row the same height.
The trust badge lost its pill when the card CSS was rewritten, and `In use`
never had one; both are tags now. Icon labels moved from `title` to a drawn
tooltip — the native one waits about a second, which reads as nothing
happening.
`core-web` said neither of the things that matter about it. The `-web`
suffix is a leftover from the whole-process `core-web.cordis.yml` overlay,
and presets are per-session and not web-specific. `core` reads as "the
foundational one" when it is in fact the one with the fewest capabilities.
`minimal` says what it is and orders the shipped set legibly by capability:
minimal, standard, cordis.
Breaking: a session created under `core-web` records that id in its header
and will fail to resolve it on resume. Nothing outside this repository has
shipped, so no migration path is offered.
The identically named `config/core-web.cordis.yml` — the legacy
whole-process overlay behind the web snapshot test — is a different thing
and keeps its name.
A picker showed directory names, so the settings page could only ever list
`standard` / `core-web` / `cordis` and hope the reader knew what they meant.
A preset may now publish display text in an optional `preset.yml` beside
its composition, and the section renders cards — name, description, and the
one in use — instead of rows.
The file carries display text ONLY. `id` is the directory name and `trust`
comes from the root a preset was discovered under, so neither is writable
there: otherwise a locally authored preset could name itself into the
shipped set. It is a separate file because a composition is a top-level list
of plugin rows — YAML cannot carry sibling keys beside it, and a fake
metadata row would hand the Loader something to load.
Every read failure degrades to no metadata; absent, malformed, wrongly
typed, and blank all mean the same thing and the picker falls back to the
id. Presentation is not capability: a preset whose name is broken still
mounts.
The editor gained name and description fields above the YAML, and clearing
both removes the file rather than storing a blank name.
The comment claimed `ui-question`'s host half owns the `ask_user_question`
registration. That was true and is the exact mechanism that leaked the tool
past every preset; with the node half emptied, disabling the row is a UI
decision rather than a capability one.
`ui-question`'s node half called `ctx.tools.register` on the host context.
`ScopedLayers.merge()` combines the global layer with the agent's exact-scope
layer, and an unscoped registration lands in the global one — so the tool
reached every agent no matter which preset composed it. `core-web`, sold as
a two-tool benchmark surface, really presented three, and a locally authored
`bash-only` preset presented two.
Rendering a question is a host UI capability; having the tool is an agent
capability, and only a preset decides that. The node half is now empty and
the `tool-ask-user` row moved into the presets that want it. The TUI keeps
its own row, having no presets.
The web composition test now asserts the global tool layer is EMPTY, which
is the invariant that would have caught this: any tool outside a preset
reaches every agent.
`dsh web` failed to boot: the aggressive plane split moved `subagents` and
its spawn/fork backends into the preset's entry-local realm, but
`dsh-host-apiproxy` is a host row that injects `subagents` to answer the
browser's cross-session queries, so it waited forever for a service only
sessions now provided. A per-session copy is wrong twice over — a provider
name registers once, so the second session would have collided anyway.
The registry and its backends go back to the host composition; the presets
keep the delegation TOOLS, which resolve that host registry. `workflows`
stays entry-local: nothing outside an agent reads it.
The web real-composition test could not have caught this, because it
disabled `api-gateway` — the very row whose pending injection names the
break. It now boots with the api-proxy enabled and the browse directory
picker substituted, so the boot audit covers the whole host-plane injection
graph. Re-introducing the old split makes it fail with the three pending
entries, which is how this was verified.
A third built-in preset: the standard coding agent plus the self-referential
Cordis toolset, a persona that explains the two-plane split, and a skill
teaching composition authoring. It exists so a person can ask an agent to
author another agent.
The skill ships INSIDE the preset directory rather than in the user's skill
root, and the root is derived from the preset's own `baseUrl` — the loader
evaluates `!!js` with `with (ctx)`, so a composition can locate itself. A
preset is the unit that gets copied and edited, so its documentation should
travel with it.
The skill leads with the rule that actually bites: a row publishing a service
may not sit loose in a preset, whether a row publishes one is not visible from
its name (`tool-bash` provides `bashEnv`), and a consumer left outside its
provider's isolate group resolves the host registry and then contributes
nothing — the quietest failure this design has.
Writing the test surfaced a consequence worth stating: an entry-local realm
makes the service invisible to the agent's own scope too, not just to the host.
Only rows inside that group resolve it, which is precisely what makes
`tool-skill` this agent's own rather than a shared one. The test asserts what
is actually observable from outside instead of reaching for the isolated
service.
TRUST: `cordis_mount` evaluates model-written JavaScript against the live
runtime, and a composition this agent writes becomes a preset other sessions
mount. Both the preset header and the toolset's own documentation say to treat
this as shell access. The tools stay opt-in per session — a test pins that they
are absent from every other preset.
Two consequences of moving the agent plane behind presets, both invisible
until the host plane stopped carrying model-facing rows.
`sessions.fork` built its child with a bare `installTarget` and a `meta`
without `agentPreset`. That was harmless while every tool sat in the host
plane — the child inherited them for free. It now comes up with an EMPTY
tool set. The child composes the parent's preset instead, for the same
reason a resumed session keeps its own: the seeded history was produced
under those tools.
`bashEnv` lives in its own `dsh-bash-env` row rather than inside
`tool-bash`, so a preset that isolates the realm must compose the provider
beside its consumer; the host row is disabled here like every other
model-facing one. Nothing outside the agent plane injects `bashEnv`, so it
stays per-session.
`session.create` takes an optional `agentPreset`, and the host resolves it,
mounts it during pre-publication setup, and records the resolved id on the
session header so a later resume rebuilds the same agent.
Resolution happens BEFORE the session exists, not inside setup: the session
boundary snapshots `meta` before asynchronous setup begins, so an id discovered
during setup could never reach the header. Mounting still happens in setup,
where a failure rolls the whole creation back rather than publishing a session
whose capabilities are half-installed.
Resume ignores whatever the request names and rebuilds from the stored id. A
resumed session's history was produced under that composition; restoring a
different one would replay tool calls the model can no longer make.
`dsh-agent-presets` now throws `UnknownPresetError` / `PresetMountError` so
the host can tell a bad request from a broken preset — they become
`agent-preset-not-found` and `agent-preset-invalid`.
Ships the two built-in compositions (`standard`, `core-web`) and the persona
row that lets them differ in identity. Nothing mounts them yet: no roster is
configured, so `composeAgent` finds no service and every session keeps the host
composition. Wiring the roster and moving base's agent-plane rows behind it is
the next commit, so the switch happens atomically with a real-composition test.
dsh --profile <name> replaces the fixed entry modes: --config and -p are
removed, --patch adds overlays over the composed profile, a positional task
selects one-shot mode (requires the headless-runner row), and dsh web stays as
the alias for --profile web carrying the Web flag family as patches. dsh
plugin --profile <name> forwards verbatim to pnpm in the profile directory,
initializes on first use, and reconciles the dsh.plugins layer list after
add/remove (patch-less packages warn and stay plain dependencies). Config
dumps and the keyless web e2e scaffold compose the same bundle layers over the
same empty root as the boot.
session-title already registers a title projection unit; /resume now
reads it instead of scanning logs: live rows from the registry
snapshot, persisted rows from the durable checkpoint row
(cachedSnapshot, zero I/O), and only rows without a usable checkpoint
pay a coldSnapshot — checkpoint plus readFrom tail, written back so the
next scan is metadata-only. Cold reads are bounded by the new
resumeScanConcurrency config; compositions without the cache fall back
to the bounded readTitleSnapshots batch. The TUI overlay mounts the
projection registry, storage, and projection-cache rows over the same
storages root the web surface uses, so checkpoints serve both.
The opt-in `core-web.cordis.yml` profile promises "exactly persistent
`bash` plus `str_replace_editor`" (its header comment and `apps/cli/
README.md`), but the base registration of `tool-subagent-list-agents`
(added with the durable child catalog) was not disabled by the overlay,
so the profile actually exposed `bash`, `str_replace_editor`, and
`list_agents`. The assembled snapshot was updated to accept the third
tool, which ratified the contract break instead of fixing it.
Disable `tool-subagent-list-agents` in the overlay and restore the
snapshot's expected tool registry to the documented two tools.
- Wire the control service and send_message tool into every shipped
composition with a resumable provider and background enabled
(headless-agent, tui-agent, and the SDK helper's subagent feature
base resources); jsonrpc-agent disables background and is unchanged.
- Resolve the send_message availability check in the CALLER's tool
scope so a restriction that removes the follow-up tool from one
agent also blocks that agent's continuable start.
- Control-service disposal now cancels live activations and awaits
producer settlement instead of stranding them: TaskService keeps
producer Tasks across a reload, so the disposing service aborts each
activation-owned controller, resolves its terminal gate (the
effect-scoped onTaskDone listener is already gone), and awaits done.
A new test kills a mid-start activation through HMR disposal.
The shipped-roster change made @deepseek-ai/dsh-tool-session-query a
default row of the shared base.cordis.yml, so the TUI and Web surfaces
put the five session-search tools in front of the model. That
contradicts the recorded opt-in stance for the model-facing session
query consumer; the ACP example remains the mounted reference.
Remove the row from the shared base, the now-dangling disabled patch in
the opt-in core-web profile, and the workspace dependency. The
ctx.sessionQuery index stays: the TUI's /resume and the Web content
search consume it directly. Both shipped-composition tests now pin the
20-tool catalog.