The inventory keeps dsh-app-boot an optional, type-only peer, so the walk
over an entry's owning groups moves behind ProfileRuntime.userDisables(): a
consumer with the runtime asks it, one without reports composition as before.
The module graph and the cordis catalog follow.
The runtime's user-disabled row ids now come from the committed composition —
composeProfileStack collects them from the user layers it composed — so a
rejected or half-written patch file no longer makes the list disagree with the
running tree or fail to parse. The inventory attributes a row disabled through
a group the user disabled to the user, through the shared userDisablesEntry
walk, and lists a user-layer row the composition left out under the new user
trust instead of builtin.
The `cordis.patch.yml` parser and the comment-preserving writer are the
Loader's format, not a mechanical primitive: they build on the include
plugin's dialect and the Loader's row types, which no other `util` package
does. They now live in `app-boot` as `src/patch-file.ts`, exported as
`@deepseek-ai/dsh-app-boot/patch-file` with its own bundle entry, so the
agent-preset roster and the plugin manager load them without the boot entry.
`dsh-patch-file` goes, with its README and catalog entries; `yaml` moves into
app-boot's dependencies.
A dozen plugin packages already depend on `dsh-app-boot`, so the roster
joining them costs nothing; the note records why the separate package was
tried and folded back.
What plugin management does now lives in `dsh-plugin-manager`, a boot-group
package beside `app-boot`: `PluginInstaller` runs pnpm, probes, and judges new
packages over the profile on disk, and `PluginManager` adds every operation
over the booted tree, each refusal a `PluginOperationError` with a `plugins/*`
code and no Remote protocol attached. The manager reads the profile runtime,
the preset roster, and the running-agent count through per-call readers, and
the roster only as `PresetLayers`, so the package depends on neither the
roster nor the agent registry.
`dsh-host-plugin-manager` keeps `pluginManager` and the `plugins` Remote as a
relay: one Remote method per manager method, and one exhaustive switch
turning a failure into the Remote error of the same code (`plugins/bad-request`
into `gateway/bad-request`). Its `./types` re-exports the manager's types, so
`api-remotes` and the client import one vocabulary unchanged.
`dsh plugin add` and `remove` now run the installer instead of a bare pnpm
forwarder: every new package is probed, a package that declares neither a
bundle nor a plugin module is removed again with its reason, and newly
installed bundles are enabled as before. Other pnpm verbs still forward.
`dsh-global-tool-mask` wrapped `tools.restrict()` as a composition row so a
preset's user patch layer could hide a host tool. No shipped preset, the
plugin manager, and the settings UI compose such a row, so the package had a
README, a test, and no consumer. It goes, with its `apps/cli` dependency,
its `tsconfig.host.json` reference, the README example that named it, and the
generated catalogs. `tools.restrict()` stays: subagent tool filters call it.
Hiding a host tool from one preset is a per-preset setting for the
scope-resolved settings of the next layer, not a hand-written row; the Agent
Note records the removal under alternatives considered.
Windows x64 runtime builds resolve their hosted matrix.runner unconditionally
again (windows-2025 for pull-request CI). Remove the DSH_CI_FAILOVER_WINDOWS
selector, job-private Python toolchain, self-hosted dependency install and
post-step cleanup, the private setup script, and the routing spec introduced
in #3629. The Windows failover switch again covers only the native Windows
jobs in ci.yml.
scripts/test-proxy-environment.spec.ts pins the Vitest configs that declare
setupFiles so the proxy isolation setup is never dropped; the benchmark
lane declares it too.
Add a benchmark lane (`vitest.bench.config.ts`, `pnpm run test:bench`,
gate mode `ci-bench`) and a required `node 24 / benchmarks` CI job that
runs it alone. Benchmarks synthesize their input in-process from fixed
parameters and fail on documented budgets:
- `open-generation.bench.ts`: a 200-turn released-v0 log with 500 text
and 125 reasoning deltas per reply (127,400 events, ~2.8 MB) encoded
through the frozen v0 codec; the migrating first `open()` must finish
within 2,000 ms in a child process capped at 128 MB of old space, and a
fresh process must open the published current generation within 500 ms.
- `conversation-fold.bench.client.ts`: 200 replies whose compact streams
hold 2,000 text + 500 reasoning deltas each, folded through every Chat
Definition by the real assembler; the fold must finish within 150 ms
and stay within 3x the fold of the same window with 100 deltas per
reply.
On this commit both gates fail: the migration exhausts the 128 MB heap
(4.8 s and 696 MB peak RSS without the cap; the pre-stack decode of the
same bytes took 34 ms and 168 MB) and the fold scales 11x with the delta
count. The stacked fixes bring both paths to O(records).
The probe's child was a script string that wrote its report to stdout, and
the parent parsed the whole stream as one JSON value: a plugin that merely
printed a line at import could not be probed, and a package that kept a
timer alive after import ran until the timeout. The parent asserted the
parsed value as the report, and the cache read checked only `format`, so a
file holding nothing but `{ "format": 2 }` came back as a complete record.
The child is now its own module, `probe-child.ts`, run through tsx under a
source launch and bundled as `lib/probe-child.js`, and it sends one report
over an IPC channel; stdout is not read at all, and the child is killed as
soon as the report arrived. `parseChildReport` and `parseProbeRecord`
validate the report and the cached record field by field, so an
unrecognized report is a rejection and an unrecognized record is probed
again. `PluginProbe.ok` now states what it is: the main export imported and
cordis is not a second copy; `kind` and `addable[].ok` decide what can be
enabled or added.
- Stop treating three archived records as current authority: bounded-llm
citation now points at the llm-retry README, the semantic-composer
proposal cites the runtime-owned child guard as history it builds on,
and the code-runtime comment states the deferred rationale locally.
- Fix the stale "dsh family stays restricted" claims left without their
rationale citation: the check-workspace-constraints comment, the
release publish comment, and the kept npm-release-sequences note now
record that the family published publicly on 2026-08-13 (87f8e6a728).
- The rejected client-settings-locale-theme body keeps its pristine
proposal prose with only the Status line and the two link paths whose
targets this PR archived; the link remaps are required for
verify-md-links and pairing, and every prose byte is untouched.