- Worker consumes PORTABLE_RESERVED_WORDS / RESERVED_ERROR_MEMBERS by
their seam names directly, dropping the local re-alias (symmetry with
the other two imported constants).
- Split the reserved-vs-duplicate diagnostics: a backend-owned global now
reports "reserved binding global", not the misleading "duplicate".
- DUNDER_MEMBER uses `__.+__` so a bare `__` (empty middle, not a real
CPython dunder) is not matched; add coverage.
- Worker misuse tests add `a$b` (second-char `$`) and `lambda` (Python
keyword) so the identifier narrowing and reserved-word adoption are
each pinned directly, not only transitively.
- Clarify the seam JSDoc (dunder-vs-explicit-set wording, Python backend
is a later stack PR) and record in the Agent Note the obligation to
widen RESERVED_BINDING_GLOBALS when the bootstrap seeds more globals.
Update the type-equiv blocks in docs/core-data-structures/code-runtime
(both languages, re-recorded) to mirror the new CodeBindingNamespace /
CodeBindingErrorClass JSDoc, and regenerate the cordis services catalog
for the shifted source line. Keeps the worker-only intro until the
Python backend package exists.
Move the reserved-word, reserved-global, reserved-error-member, and
dunder exclusion sets from the worker backend up to the code-runtime
seam package, and narrow the portable identifier subset to drop the
JS-only `$`. Every backend now imports one contract so a binding
namespace list valid on one backend is valid on all.
Delivers only the seam extension and the worker's adoption; the Python
backend, py-types renderer, and Code Mode language dispatch are later
PRs in the stack that depend on these exports.
Copying is already its own action, offered on the row being copied, so the two
routes now arrive at the same editor by whichever one the author chose. **New
preset** starting from some preset nobody named put a composition in the editor
that the author had to recognise as unwanted before deleting it — and the
preset it copied was the deployment default, which is the one least likely to
be what someone reaching for "new" wanted.
A blank draft has no source, so it names none and makes no read: the editor's
"copied from" line and header clause appear only when there is a source.
- subagents.history computes its projections best-effort on both arms
(a hostile unit's fold rejection serves the page without the block,
matching the session-list precedent) with dual-arm coverage
- the lifecycle-witness spec probes every field of the seven-key
witness, protecting the key list itself
- list-children's own module docs catch up with the seq-gate contract,
and the design note records the rung-two later-event window as an
accepted, self-healing deviation of the corruption class
Authoring writes a FILE, not a settings field, so nothing on the wire announces
it — `settings/changed` covers the default moving, never the directory. The
chip therefore kept the roster it read when it first mounted, and a preset
authored to be used was missing from the one screen that starts sessions.
The page that changes the directory now says so, and every surface reading the
same roster re-reads it. The chip subscribes rather than being reached from the
outer scope, because it is registered later, under `conversation`/`sessions`.
The General row and the preset page already re-read on `settings/changed`, but
the hero chip did not — so changing the default and starting a session without
reloading composed the previous default. That is the one session the setting
claims to govern ("对此后新建的会话生效"), and the chip is what decides it.
A staged pick still wins: `load()` prefers the stage over the refreshed
fallback, so a refresh never overwrites a choice the user just made.
The preference row rendered `option.id` while every other surface — the
new-session chip, the session header label, the preset cards — renders the
metadata name. So the same roster read `标准模式` on one screen and `standard`
on the next, and the id is addressing, not a label. A preset that names itself
nothing still falls back to its id, which is then all there is to say about it.
The row's Chinese copy also still ended on the English word: the section is
`Agent 预设`, so the sentence about a running session keeping its composition
says 预设 too.
`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.
The bundle split emptied `apps/cli`'s plugin dependencies, and the flat module
fallback links only that manifest's closure — so a preset row naming
`@deepseek-ai/dsh-persona` resolved to nothing and every preset mount failed,
leaving each session with an agent that had no tools, persona, or token meter.
Which packages the shipped presets compose is not implied by any bundle: the
presets live beside this manifest, so this is where their closure is declared.
The hermetic skill test now addresses the registry through the composed agent,
the only shape that can see a preset's `isolate` realm.
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 Windows layer previously kept the approval service with policy 'never',
which made the approval plugin inject 'Approval prompts are disabled in
this session: actions that require approval are rejected automatically'
into the model context. On Windows nothing asks for approval — the
escalation surfaces (sandbox_permissions) do not exist — so the sentence
described a rejection surface that is not there. The layer now disables
the approval row too: the service is absent, the model is never told
approval exists, and the danger-full-access degradation is complete.
base.spec.ts pins approval among the Windows disables; the Agent Note and
bundle README record the absent service.
The Windows platform layer previously kept fs path-rule confinement
(sandbox-policy + fs-sandbox) next to the unconfined pwsh shell. Windows
has no OS sandbox runner (landlock/bwrap/seatbelt are POSIX-only), so the
shell can bypass fs-only path rules with one command — the policy was
theater. The layer now removes the whole sandbox stack (sandbox,
sandbox-policy, fs-sandbox disabled), mounts the unconfined dsh-fs-local,
and degrades to danger-full-access: permission/ui-permission leave the
roster and the approval policy is 'never'.
dsh-base declares dsh-fs-local so the profile module fallback links it for
cold starts; base.spec.ts pins the shipped Windows roster (disables,
inserts, approval policy); the Agent Note records the rejected fs-only
confinement alternative.
The shipped host composition moved: `base.cordis.yml` and `web.cordis.yml`
are the dsh-base and dsh-web-app patch layers now. The gate still opened the
old paths and crashed on ENOENT — a gate that cannot read its inputs proves
nothing, loudly or otherwise.
Retargeting it also surfaced what the move implies for ownership: the web
bundle carries the roster and its browser plugin rows now, so the bundle's own
manifest is what must declare them. The gate's existing bare-plugin check said
so as soon as it could parse the file again.
The shipped surface stopped being two yml files: `base.cordis.yml` and
`web.cordis.yml` are bundle patch layers now, applied over an empty preset
root. This test still opened the old paths, so it failed before asserting
anything. It composes the same two layers the profile boot composes, over the
same empty root, and heals the flat module fallback the way the boot does —
the root lives outside this workspace, so bare plugin names have no other way
to resolve.
The web bundle's runtime row is disabled beside the webserver: it injects
`httpServer`, so a disabled port leaves it pending forever. It owns dist
serving and the URL prompt line, neither of which decides an agent's
capabilities.
`apps/cli` declares the packages the shipped agent presets name again. The
bundle split emptied its plugin dependencies, and the flat fallback links only
the app's dependency closure — so a preset row naming `dsh-persona` resolved
to nothing, and every preset mount failed. Which packages the shipped
presets compose is not implied by any bundle: the presets live beside this
app's config, so this app is what has to declare them.
The README and the note each restated which presets ship. `code` was added a
layer later and neither followed, so both said three where the directory holds
four — the drift the review predicted, arriving on schedule. They point at
`apps/cli/config/agent-presets/` now: one directory per preset, and the
listing is the answer. The real-composition test still pins the exact set,
which is where a roster change should be felt.
`code` still carried `bash-env` behind its own `isolate` realm and its own
`tool-subagent-report` row — the two the other three presets had already given
back to the host. It was added a layer above the fix, so the rebase carried it
forward untouched, and the shipped deployment ran a preset whose sessions get
no `DSH_WEB_URL` in their shell and hand every subagent a second `report`
registration on the host registry.
Nothing caught it. A tool-catalog assertion cannot: neither row contributes a
tool. The web lane cannot: no scenario composes `code` beside another preset,
which is when the second `report` throws. The presets are near-copies of one
another, so "fixed in three of four" is the shape this failure takes, and it
will take it again.
So the invariant is checked rather than described. `verify-cordis-config` now
rejects any shipped preset row that is also active on the host plane, which is
the property both defects violated: a row active on both planes is mounted once
per process and once per session, and what that costs depends on the row — a
provider behind an `isolate` realm shadows the host's for its own consumers, so
a host contributor reaches nobody; a row registering into a host singleton
registers once per live session, so the second collides.
Code Mode was a deployment-wide field on the host `tools` row: a
deployment ran every session that way or none. The obvious product
shape — 代码模式 beside 标准/极简/创造 in the preset picker — had
nothing to hang on.
The registry itself cannot move into a preset; the agent loop's
scheduler, the api-proxy's presenters, and every tool plugin are its
consumers. So split the registry from its projection: `presentAs(mode)`
writes one cell on the calling agent's scope layer, exactly as
`restrict()` does, and the three reads that decided presentation take
that scope's mode instead of the service's. The config `mode` becomes
the default agents shadow rather than a process-wide fact.
Two consequences are load-bearing. `run_code` now enters a view only
for scopes whose own mode presents it — a native agent must not find it
dispatchable because another agent in the process does — and the
reserved name holds whatever the configured mode, since any agent may
select a code mode later.
`dsh-agent-tool-mode` is the row a preset carries to declare this. A
code mode waits for the host's `codeRuntime` rather than assuming it,
so a runtime-less deployment fails the preset at mount, naming the
row, instead of at the session's first request.
The shipped `code` preset is `standard` plus that row, ordered second.
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.