`dsh-system-prompt` owns the deployment persona as its own config and registers
that section unconditionally, so a process has exactly one. An agent preset
cannot mount the prompt registry itself, which means that without a row of its
own a preset could change an agent's tools but never its identity — and a
roster of presets that all sound the same is not worth having.
The row is scope-only by construction: mounted outside an agent scope it
collides with the registry's own `deployment:persona` registration and fails
loud. That is the correct shape rather than a gap, because the unscoped slot
already has an owner and this row exists to shadow it for one agent.
Two behaviours are pinned by test because both read the other way at a glance:
an empty persona still occupies the slot (shadowing the deployment persona away
entirely, then disappearing at render), and `assemble()` keeps section text
uninterpolated — `renderPrompt()` is the stage that resolves `{{…}}`.
A composition that shares one `isolate` realm across rows needs a
`cordis:group` row, and a preset living outside this workspace — the
authored ones under the Harness home — cannot resolve
`@cordisjs/plugin-group` by name: Node's upward `node_modules` walk
never reaches the harness from there. Registering it as a loader builtin
beside `cordis:include` loads both through the ambient module pipeline
instead.
Record it in the preset Agent Note, which leans on the realm vocabulary
without saying where the group row comes from, and drop the preset
README's limitation claiming this builtin is unavailable — it described
the state this change ends.
The test's assertion had a vacuous escape: `provide` mints the root
symbol unconditionally, so the `rootKey === undefined` disjunct could
never hold and the comment claiming the root realm never learned the
name was wrong. Pin both halves — the symbol exists, nothing is stored
under it — and clean up the global the fixture writes.
A preset is a directory holding one `agent.cordis.yml`. Mounting it under an
agent's scope context during `setup(agentCtx)` gives that one session its own
tools and prompt sections while every other live session keeps its own.
No registry gains a tier. `dsh-tools` and `dsh-system-prompt` already file
registrations into the calling context's scope layer, and entry contexts chain
to the context a subtree was plugged into, so a composition mounted under
`agent.ctx` is that agent's alone and unwinds with it.
The mount audits itself because a directly-plugged subtree is absent from
`ctx.loader.entries()` and no boot audit covers it. It rejects an unscoped
target, a row that never became usable, and a row that published a service into
the root service realm — that last one is process-global rather than
per-session, and its collision with the next session surfaces as an unhandled
rejection `setup` never observes, leaving a half-composed agent that looks
healthy. The package invariant re-checks that rule on every service
notification, since a row publishing from a timer would escape a one-shot audit.
Raises the `packages/README.md` word ceiling from 920 to 980: the group table
must enumerate every group, and the new `preset/` row is necessary content.
Design: .agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.md
Port the two master-side markdown fixes onto the mdast renderer:
cjkFriendlyStrong joins both parse.ts grammars as a micromark extension
(ex remark plugin), and inline-code HTTP(S) URLs gain the safe anchor in
render.tsx without normalizeUri. Re-record the two new web-e2e goldens
whose footer clock spacing diverged from the nine established goldens,
and drop the timing-flaky LLM duration segment one of them captured.
- Clone patch lists per generation (boot + composeLive): the include pushes
insert rows by reference and mutates them in place, so a reused object
baked user overrides into bundle rows and removal could not revert; the
built-bin hot-reload e2e now asserts an override AND its removal reverting.
- The headless runner awaits Loader settlement before prompting (its inject
gate covers only apiProxy/httpServer) and abandons cleanly when the tree
died during the wait.
- healProfilesModuleFallback walks the app's full dependency+peer closure:
out-of-tree plugins import seam packages (dsh-compact, dsh-subprocess, ...)
that only implementations reach, and peers are how seams are declared.
- Profile init writes pnpm-workspace.yaml (nodeLinker: hoisted), not .npmrc
— pnpm >=10 reads settings from the workspace manifest.
- Web dumps reject boot-only flags instead of printing a tree that differs
from the same invocation's boot; --port validates at the flag;
--dump-default-config no longer parses the (possibly broken) user layer;
trustedHosts flag derivation merges over the composed value instead of
replacing it; web-runtime gains surfaceContext (headless disables the GUI
prompt/bash-vars the old -p never mounted); 'node_modules' is a reserved
profile name; plugin-warning names the recovery step; client AGENTS.md
registration surfaces point at the web-app bundle.
- Ship session-reference/tmux-context/tool-ask-user as app dependencies for
terminal front-door patch layers (turtle-ui), same stance as mcp-client.
Profile bundles are npm packages declaring dsh.patch in their manifest:
dsh-base carries the former base.cordis.yml rows as one insert over the empty
profile root; dsh-web-app carries the web overlay plus a runtime glue plugin
owning what used to be launcher code (frontend dist resolution via
frontend-static, the web-surface prompt section, bash runtime variables, the
readiness-gated URL line); dsh-headless carries the one-shot runner driving a
task turn through the in-process API carrier under the launcher-provided
ctx.headlessIo seam.
The per-file 100% thresholds name only the failing file. A custom istanbul
reporter now prints one clickable path:line:col record per uncovered
statement, branch path, and function, right above the threshold errors, in
both the CI coverage lane and local test:coverage runs (they share this
config). CJS because istanbul-reports loads custom reporters with a bare
require() outside the tsx/ESM pipeline.
The contextBreakdown and contextPressure units carried the full priced
surface, so each session's persisted projection checkpoint grew without
bound. A surface replacement is now priced by the shadow-price event
logged directly before it — compact/summary for compaction, the new
compact/prune from tool-result pruning (priced through the injected
token meter) — and the unit states shrink to a fixed handful of numbers.
Regenerate the persistence/cordis/module/config catalogs.
The pwsh-terminal e2e probed the bare 'pwsh' literal, which knip flagged as
an unlisted binary. Reuse resolvePwshPath from @deepseek-ai/dsh-pwsh-local
(the judgment the tool-pwsh tests already use), so the probe matches the
executor's own resolution and the reference stays indirect; record mode
skips the lane anyway, so the probe stays inert there. Regenerate the
cordis and config catalogs the seam moves left stale (line-number shifts).
The modern IFileOpenDialog becomes the primary win32 tier: a koffi-driven
COM conversation on a worker_threads worker (the modal Show never blocks
the host event loop), per-monitor-v2 DPI via SetThreadDpiAwarenessContext,
and abort service by re-posting WM_CLOSE to the dialog thread's windows,
with terminate+unref as the last resort (Node cannot interrupt a thread
blocked in native code, and such a worker must never hold the process open).
The PowerShell chain stays as the fallback tier with its trigger widened
from ENOENT to any pwsh failure, closing the review-flagged PowerShell 6
regression (no WinForms: exit 1, not ENOENT, so 5.1 never ran).
Layering keeps per-file coverage honest on every host: pure sequencing and
the driver test against fakes anywhere; the bindings run against a mocked
koffi COM world (the session-persistence-jsonl technique); POSIX hosts
drive the real spawn plumbing to its koffi-load rejection; win32 hosts run
a real open-and-abort-close smoke. The smoke joins processBoundTests: a
worker blocked in a native modal wedges the threads pool's teardown, while
a fork contains it. The worker bundles as its own CJS tsdown entry
(workflow-workerthread's pattern; no TLA), and the host module is imported
statically so the node-half bundle stays chunk-free.
Built-plane and real-COM behavior verified on native Windows: standalone
probes for the source worker, the built CJS worker, and the driver's abort
path all open and close the real dialog.
Agent Notes: new implemented/feature/2026-08-02-win32-in-process-folder-dialog
(bilingual) owns the decision; the DPI note is re-scoped to the fallback tier
it now describes and its AutoUpgradeEnabled attribution corrected (.NET Core
3.0 rewrote FolderBrowserDialog; the opt-out arrived in .NET 6).
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.