Review found two defects in the previous commit's ordering fix.
@pku-xht: `historyStateFor` copied the attached session's events, the handler
then awaited `presenterScopeFor`, and only then read the projection baseline
off the still-live Session. An append during that await served events cut at N
beside a baseline folded to N+1 — one response describing two moments. The
same restructure had also moved the baseline read outside the `try`, so a
failing snapshot escaped the structured `internal` error.
Both awaits now happen before the cut: `historySourceFor` resolves which
session serves the read, `presenterScopeFor` ensures the recorded composition,
and `historyCutOf` then reads events and baseline adjacently with nothing
between them. The whole sequence is back inside the try.
The invariant judged any scoped assembly with a chain of one as an unjoined
agent, which rejects a legitimate assembly in a standing preset key (that key
has no parent of its own). It now gates on `context.agent` — a scope-only read
is not an agent and is out of range by construction rather than by a premise
about who else calls `assemble` — and asks the roster's own
`composedPreset()` instead of introspecting chain length. The advisory warning
uses the same relation.
Also from review: the `2026-08-05-per-agent-tool-presentation` note still
described `presentAs` as per-agent, which standing mounts made false and this
branch's own rewording contradicts; the duplicated "process-wide unit table"
argument collapses to the Agent Note with pointers from the five copies; a
dead `.sort()` before `arrayContaining`; and change-history narration in the
tool-cordis README.
The pre-commit staged-pairing hook was bypassed for this merge: master's
`docs(notes): archive superseded dsh run decision` (7ee9e16001) arrives as a
rename into `.agents/notes/archived/`, and the hook hands that path to
`verify-translation-pairing`, which correctly refuses an archived note as
out-of-corpus. The full-corpus gate passes (851 pairs).
`minimal` was rewritten upstream into a fixed-prompt PTY surface that now
carries its own compaction group, so this branch's token-meter move applies to
it too: the row leaves the group and `tokenMeter` leaves the realm, exactly as
in the other three presets. Master's header prose is kept whole — the note this
branch had added there described a composition that no longer exists.
`docs/event-producer-consumer.*` is generated; regenerated after the merge.
Replaces the patch round trip. An app's entrypoint resolves the command
line into a service, and the rows it configures read that service from
their own config — port: !!js ctx.get('webStartup')?.port ?? 3080 — so the
resolved value beats the value written beside it and nothing is written
back into a row or handed to the launcher.
A bundle names the entrypoint row in its manifest (dsh.bundle.entrypoint),
which is what lets the boot mount in two passes: entrypoints alone, then
the whole composition. That ordering is required, not cosmetic — a row's
config expressions are evaluated when the include applies the row, and a
strict ctx.get only answers for a service whose providing fiber is already
active.
What this removes: ctx.appPatches and the launcher-owned patch layer, the
disable/re-enable recycle and its in-flight-mount barrier, overrideConfig,
and the reload hazard they existed for. A live config edit now re-applies
the second pass against services that are still up, so a served port
survives by construction.
What it adds: ctx.appReady, because Loader settlement no longer means the
app is up — a row mounted in the second pass can observe a settled tree
while that pass is still running, or already rolling back. The web URL line
waits for it, so a boot that fails in the second pass announces nothing.
dsh-web-app owns --host/--port/--dev/--workspace-root/--trusted-host and
its --help in a web-startup row; the rows it configures wait for the
webStartup service, and the client-plugin HMR receiver now ships disabled
so --dev is a row toggle rather than a runtime insert (the Loader cannot
resolve a row inserted from inside a mounting plugin).
dsh-headless owns the task positional and rejects a missing task as its own
usage error. Its runner ships disabled, not merely waiting: the schema
requires the task, and a row's config is validated when its fiber is
created, before the startup row can supply one. A composition has exactly
one command-line owner, so the patch disables the web startup row and this
one provides webStartup too, leaving the web rows on their composed
one-shot values.
The keyless web scaffold provides the same three values with no arguments,
which is what an embedding host with no command line does.
Moving the agent plane behind presets left two readers on the wrong side of
the host/agent line.
`dsh-token-meter` was disabled on the host and mounted inside each preset's
`compaction` realm, but its three projection units register into the
process-wide `sessionProjections` table. A unit registered from one preset
answers for every session, so whether a `minimal` session showed a context
meter depended on whether some other session had mounted `standard` since
boot, and a process that only ever ran `minimal` showed none. The meter takes
no configuration, keys every fold by Session, and registers no tool or prompt
section, so it returns to the host composition and leaves the presets'
`isolate` map; the realm and `compact-basic` stay, because what a preset
chooses is whether its agent compacts, not whether its tokens are counted.
Nothing named an agent that joined no preset. The join is a scope-parent link,
and without it the tools, prompt-section, and skill views resolve the empty
global layer: the agent publishes, the turn runs, and the model receives
nothing. `AgentPresets` now logs one warning per such agent while a roster is
configured, and the invariant companion fails outright — at
`system-prompt/assemble` rather than at publication, because an unjoined agent
is legal until it addresses a model and `recompose` binds exactly such an
agent. The warning stays advisory: a synchronous `agent/created` throw vetoes
publication, and the ACP bridge, SDK server, and headless bundle all create an
unjoined agent today.
Three limits are recorded rather than fixed: projection key presence is not a
per-session capability signal, a superseded standing generation is never
reclaimed, and a `cordis_mount` temporary plugin belongs to the composition
rather than the session that mounted it.
Fixes#2203
`tool-bash` resolves the background-task registry with `ctx.get('tasks')`,
and it sits at the preset's top level. The registry sat inside an
entry-local `isolate: { tasks: true }` realm, which is invisible to every
sibling row outside it, while the Web surface disabled the host row — so
both lookups missed and every `run_in_background` call answered
"background tasks unavailable" with `task_output`, `task_list`, and
`task_kill` still listed in the catalog. `task_list` returning
"(no background tasks)" is what made the outage read as an empty queue
rather than a severed producer.
That is the `goals` criterion read from inside the preset: a Service a row
outside its realm READS belongs to the plane both can see. `tasks` already
keys access by owning agent (`assertAccess` compares `task.owner.id`) and
mints an independent token per `attachSurface` call, so one host instance
serves every session exactly as before presets — the per-preset-standing-mounts
note records that sharing `tasks-local` is a return to its design.
`minimal` mounts no `tool-tasks`, and the `start()` control-surface gate is
a service-wide set that another preset's controls would open for it, so its
`tool-bash` disables `run_in_background` and drops the parameter from the
schema.
Fixes#2141
The skills capability moves back to the host plane as one singleton, layered
with the ScopedLayers infrastructure the tools registry established: a
registration files into its calling context's layer (repository plugins and
host rows land global, a preset's skill-local lands in that preset's layer),
and a read merges the global layer with the viewing scope's chain, the
nearest layer winning duplicate names outright while rank keeps deciding
duplicates within one layer.
The web-app bundle re-enables the base skill registry row (skill-local and
tool-skill stay preset-owned), the standard preset drops its isolate skills
realm for bare rows over the host registry, tool-skill passes the calling
agent as the view scope, and the gateway's skills domain reads the host
registry in the presenter scope — a cold session now resolves its recorded
preset's standing key instead of failing.
Delete the three §-citation residuals (web-app cordis.patch.yml, two
client design.md §-headers); recast the two CSS used-to narrations and
hmr's four v1 labels as current-state prose; narrow the zh exemption
example to 'this cut' (「本版本」 legitimately renders 'This version');
extend the candidate gate list with the post-battery shapes (§\d with a
committed-owner carve-out, 'used to', bare v1) on both sides and
re-record; honor the over-target ceiling freeze — docs/AGENTS.md
condensed to 1320 and the ceiling restored to 1320.
The Gateway serves the goal domain as Remote endpoints, and a Remote method
picks its receiver Service from a generated descriptor — `clear(agent, ref)`
takes its agent as a PARAMETER, so the invocation is direct and the receiver
resolves on the host. Behind the preset's entry-local realm there was nothing
to resolve, and every browser goal call answered `service-unavailable`: the
composer's Clear goal button left the bar on screen.
That is the `bash-env` criterion read from the other side. Injection is not the
only host relationship a Service can have; being READ from a host row is one
too. The registry is keyed by session, so one host instance serves every
session exactly as it did before presets. The preset keeps the model-facing
tool, which is the choice a preset is for.
Also reverts the cold-transcript presenter resolve: resuming an agent to reach
its presenters made the context meter drop its cache and token counts on every
cold-opened session, trading one silent degradation for another. The card
degradation it addressed is diagnosed and still open.
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
master extracted this layer's inline cold-resume resolver into
@deepseek-ai/dsh-api-remotes, whose `setup` was a fixed AgentSetup. A resumed
session composes the preset ITS header recorded, so the option becomes a
function of that header; the resolver builds the setup before the published
re-checks so those stay adjacent to `resume`.
Conflicts:
docs/cordis-catalog/services.md
docs/module-graph.md
packages/host/apiproxy/package.json
packages/host/apiproxy/src/api-proxy.ts
pnpm-lock.yaml
Adapter schemas now carry only credential references, but the Models join, onboarding readiness, shipped overlays, SDK scaffolding, fixtures, and active decision prose still treated a redacted literal apiKey as a supported compatibility state. That residue made an unsupported field look contractual and pinned Schemastery silent-dropping as behavior.
Delete those branches and examples, and let compositions and scaffolds use adapter-owned reference and environment resolution. Do not add a tombstone validator or change generic unknown-key behavior: literal adapter credentials have no migration contract to preserve.
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.
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.
One settings row picking which preset new sessions are composed from.
It is deliberately a new-session preference, not a live switch: a session's
preset is fixed at creation and the host refuses to adopt an existing session
under a different one, so the row says "applies to sessions you start from now
on" rather than implying it can retune a running agent.
Options and the current value come from one `agentPreset.list` call — the
roster already reports which id an unspecified session gets, so the row needs
no settings-schema introspection, unlike the permission row it is modelled on.
The write targets only the namespace's `default` field.
The menu marks `user` rows: a locally authored preset is exactly as privileged
as the plugins it names, and presenting it identically to a shipped one would
hide that.
An empty roster reads as `unavailable` and renders nothing, because composing
no presets is a valid deployment rather than a failure — distinct from a
roster call that failed, which surfaces its message.
None of these change behavior; each said something that was not true.
`SessionCwdConflict`'s doc block had been left stranded above the
`AgentPresetConflict` inserted under it, so one class carried a comment
about the other and the second carried none.
The roster comment named a `.system` directory that does not exist; the
shipped root is `config/agent-presets/`, and `system` is the trust its
entries carry.
The real-composition test attributed the disabled `api-gateway` row to
"side effects outside this process" alongside the port and the exporter.
It is disabled for a different reason — the api-proxy cannot mount in
this layer at all — and hiding that behind the same phrase would leave a
later layer unable to tell whether the line can come out.
One test claimed to refuse an adoption while asserting only that the
header records the preset; it now says what it checks.
`PERSONA_SECTION`/`PERSONA_ORDER` existed twice, once in the registry
that declares the slot and once restated in the row that replaces it —
a drift that would land a preset's persona beside the deployment's
instead of shadowing it. The registry exports them now.
The preset conflict message read "already runs agent preset undefined"
for a session that records none, which is the shape a deployment with no
roster produces; it names that case instead, with the regression that
reaches it through the gateway.
Finally, `PresetTree.write()` drops the `loader/config-update` the
inherited method emits — recorded on the override, since a future
edit-while-running flow needs its own persistence path.
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.
The Web overlay disables base's 32 agent-plane rows and mounts the preset
roster instead, so each session composes its own tools and prompt rather than
sharing one process-wide set. The TUI keeps base unchanged: it is single-session
and composing its agent process-wide is correct there.
`roots` is patched in by AppCLIEntry, like `distIndex`: the shipped presets sit
beside the composition that names them and the user's live under the Harness
home, neither of which a config author chooses.
A session's preset is fixed at creation. Naming a different one for an existing
identity is `agent-preset-conflict` rather than a switch, because that
session's history was produced under the first preset's tools. The guard sits
after `await creation`, beside the cwd check, so it covers every path that
yields a live agent — freshly created, adopted live, resumed, or recovered by
the concurrent-creation catch. A request naming no preset adopts the session as
it is, keeping reconnect and retry ordinary.
Two bugs the real-composition test caught, both invisible to unit tests:
`PresetTree` now refuses to write. The Loader persists a tree whose plugin
self-disposed, and tearing an agent down disposes its whole subtree — inherited,
that rewrote the shipped composition, truncating a 241-line preset to `[]` the
first time a session ended.
`dsh-tool-skill` compared against a lookup of its own name in the global layer,
so it threw inside any preset: `register()` files into the calling context's
scope. It now compares against the definition it registered, which is what the
identity check meant all along.
The `standard` catalog is asserted exactly, not spot-checked: a row that
registers into the wrong layer mounts cleanly and simply contributes nothing, so
an omission is this design's quietest failure. It matches the shipped TUI
catalog plus `glob`/`grep`, the pair that composition documents as
ripgrep-dependent.
Re-records `cordis-inspect-jsdoc`, whose rendered `SessionHeader` gains the
`agentPreset` field. `fs-glob-sampling` fails identically on pristine master
and is untouched here.
The browser e2e scaffold gains the roster fact AppCLIEntry supplies. `roots` is
resolved and patched in by the CLI entry, like `distIndex` on the webserver row,
and this lane boots the shipped tree without that entry — so it has to supply
the same fact or the roster resolves nothing and every session in the lane
composes an agent with no tools, no persona, and no token meter. Only the
shipped root: a developer's own `~/.dsh/.agent-presets` must not decide a golden. The
`cordis:group` builtin comes with it, exactly as `boot()` registers it, because
a preset resolving package names from its own directory cannot reach
`@cordisjs/plugin-group` by name.
The lane stays red through this layer and the next four for the reason stated
above — the api-proxy injects `subagents`, `workspace`, and `tools`, so
`api-gateway` cannot activate and the browser has no `/api` at all. It goes
green again in the layer that returns those registries to the host plane; this
change is what makes that layer's fix sufficient rather than partial.
- 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