Merge remote-tracking branch 'origin/master' into codex/pr-555-ci-fix

# Conflicts:
#	docs/module-graph.i18n.yaml
#	docs/module-graph.md
#	docs/module-graph.zh.md
#	packages/client/ui-conversation/src/client/input/hub.ts
#	packages/client/ui-conversation/tests/input-bar.spec.tsx
#	packages/client/ui-conversation/tests/service-orchestration.spec.ts
This commit is contained in:
creatixchu
2026-08-10 15:19:32 +08:00
259 changed files with 8182 additions and 626 deletions
+1 -1
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/README.md
README.md: 3cfbb5154713046846a3bfcb2ccea62c0e4cb6c0
README.md: d3a8943a78238d974d54028e38b773e932429b0e
README.zh.md: 4b3a1ee57ea61a8e8ba4d01cf7c719bbf8440e30
+1 -1
View File
@@ -2,7 +2,7 @@
English | [中文](README.zh.md)
One kind of design doc lives here. An **Agent Note** records a decision or proposal that shapes this codebase — the *why* and *what we gave up*, the parts code and docs can't carry. This file is the front door and contract: where Agent Notes live, when to write one, and [the in-file format](#the-file-format).
One kind of design doc lives here. An **Agent Note** records a decision or proposal that shapes this codebase — the *why* and *what we gave up*, the parts code and docs can't carry. This file is the entry point and contract: where Agent Notes live, when to write one, and [the in-file format](#the-file-format).
## Layout and naming
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.md
2026-07-15-llm-model-catalog-and-acp-selection.md: 77f8e379e2b07ecf4e67fa7197752543cfedd6dd
2026-07-15-llm-model-catalog-and-acp-selection.zh.md: ce4a04a66e345834bc2b16b743be89dc7a0b9424
2026-07-15-llm-model-catalog-and-acp-selection.md: bfd17c73b01319c10d5dc03333b3c726db5d6f33
2026-07-15-llm-model-catalog-and-acp-selection.zh.md: aeddada5591bb2da2c0861acc368516eff148172
@@ -26,17 +26,17 @@ Catalog membership is advisory. It drives selectors and diagnostics but never ch
`dsh-llm-pi-ai` maps the configured provider's installed `getModels(provider)` entries into the neutral catalog. Its existing request-time catalog lookup remains authoritative and still rejects unknown models with `UNKNOWN_MODEL`. `dsh-llm-deepseek` accepts an optional `models` config containing display entries, defaulting to `deepseek-v4-flash` named `DeepSeek-V4-Flash` and `deepseek-v4-pro` named `DeepSeek-V4-Pro`. An explicit list replaces those defaults and an empty list disables discovery. The entries improve selector UX for known public or private models, while every unlisted model id continues to pass through unchanged.
### Per-session selection in the front door
### Per-session selection in the front end
A selection is owned by the front door that offers it (today the TUI `/model` selector), never by `LlmService` or `AgentOptions`: those are deployment-wide or creation-wide objects, and mutating them would couple concurrent sessions. Each opaque choice carries the full provider/model pair, because the same model id may appear under multiple routes.
A selection is owned by the front end that offers it (today the TUI `/model` selector), never by `LlmService` or `AgentOptions`: those are deployment-wide or creation-wide objects, and mutating them would couple concurrent sessions. Each opaque choice carries the full provider/model pair, because the same model id may appear under multiple routes.
The ACP automation transport is not a catalog consumer. Its deployment config supplies one optional provider/model target for newly created agents, and it advertises no model selector or configuration-option interface.
### Prompt/request consistency and durability
`installModelSelection` (in `dsh-agent`) installs scoped `system-prompt/assemble` and `agent/request` listeners for a front-door-owned selection. Prompt assembly snapshots the selected pair once per step, overwrites the assembled `provider` and `model` variables after downstream prompt listeners, and the request listener applies that same snapshot after downstream request listeners. A selection during asynchronous assembly therefore starts on the next step rather than splitting prompt text from routing. Other call-config fields remain untouched.
`installModelSelection` (in `dsh-agent`) installs scoped `system-prompt/assemble` and `agent/request` listeners for a front-end-owned selection. Prompt assembly snapshots the selected pair once per step, overwrites the assembled `provider` and `model` variables after downstream prompt listeners, and the request listener applies that same snapshot after downstream request listeners. A selection during asynchronous assembly therefore starts on the next step rather than splitting prompt text from routing. Other call-config fields remain untouched.
The request header remains the durable source of truth. When a selection is actually used, the existing full `request/header` snapshot records it, and a front door initializes its selection from the folded last request header before falling back to creation options. A selection that is never used by a request is intentionally in-memory only because it never became model-visible state.
The request header remains the durable source of truth. When a selection is actually used, the existing full `request/header` snapshot records it, and a front end initializes its selection from the folded last request header before falling back to creation options. A selection that is never used by a request is intentionally in-memory only because it never became model-visible state.
## Alternatives considered
@@ -26,17 +26,17 @@ ACP 选择还必须保留提供方维度。同一个模型 ID 可能存在于多
`dsh-llm-pi-ai` 将已配置提供方的安装目录 `getModels(provider)` 映射为中立目录。其现有请求时目录查询仍是权威依据,未知模型仍以 `UNKNOWN_MODEL` 失败。`dsh-llm-deepseek` 接受可选的 `models` 配置作为展示条目,默认包含名为 `DeepSeek-V4-Flash``deepseek-v4-flash` 和名为 `DeepSeek-V4-Pro``deepseek-v4-pro`。显式列表会替换这些默认值,空列表则关闭发现。这些条目改善已知公开或私有模型的选择体验,而所有未列出的模型 ID 仍会原样透传。
### 前内的会话级选择
### 前内的会话级选择
选择由提供它的前拥有(今天是 TUI 的 `/model` 选择器),而不由 `LlmService``AgentOptions` 拥有:它们是部署级或创建级对象,改动它们会把并发会话耦合在一起。每个不透明选项都携带完整的提供方/模型对,因为同一模型 ID 可能出现在多个路由下。
选择由提供它的前拥有(今天是 TUI 的 `/model` 选择器),而不由 `LlmService``AgentOptions` 拥有:它们是部署级或创建级对象,改动它们会把并发会话耦合在一起。每个不透明选项都携带完整的提供方/模型对,因为同一模型 ID 可能出现在多个路由下。
ACP 自动化传输层不是目录消费方。它通过部署配置为新创建的 agent 提供一个可选的提供方/模型目标,不展示模型选择器或配置选项接口。
### 提示词/请求一致性与持久化
`installModelSelection`(位于 `dsh-agent`)为前拥有的选择安装 agent 作用域的 `system-prompt/assemble``agent/request` 监听器。提示词组装在每个步骤对所选组合做一次快照,在下游提示词监听器之后覆写组装出的 `provider``model` 变量;请求监听器在下游请求监听器之后应用同一快照。因此,发生在异步组装期间的选择会从下一个步骤生效,而不会让提示词文本与路由分裂。其他调用配置字段保持不变。
`installModelSelection`(位于 `dsh-agent`)为前拥有的选择安装 agent 作用域的 `system-prompt/assemble``agent/request` 监听器。提示词组装在每个步骤对所选组合做一次快照,在下游提示词监听器之后覆写组装出的 `provider``model` 变量;请求监听器在下游请求监听器之后应用同一快照。因此,发生在异步组装期间的选择会从下一个步骤生效,而不会让提示词文本与路由分裂。其他调用配置字段保持不变。
请求头仍是持久化的真源。当某个选择真正被使用时,现有的完整 `request/header` 快照会记录它;前先从折叠后的最后一个请求头初始化其选择,然后才回退到创建选项。从未被请求使用的选择有意只保留在内存中,因为它从未成为模型可见状态。
请求头仍是持久化的真源。当某个选择真正被使用时,现有的完整 `request/header` 快照会记录它;前先从折叠后的最后一个请求头初始化其选择,然后才回退到创建选项。从未被请求使用的选择有意只保留在内存中,因为它从未成为模型可见状态。
## 考虑过的替代方案
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md
2026-07-19-gui-layering-and-rpc-protocol.md: 7997a682c8745f7b3d0a9721acfa9355603f9b2e
2026-07-19-gui-layering-and-rpc-protocol.zh.md: cb36b5cb725128e1c5067e5e69e52aa851668e52
2026-07-19-gui-layering-and-rpc-protocol.md: 705b0df5feb5fedaae4d198aed71758b54586e93
2026-07-19-gui-layering-and-rpc-protocol.zh.md: b28b08b4b9da058e01af62e610d4e226d794151f
@@ -31,7 +31,7 @@ Directories layer as follows:
- **Fetch-arrival plugin packages** (`ui-layout`, `ui-sidebar`, `ui-conversation`, `ui-trajectory`): dual-entry — the root index is the node half (an empty `apply`, existing so the host Loader governs lifecycle and the web plugin registry discovers the package.json `dshClient` declaration); the implementation lives under `src/client/`, shipped as the `./client` subpath (a tsdown closure-factory bundle). Cross-plugin consumption of `/client` is type-only; value cooperation goes through cordis services.
- `apps/` holds the externally exported application shapes, assembled from Client / Host mixtures.
- `apps/web` (`dsh-frontend`) is the vite application: a thin `main.ts` over the shell surface exported by `dsh-client-web`.
- `apps/cli` (`@deepseek-ai/dsh`) dispatches shapes: `dsh web` = Host + webserver + the built `dsh-frontend` dist; `dsh run` = [a direct core Agent/Session front door](2026-08-09-headless-direct-core-front-door.md), with zero Host, HTTP, or browser layer.
- `apps/cli` (`@deepseek-ai/dsh`) dispatches shapes: `dsh web` = Host + webserver + the built `dsh-frontend` dist; `dsh run` = [a direct core Agent/Session entry point](2026-08-09-headless-direct-core-entry-point.md), with zero Host, HTTP, or browser layer.
- A future Electron shape reuses the same web client packages over an IPC fetch carrier.
```
@@ -79,7 +79,7 @@ Packages under `packages/host/*` and `packages/client/*` **must carry the direct
2. **Write an assembly module under `apps/`**: `startHost()` + a client subclass + the shape's private signal/print/exit semantics; a mixture never becomes a package — assembly is written in the app.
3. **Import `dsh-host-webserver` only if you need HTTP carriage**, otherwise zero ports.
The two existing shapes preserve the boundary: the Web shape mounts Host, carrier, and browser composition, while `dsh run` mounts a direct core runner with zero Host, HTTP, or ports. ACP-class protocol bridges do not follow the client-carrier checklist: they expose core to the external ecosystem, mount via `ctx.plugin(front-door plugin)` directly, and wear no fetch.
The two existing shapes preserve the boundary: the Web shape mounts Host, carrier, and browser composition, while `dsh run` mounts a direct core runner with zero Host, HTTP, or ports. ACP-class protocol bridges do not follow the client-carrier checklist: they expose core to the external ecosystem, mount via `ctx.plugin(entry-point plugin)` directly, and wear no fetch.
## Message protocol
@@ -242,7 +242,7 @@ Every client shape consumes one contract: adding a unary method is a five-step m
|---|---|
| Packaging by "product shape" (a web family, an electron family) | What shapes share is host/client capability, not the shape itself; capability-provider layering means a new shape needs zero new packages |
| A package per mixture (e.g. a standalone headless package) | A mixture has exactly one consumer (its own app); packaging it is ownerless abstraction, while assembly in the app is readable and disposable |
| Consuming clients connecting to ctx directly (skipping the apiproxy layer) | Client shapes require wire validation, observability, and multi-client consistency. Direct headless is a local front door with no client boundary and uses the public Agent/Session seams rather than a client command plane |
| Consuming clients connecting to ctx directly (skipping the apiproxy layer) | Client shapes require wire validation, observability, and multi-client consistency. Direct headless is a local entry point with no client boundary and uses the public Agent/Session seams rather than a client command plane |
| webserver depending on runtime (saving the handler injection) | Structural-typing injection keeps webserver reusable by sidecars/tests with zero workspace deps; a package dependency would drag assembly knowledge into the carrier layer |
| Package names without the group prefix (continuing dsh-<tail>) | `dsh-runtime`/`dsh-web-ui` lose their belonging in the flat npm namespace; the cost is one explicit paths entry per package |
| Reusing the in-repo JSON-RPC 2.0 (dsh-jsonrpc) | Numeric error codes degrade to a single fallback code, contracts get aligned by hand in two copies, and naming drifts without a convention |
@@ -29,7 +29,7 @@ Status: implemented
- **fetch 到达插件包**`ui-layout``ui-sidebar``ui-conversation``ui-trajectory`):双入口——根入口是 node 半边(空 `apply`,其存在是为了让 host Loader 管辖生命周期、让 web 插件注册表发现 package.json 的 `dshClient` 声明);实现住在 `src/client/` 下,经 `./client` 子路径发布(tsdown 闭包工厂 bundle)。跨插件消费 `/client` 只限类型;值层面的协作走 cordis 服务。
- `apps/` 作为对外导出的应用形态入口,可以由 Client / Host 混合组装。
- `apps/web``dsh-frontend`)是 vite 应用:`dsh-client-web` 导出的壳表面之上的一层薄 `main.ts`
- `apps/cli``@deepseek-ai/dsh`)做形态分发:`dsh web` = Host + webserver + 构建出的 `dsh-frontend` dist`dsh run` = [直接使用核心 AgentSession 的前门](2026-08-09-headless-direct-core-front-door.md),不含 Host、HTTP 或浏览器层。
- `apps/cli``@deepseek-ai/dsh`)做形态分发:`dsh web` = Host + webserver + 构建出的 `dsh-frontend` dist`dsh run` = [直接使用核心 AgentSession 的入口](2026-08-09-headless-direct-core-entry-point.md),不含 Host、HTTP 或浏览器层。
- 将来的 Electron 形态经由 IPC fetch 载体复用同一套 web client 包。
```
@@ -77,7 +77,7 @@ TypeScript 以 solution 根引用的**两个聚合 program** 检查(`tsconfig.
2. **在 `apps/` 下写拼装模块**`startHost()` + 客户端子类 + 该形态私有的信号/打印/退出语义;混合体不建包,拼装写在 app 里。
3. **需要 HTTP 承载才 import `dsh-host-webserver`**,否则零端口。
现有两种形态保持这一边界:Web 形态挂载 Host、载体与浏览器组合,而 `dsh run` 挂载直接使用核心服务的 runner,不包含 Host、HTTP 或端口。ACP 类协议桥不遵循 client 载体清单:它把 core 暴露给外部生态,直接通过 `ctx.plugin(前门插件)` 挂载,不套 fetch。
现有两种形态保持这一边界:Web 形态挂载 Host、载体与浏览器组合,而 `dsh run` 挂载直接使用核心服务的 runner,不包含 Host、HTTP 或端口。ACP 类协议桥不遵循 client 载体清单:它把 core 暴露给外部生态,直接通过 `ctx.plugin(入口插件)` 挂载,不套 fetch。
## 消息协议
@@ -240,7 +240,7 @@ export type ResponseValue<K> =
|---|---|
| 按「产品形态」分包(web 一族、electron 一族) | 形态间共享的是 host/client 两侧能力而非形态本身;能力支持方分层让新形态零新包 |
| 混合体建包(如 headless 独立包) | 混合体只有一个消费者(它自己的 app),建包是无主抽象;拼装写在 app 里可读可弃 |
| 消费型 client 直连 ctx(省 apiproxy 一层) | client 形态需要 wire 校验、观测与多 client 一致性。直接 headless 是没有 client 边界的本地前门,使用公开的 AgentSession seam,而不是 client 命令面 |
| 消费型 client 直连 ctx(省 apiproxy 一层) | client 形态需要 wire 校验、观测与多 client 一致性。直接 headless 是没有 client 边界的本地入口,使用公开的 AgentSession seam,而不是 client 命令面 |
| webserver 依赖 runtime(省 handler 注入) | 结构 typing 注入让 webserver 可被 sidecar/测试复用且零 workspace 依赖;包依赖会把装配知识拖进承载层 |
| 包名不带组前缀(沿用 dsh-<尾段> | `dsh-runtime`/`dsh-web-ui` 在扁平 npm 命名空间里失去归属信息;代价只是每包一条显式 paths |
| 复用仓内 JSON-RPC 2.0dsh-jsonrpc | 数字错误码退化成单码兜底、约定双份人肉对齐、命名无 convention 自然漂移 |
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-24-web-config-tree-boot-and-transport-layering.md
2026-07-24-web-config-tree-boot-and-transport-layering.md: 496499a691dbca012e5e953cbb6eb1d0bf25b635
2026-07-24-web-config-tree-boot-and-transport-layering.zh.md: c4bed7b730cf37e1d90750f5c93fbccb920ced21
2026-07-24-web-config-tree-boot-and-transport-layering.md: 92ec665acc745e61f656bd0e57454ad266b722f9
2026-07-24-web-config-tree-boot-and-transport-layering.zh.md: 7cd96ad9e52c19a005e6bff356dc5159ad3a31bc
@@ -16,23 +16,23 @@ English | [中文](2026-07-24-web-config-tree-boot-and-transport-layering.zh.md)
**Boot glue is a class pair.** `AppCLIEntry` (apps/cli) and `AppWebEntry` (the shell kernel) hold only what must exist independently of cordis: argv facts, the composed patch set, the parsed boot manifest, the module system instance, loading-page handles — everything else lives in plugins. `AppCLIEntry.run()` is three stages: layered env (ambient > cwd `.env` > `$DSH_HOME/.env`, closing the defect above) → patch composition → Loader include boot plus the activation audit. `AppWebEntry.run()` mirrors it browser-side: parse `window.__DSH_BOOT__` into a `BootManifest` (two views: npm-package rows for the module table, cordis-plugin rows for entry composition; malformed wire throws), build the module system, render the loading page, prefetch the `immediately` tier in parallel with Context/Loader setup, **await the prefetch before creating entries** (materialization is `tree.import`'s synchronous require, unprotected by fiber inject waiting; cross-package require edges such as i18n → runtime/client need every immediately-tier factory registered first — an empirically found 1025% boot race otherwise), adopt the modules entry, create the graph rows, settle, sweep.
**Config sources have one declaration place each.** Bundle yml values are engineering defaults, Settings sections are writable user preferences, CLI flags address their owning launcher rows, and env values enter through yml `!!js` expressions. Patches replace a row's config wholesale. The resolved frontend `distIndex` uses that patch channel as an assembly fact. The transport-independent provider/model default belongs to `ctx.agentDefaultModel`; the [direct headless front door](2026-08-09-headless-direct-core-front-door.md) and the Web gateway consume the same state.
**Config sources have one declaration place each.** Bundle yml values are engineering defaults, Settings sections are writable user preferences, CLI flags address their owning launcher rows, and env values enter through yml `!!js` expressions. Patches replace a row's config wholesale. The resolved frontend `distIndex` uses that patch channel as an assembly fact. The transport-independent provider/model default belongs to `ctx.agentDefaultModel`; the [direct headless entry point](2026-08-09-headless-direct-core-entry-point.md) and the Web gateway consume the same state.
**The transport splits five ways.** `dsh-host-apiproxy` is the gateway plugin (`api-gateway` row): it default-exports `ApiProxyService`, configures only `{workspaceRoot?}`, consumes the base layer's front-door-neutral `ctx.agentDefaultModel`, provides `ctx.apiProxy`, remains transport-agnostic, and registers no routes. `dsh-host-webserver` is a plain route-registration plugin: `HttpServerService` provides `ctx.httpServer` (`register(route) → disposer` with duplicate-pattern throw, `tapIndex` transforms applied in registration order, `port`), listens on activation, answers per-request failures with 400 and logging, and knows no harness concepts. The connection node half owns the `/api` binding from `ctx.apiProxy` through `toFetchHandler`. The modules node half (`ClientModuleHostService`, providing `ctx.clientModuleHost`) owns incremental package scanning, the bundle route, the index tap, and `onRebuilt`/`onGraphChanged` notification. The hmr node half owns dev reload through `fs.watchFile` membership and the `/plugins/events` SSE route.
**The transport splits five ways.** `dsh-host-apiproxy` is the gateway plugin (`api-gateway` row): it default-exports `ApiProxyService`, configures only `{workspaceRoot?}`, consumes the base layer's entry-point-neutral `ctx.agentDefaultModel`, provides `ctx.apiProxy`, remains transport-agnostic, and registers no routes. `dsh-host-webserver` is a plain route-registration plugin: `HttpServerService` provides `ctx.httpServer` (`register(route) → disposer` with duplicate-pattern throw, `tapIndex` transforms applied in registration order, `port`), listens on activation, answers per-request failures with 400 and logging, and knows no harness concepts. The connection node half owns the `/api` binding from `ctx.apiProxy` through `toFetchHandler`. The modules node half (`ClientModuleHostService`, providing `ctx.clientModuleHost`) owns incremental package scanning, the bundle route, the index tap, and `onRebuilt`/`onGraphChanged` notification. The hmr node half owns dev reload through `fs.watchFile` membership and the `/plugins/events` SSE route.
**Package export discipline.** The modules package exposes exactly `.` (node half) and `./client` (the complete browser half: `ClientModuleSystem`, `parseBootManifest`, the adoption plugin face) — no bespoke subpaths; wire types re-export through the root for host-side consumers. The adoption handshake: the kernel writes the constructed instance to `window.__DSH_MODULES__` before cordis exists; the `./client` apply reads the slot (missing = loud throw) and provides `ctx.modules`.
## Consequences
- Recomposing a web deployment is a yml/patch edit; the retired pieces (`mountWebPlugins`, `CLIENT_PACKAGES`, `createHostWebPluginRegistry`, `startWebServer`, the webserver's graph/SSE/api knowledge) are deleted.
- [Headless is a direct core front door](2026-08-09-headless-direct-core-front-door.md): its shipped profile contains the shared base Agent capabilities and omits Host, HTTP, Web, and browser layers. The transport split in this note is the browser surface's contract.
- [Headless is a direct core entry point](2026-08-09-headless-direct-core-entry-point.md): its shipped profile contains the shared base Agent capabilities and omits Host, HTTP, Web, and browser layers. The transport split in this note is the browser surface's contract.
- A TypeScript pitfall worth remembering: a `declare module 'cordis'` augmentation in a file with **no cordis import** is demoted to a standalone module declaration and silently shatters the program-wide `Context` merge (`ctx.on`/`ctx.effect` vanish across the program). Anchor with `import type {} from 'cordis'`.
## Alternatives considered
| Rejected | One-line reason |
|---|---|
| Dedicated `dsh-host-profile` receiver package | User model state belongs to the Settings-backed `ctx.agentDefaultModel`; an extra Host receiver would duplicate ownership and exclude direct front doors |
| Dedicated `dsh-host-profile` receiver package | User model state belongs to the Settings-backed `ctx.agentDefaultModel`; an extra Host receiver would duplicate ownership and exclude direct entry points |
| Runtime `assembly` shim plugin providing an `apiHandler` service | Existed only because `createApiProxy` lived in runtime; moving it into apiproxy made the gateway self-hosting, and `toFetchHandler` is a pure function the binding side calls |
| Full-rescan + incremental scan coexisting | Two implementations, two semantics; the single per-package path covers the activation pass too |
| A bespoke `./impl` export on the modules package | Non-uniform export surface; the standard `./client` carries the whole browser half |
@@ -16,23 +16,23 @@ Status: implemented
**boot 胶水由两个类组成。** `AppCLIEntry`apps/cli)与 `AppWebEntry`(壳内核)只持有那些必须独立于 cordis、提前存在的东西:argv 事实、合成的 patch 集、解析出的 boot manifest(元数据清单)、模块系统实例、loading 页句柄——其余一律进插件。`AppCLIEntry.run()` 三段:分层 envambient > cwd `.env` > `$DSH_HOME/.env`,顺手关掉上述缺陷)→ patch 合成 → Loader include boot 加 activation audit。`AppWebEntry.run()` 在浏览器侧镜像它:把 `window.__DSH_BOOT__` 解析成 `BootManifest`(双视角:npm 包行给模块表、cordis 插件行给 entry 组合;畸形 wire 大声抛)、建模块系统、渲染 loading 页、immediately 层预取与 Context/Loader 准备并行、**create entry 之前等预取齐**(物化是 `tree.import` 的同步 require,不受 fiber inject 等待保护;i18n → runtime/client 这类跨包 require 边要求 immediately 层工厂全部注册完——否则有实测 10–25% 的 boot 竞态)、收编 modules entry、逐一创建图行、settle、sweep。
**每个配置源有唯一声明位置。** 组合包 yml 值是工程默认,Settings 分节是可写的用户偏好,CLI(命令行界面)flags 面向其归属的启动器配置行,env 值则通过 yml `!!js` 表达式进入。patch 会整体替换一行的 config。解析后的前端 `distIndex` 通过同一条 patch 通道作为组装事实传递。与传输无关的提供方/模型默认值归 `ctx.agentDefaultModel` 所有;[直接 headless 前门](2026-08-09-headless-direct-core-front-door.md)与 Web 网关消费同一份状态。
**每个配置源有唯一声明位置。** 组合包 yml 值是工程默认,Settings 分节是可写的用户偏好,CLI(命令行界面)flags 面向其归属的启动器配置行,env 值则通过 yml `!!js` 表达式进入。patch 会整体替换一行的 config。解析后的前端 `distIndex` 通过同一条 patch 通道作为组装事实传递。与传输无关的提供方/模型默认值归 `ctx.agentDefaultModel` 所有;[直接 headless 入口](2026-08-09-headless-direct-core-entry-point.md)与 Web 网关消费同一份状态。
**传输五分。** `dsh-host-apiproxy` 是网关插件(`api-gateway` 行):默认导出 `ApiProxyService`,只配置 `{workspaceRoot?}`,消费 base 层不偏向特定前门`ctx.agentDefaultModel`provide `ctx.apiProxy`,保持传输无关且不注册路由。`dsh-host-webserver` 是朴素的路由注册插件:`HttpServerService` provide `ctx.httpServer``register(route) → disposer`、重复 pattern 即抛、`tapIndex` 按注册序应用、`port`),激活即 listen,单请求失败时答 400 并记日志,且不认识任何 harness 概念。connection node 半拥有从 `ctx.apiProxy``toFetchHandler` 绑定到 `/api` 的逻辑。modules node 半(`ClientModuleHostService`provide `ctx.clientModuleHost`)拥有单包增量扫描、bundle 路由、index tap 与 `onRebuilt`/`onGraphChanged` 通知。HMR node 半通过 `fs.watchFile` membership 与 `/plugins/events` SSE 路由拥有开发期重载。
**传输五分。** `dsh-host-apiproxy` 是网关插件(`api-gateway` 行):默认导出 `ApiProxyService`,只配置 `{workspaceRoot?}`,消费 base 层不偏向特定入口`ctx.agentDefaultModel`provide `ctx.apiProxy`,保持传输无关且不注册路由。`dsh-host-webserver` 是朴素的路由注册插件:`HttpServerService` provide `ctx.httpServer``register(route) → disposer`、重复 pattern 即抛、`tapIndex` 按注册序应用、`port`),激活即 listen,单请求失败时答 400 并记日志,且不认识任何 harness 概念。connection node 半拥有从 `ctx.apiProxy``toFetchHandler` 绑定到 `/api` 的逻辑。modules node 半(`ClientModuleHostService`provide `ctx.clientModuleHost`)拥有单包增量扫描、bundle 路由、index tap 与 `onRebuilt`/`onGraphChanged` 通知。HMR node 半通过 `fs.watchFile` membership 与 `/plugins/events` SSE 路由拥有开发期重载。
**包出口纪律。** modules 包只暴露 `.`node 半)与 `./client`(完整浏览器半:`ClientModuleSystem``parseBootManifest`、收编插件面)——不设专用子路径;wire 类型经根出口 re-export 给 host 侧消费方。收编握手:内核在 cordis 之前把建好的实例写入 `window.__DSH_MODULES__``./client` 的 apply 读取该槽位(缺少时显式抛错)并 provide `ctx.modules`
## 后果
- 重组一个 web 部署 = 改 yml/patch;退役件(`mountWebPlugins``CLIENT_PACKAGES``createHostWebPluginRegistry``startWebServer`、webserver 的图/SSE/api 知识)全部删除。
- [Headless 是直接 core 前门](2026-08-09-headless-direct-core-front-door.md):其随附 profile 包含共享的 base Agent 能力,并省去 Host、HTTP、Web 与浏览器层。本笔记的传输划分是浏览器 surface 的约定。
- [Headless 是直接 core 入口](2026-08-09-headless-direct-core-entry-point.md):其随附 profile 包含共享的 base Agent 能力,并省去 Host、HTTP、Web 与浏览器层。本笔记的传输划分是浏览器 surface 的约定。
- 一个值得记住的 TypeScript 坑:`declare module 'cordis'` augmentation 所在文件若**没有任何 cordis import**,会被降级成独立 module declaration,无声打散全程序的 `Context` merge`ctx.on`/`ctx.effect` 全程序消失)。用 `import type {} from 'cordis'` 锚定。
## 考虑过的替代方案
| 弃案 | 一行理由 |
|---|---|
| 专门的 `dsh-host-profile` 受体包 | 用户模型状态归 Settings 支撑的 `ctx.agentDefaultModel` 所有;额外的 Host 受体会重复归属,并排除直接前门 |
| 专门的 `dsh-host-profile` 受体包 | 用户模型状态归 Settings 支撑的 `ctx.agentDefaultModel` 所有;额外的 Host 受体会重复归属,并排除直接入口 |
| 运行时里的 `assembly` 垫层插件(provide `apiHandler` | 它的存在只因 `createApiProxy` 住运行时;本体迁入 apiproxy 后网关自持插件身份,且 `toFetchHandler` 是绑定方自己调的纯函数 |
| 全量重扫与增量扫描并存 | 两条实现两份语义;单包路径足以覆盖激活初扫 |
| modules 包特设 `./impl` 出口 | 出口面不统一;标准 `./client` 承载完整浏览器半 |
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md
2026-08-05-profile-plugin-bundles.md: 8b5ab0c99282f6868fc3f70781e618af9a317c09
2026-08-05-profile-plugin-bundles.zh.md: 2a685d68b3de9210488e26f8e6dd93dfc07f956c
2026-08-05-profile-plugin-bundles.md: 2924b3cb445064fd47d82bcc94ec8d77ded5721b
2026-08-05-profile-plugin-bundles.zh.md: b2287034010bcac1048bb385b2266f1bc75921da
@@ -12,7 +12,7 @@ The `dsh` launcher hardcoded its compositions: `base.cordis.yml` + `web.cordis.y
Everything becomes a **profile**: a directory `$DSH_HOME/profiles/<name>` with a `package.json` (pnpm-managed out-of-tree plugin `dependencies` plus the profile manifest `dsh.profile` with its ordered `bundles` layer list) and a user `cordis.patch.yml`. A **bundle** is an npm package declaring `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`; the two manifest kinds live under distinct `dsh.profile` / `dsh.bundle` keys so a package.json states which role it plays. The tree composes over an empty root by applying each bundle's patch in `dsh.profile.bundles` order, then the user layer, then `--patch` overlays, then flag patches — one `applyEntryPatches` call, identical for boot, flag derivation, and `--dump-config`.
The shipped bundles are `@deepseek-ai/dsh-base` (shared core rows), `@deepseek-ai/dsh-web-app` (browser Host rows and Web runtime glue), and `@deepseek-ai/dsh-headless` (a direct one-shot runner over base, without web-app). `dsh web` is the Web-flag alias for `--profile web`; `dsh run [--profile <name>] "task"` owns one-shot execution and defaults to the headless profile; generic `dsh --profile <name>` boots without a task. Patch overlays use `--patch`. `dsh plugin --profile <name> <args...>` is a thin pnpm forwarder that initializes the profile and reconciles `dsh.profile.bundles` with installed bundle declarations; a package without a bundle declaration remains a plain dependency. [Headless as a direct core front door](2026-08-09-headless-direct-core-front-door.md) owns the headless composition contract.
The shipped bundles are `@deepseek-ai/dsh-base` (shared core rows), `@deepseek-ai/dsh-web-app` (browser Host rows and Web runtime glue), and `@deepseek-ai/dsh-headless` (a direct one-shot runner over base, without web-app). `dsh web` is the Web-flag alias for `--profile web`; `dsh run [--profile <name>] "task"` owns one-shot execution and defaults to the headless profile; generic `dsh --profile <name>` boots without a task. Patch overlays use `--patch`. `dsh plugin --profile <name> <args...>` is a thin pnpm forwarder that initializes the profile and reconciles `dsh.profile.bundles` with installed bundle declarations; a package without a bundle declaration remains a plain dependency. [Headless as a direct core entry point](2026-08-09-headless-direct-core-entry-point.md) owns the headless composition contract.
The [`dsh run` command decision](../feature/2026-08-08-dsh-run-headless-command.md) owns the one-shot grammar; this note owns the profile composition it selects.
@@ -12,7 +12,7 @@ Status: implemented
一切都变成 **profile**:即目录 `$DSH_HOME/profiles/<name>`,其中包含一个 `package.json`pnpm 管理的树外插件 `dependencies`,加上 profile manifest(元数据清单)`dsh.profile` 及其有序的 `bundles` 层列表)和一份用户 `cordis.patch.yml`。**组合包**(bundle)是声明了 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包;两种 manifest 分别位于互不相同的 `dsh.profile` / `dsh.bundle` 键下,因此一份 package.json 能说明自己扮演哪种角色。配置树在空的根之上组合:按 `dsh.profile.bundles` 顺序应用每个组合包的 patch,然后是用户层,然后是 `--patch` overlay,最后是 flag patch——全部收敛为一次 `applyEntryPatches` 调用,启动、flag 派生与 `--dump-config` 使用完全相同的路径。
随附的组合包是 `@deepseek-ai/dsh-base`(共享核心配置行)、`@deepseek-ai/dsh-web-app`(浏览器 Host 配置行与 Web 运行时粘合层)和 `@deepseek-ai/dsh-headless`(直接叠加在 base 上且不含 web-app 的一次性 runner)。`dsh web` 是携带 Web flag 家族的 `--profile web` 别名;`dsh run [--profile <name>] "task"` 负责一次性执行,默认使用 headless profile;通用的 `dsh --profile <name>` 启动 profile 而不携带任务。patch overlay 使用 `--patch``dsh plugin --profile <name> <args...>` 是一层薄薄的 pnpm 转发器,负责初始化 profile,并依据已安装包的组合包声明调和 `dsh.profile.bundles`;没有组合包声明的包保持为普通依赖。[Headless 作为直接 core 前门](2026-08-09-headless-direct-core-front-door.md)负责 headless 组合约定。
随附的组合包是 `@deepseek-ai/dsh-base`(共享核心配置行)、`@deepseek-ai/dsh-web-app`(浏览器 Host 配置行与 Web 运行时粘合层)和 `@deepseek-ai/dsh-headless`(直接叠加在 base 上且不含 web-app 的一次性 runner)。`dsh web` 是携带 Web flag 家族的 `--profile web` 别名;`dsh run [--profile <name>] "task"` 负责一次性执行,默认使用 headless profile;通用的 `dsh --profile <name>` 启动 profile 而不携带任务。patch overlay 使用 `--patch``dsh plugin --profile <name> <args...>` 是一层薄薄的 pnpm 转发器,负责初始化 profile,并依据已安装包的组合包声明调和 `dsh.profile.bundles`;没有组合包声明的包保持为普通依赖。[Headless 作为直接 core 入口](2026-08-09-headless-direct-core-entry-point.md)负责 headless 组合约定。
[`dsh run` 命令决策](../feature/2026-08-08-dsh-run-headless-command.md)负责一次性语法;本 Agent Note 负责该语法所选择的 profile 组合。
@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-09-headless-direct-core-entry-point.md
2026-08-09-headless-direct-core-entry-point.md: 49afe2993de7302adbedcdf9e8e2347d6424ee2a
2026-08-09-headless-direct-core-entry-point.zh.md: 73c1cbe5ac777025f63f46751b1d5ccebbfe9676
@@ -1,22 +1,22 @@
# Agent Note: headless is a direct core front door
# Agent Note: headless is a direct core entry point
Status: implemented
English | [中文](2026-08-09-headless-direct-core-front-door.zh.md)
English | [中文](2026-08-09-headless-direct-core-entry-point.zh.md)
## Problem
The `headless` product contract is one local task with final assistant text on stdout, a success-sensitive exit code, empty stderr on success, and no listening port. A composition containing Workspace Host services, ApiProxy, HTTP, the Web runtime, or browser plugins contradicts that contract and makes local completion depend on an unrelated transport tree.
The direct front door still needs the same deployment model state as Web-created Agents. A separate provider/model default would give one deployment two answers, while deriving completion before the Agent and Session persistence are quiescent permits stdout and the exit code to observe incomplete state.
The direct entry point still needs the same deployment model state as Web-created Agents. A separate provider/model default would give one deployment two answers, while deriving completion before the Agent and Session persistence are quiescent permits stdout and the exit code to observe incomplete state.
## Decision
The shipped `headless` profile contains `dsh-base` and `dsh-headless`. The headless bundle supplies its persona and tool mode, disables HMR, mounts the Code Mode worker explicitly, and inserts `headless-runner`. Its tree contains no `@deepseek-ai/dsh-host-*` package, ApiProxy, HTTP server, Web runtime, or browser client. Code Mode and Session persistence are one-shot Agent capabilities independent of Web presentation.
`headless-runner` is a direct core front door. After Loader settlement, it reads `ctx.agentDefaultModel.currentSelection()`, creates a fresh persisted Agent through `ctx.agents.create`, installs that `ModelSelection` in the Agent scope, waits for startup quiescence, anchors the Session sequence, submits one ordinary user message, and waits for quiescence again. It awaits `ctx.sessions.flush`, folds its durable event interval for the last non-empty assistant text and final `turn/end` reason, writes the text plus one newline to stdout, and requests bounded launcher shutdown with exit 0 exactly when the reason is `completed`. A terminal `error` reason writes its durable code and message to stderr; unexpected driver failures also use stderr and exit 1.
`headless-runner` is a direct core entry point. After Loader settlement, it reads `ctx.agentDefaultModel.currentSelection()`, creates a fresh persisted Agent through `ctx.agents.create`, installs that `ModelSelection` in the Agent scope, waits for startup quiescence, anchors the Session sequence, submits one ordinary user message, and waits for quiescence again. It awaits `ctx.sessions.flush`, folds its durable event interval for the last non-empty assistant text and final `turn/end` reason, writes the text plus one newline to stdout, and requests bounded launcher shutdown with exit 0 exactly when the reason is `completed`. A terminal `error` reason writes its durable code and message to stderr; unexpected driver failures also use stderr and exit 1.
`@deepseek-ai/dsh-agent-default-model` owns the transport-independent default used for an Agent without a session-local selection. `AgentDefaultModelService` provides `ctx.agentDefaultModel` and registers the `agent-default-model` Settings section. Composition config supplies `{provider, model}`; user settings may also supply `reasoningEffort`. `currentSelection()` returns the live complete selection and `saveSelection()` writes it as a complete section, so a selection without an effort clears any stored effort. `dsh-base` supplies the composition entry. Direct and ApiProxy front doors consume this service; ApiProxy alone owns session-local precedence, model validation, and persistence of accepted Web selections.
`@deepseek-ai/dsh-agent-default-model` owns the transport-independent default used for an Agent without a session-local selection. `AgentDefaultModelService` provides `ctx.agentDefaultModel` and registers the `agent-default-model` Settings section. Composition config supplies `{provider, model}`; user settings may also supply `reasoningEffort`. `currentSelection()` returns the live complete selection and `saveSelection()` writes it as a complete section, so a selection without an effort clears any stored effort. `dsh-base` supplies the composition entry. Direct and ApiProxy entry points consume this service; ApiProxy alone owns session-local precedence, model validation, and persistence of accepted Web selections.
`loadProfile` recognizes the exact installation-owned headless tuple (`dsh-base`, `dsh-web-app`, `dsh-headless`) and normalizes it to the shipped headless template while preserving every other manifest field. Extra, missing, or reordered bundle lists are user-owned and remain untouched.
@@ -31,7 +31,7 @@ Package tests use the real Session store and Agent registry around a scripted Ag
| Alternative | Contract mismatch |
|---|---|
| Keep `dsh-web-app` but suppress its observation line | The process still opens a port and carries the Host, Web, and browser trees. |
| Build a Host-only one-shot bundle around ApiProxy | ApiProxy is a client protocol gateway; a local one-shot front door has no client boundary. |
| Build a Host-only one-shot bundle around ApiProxy | ApiProxy is a client protocol gateway; a local one-shot entry point has no client boundary. |
| Use `InProcessApiClient` for product-level protocol coverage | Product execution would depend on an unrelated protocol solely to exercise that protocol. |
| Give headless a separate provider/model config | Direct and Web creation would have independent defaults and persistence. |
| Omit Code Mode and Session persistence | Both capabilities belong to one-shot Agent execution rather than Web presentation. |
@@ -2,13 +2,13 @@
Status: implemented
[English](2026-08-09-headless-direct-core-front-door.md) | 中文
[English](2026-08-09-headless-direct-core-entry-point.md) | 中文
## 问题
`headless` 的产品约定是一个本地任务:最终 assistant 文本写入 stdout,退出状态反映成功与否,成功时 stderr 为空,并且不打开监听端口。包含 Workspace Host 服务、ApiProxy、HTTP、Web 运行时或浏览器插件的组合违背这一约定,也使本地完成状态依赖无关的传输树。
直接前门仍需要与 Web 所创建 Agent 相同的部署模型状态。独立的提供方/模型默认值会让同一部署产生两种答案,而在 Agent 与会话持久化完全停稳之前推导完成状态,会让 stdout 与退出状态观察到不完整状态。
直接入口仍需要与 Web 所创建 Agent 相同的部署模型状态。独立的提供方/模型默认值会让同一部署产生两种答案,而在 Agent 与会话持久化完全停稳之前推导完成状态,会让 stdout 与退出状态观察到不完整状态。
## 决策
@@ -16,7 +16,7 @@ Status: implemented
`headless-runner` 是直接使用核心服务的入口。Loader 完全加载后,它读取 `ctx.agentDefaultModel.currentSelection()`,通过 `ctx.agents.create` 创建一个新的持久化 Agent,在 Agent 作用域中安装该 `ModelSelection`,等待启动工作完全停稳,锚定会话事件序号,提交一条普通用户消息,再次等待完全停稳。随后,它等待 `ctx.sessions.flush`,折叠自身持有的持久事件区间,以取得最后一条非空 assistant 文本和最终 `turn/end` 结束原因,将文本连同一个换行写入 stdout,并且仅在结束原因为 `completed` 时请求启动器以退出状态 0 有界关闭。结束原因为 `error` 时,其持久化错误码与消息写入 stderr;驱动器的意外失败也写入 stderr 并以 1 退出。
`@deepseek-ai/dsh-agent-default-model` 拥有与传输无关的默认值,供没有会话级选择的 Agent 使用。`AgentDefaultModelService` 提供 `ctx.agentDefaultModel` 并注册 `agent-default-model` Settings 分节。组合配置提供 `{provider, model}`,用户设置还可以提供 `reasoningEffort``currentSelection()` 返回当前的完整选择,`saveSelection()` 则写入完整分节,因此不含强度的选择会清除已存强度。`dsh-base` 提供组合条目。直接前门与 ApiProxy 前门均消费该服务;只有 ApiProxy 负责会话级优先级、模型校验与已接受 Web 选择的持久化。
`@deepseek-ai/dsh-agent-default-model` 拥有与传输无关的默认值,供没有会话级选择的 Agent 使用。`AgentDefaultModelService` 提供 `ctx.agentDefaultModel` 并注册 `agent-default-model` Settings 分节。组合配置提供 `{provider, model}`,用户设置还可以提供 `reasoningEffort``currentSelection()` 返回当前的完整选择,`saveSelection()` 则写入完整分节,因此不含强度的选择会清除已存强度。`dsh-base` 提供组合条目。直接入口与 ApiProxy 入口均消费该服务;只有 ApiProxy 负责会话级优先级、模型校验与已接受 Web 选择的持久化。
`loadProfile` 识别安装过程拥有的精确 headless 元组(`dsh-base``dsh-web-app``dsh-headless`),将其规范化为随附的 headless 模板,并保留 manifest(元数据清单)的其他所有字段。带额外项、缺少项或顺序不同的组合包列表归用户所有,保持不变。
@@ -31,7 +31,7 @@ Status: implemented
| 替代方案 | 约定不匹配之处 |
|---|---|
| 保留 `dsh-web-app`,但隐藏观察行 | 进程仍会打开端口并携带 Host、Web 与浏览器插件树。 |
| 围绕 ApiProxy 构建纯 Host 一次性组合包 | ApiProxy 是客户端协议网关,而本地一次性前门没有客户端边界。 |
| 围绕 ApiProxy 构建纯 Host 一次性组合包 | ApiProxy 是客户端协议网关,而本地一次性入口没有客户端边界。 |
| 使用 `InProcessApiClient` 实现产品级协议覆盖 | 产品执行会仅为测试无关协议而依赖该协议。 |
| 为 headless 单独提供提供方/模型配置 | 直接创建与 Web 创建会拥有彼此独立的默认值和持久化。 |
| 省略 Code Mode 与会话持久化 | 两项能力都属于一次性 Agent 执行,而不是 Web 呈现。 |
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-07-20-error-cause-chain-diagnostics.md
2026-07-20-error-cause-chain-diagnostics.md: 32716b5a68b3b73bded47633eca95995cdbfc586
2026-07-20-error-cause-chain-diagnostics.zh.md: 9911c32b6d68c1f5569a1fceadb65e23c6586594
2026-07-20-error-cause-chain-diagnostics.md: b80dd08d79a57738a6eef2f8638b0336ca80d4bf
2026-07-20-error-cause-chain-diagnostics.zh.md: 914e983d7ea81eef8bca8fd5aa3791f2f44d4080
@@ -9,7 +9,7 @@ English | [中文](2026-07-20-error-cause-chain-diagnostics.zh.md)
A TUI run against an unreachable DeepSeek endpoint failed with the single notice `fetch failed` and no further detail. Two independent gaps produced that dead end:
1. undici's `fetch` wraps every transport failure (DNS, refused connection, TLS, proxy) in a bare `TypeError: fetch failed` whose actionable detail — `ECONNREFUSED`, `bad port`, the Happy Eyeballs AggregateError — lives on `error.cause`. Every diagnostic boundary in the harness rendered only `error.message` (or `String(error)`, which is equivalent for Errors), so the wrapper masked the diagnosis in the TUI notice, the durable `turn/end` reason, and every logger line.
2. The readline front door (`dsh-stdio`) rendered no failure reason at all: a `turn/end` with `reason.kind === 'error'` printed nothing but the next `> ` prompt, so the same failure in `demo:repl` was pure silence.
2. The readline entry point (`dsh-stdio`) rendered no failure reason at all: a `turn/end` with `reason.kind === 'error'` printed nothing but the next `> ` prompt, so the same failure in `demo:repl` was pure silence.
## Decision
@@ -9,7 +9,7 @@ Status: implemented
TUI 连接不可达的 DeepSeek 端点时,失败只显示一条 `fetch failed` 通知,没有任何进一步细节。两个独立缺口共同造成了这个死胡同:
1. undici 的 `fetch` 把所有传输层失败(DNS、连接被拒、TLS、代理)包装成裸的 `TypeError: fetch failed`,可操作的细节——`ECONNREFUSED``bad port`、Happy Eyeballs 的 AggregateError——都在 `error.cause` 上。harness 里的每个诊断边界都只渲染 `error.message`(或对 Error 等价的 `String(error)`),于是包装层在 TUI 通知、持久化的 `turn/end` reason 和所有日志行里都掩盖了诊断信息。
2. readline 前门`dsh-stdio`)完全不渲染失败原因:`reason.kind === 'error'``turn/end` 只打印下一个 `> ` 提示符,同样的失败在 `demo:repl` 里就是纯粹的沉默。
2. readline 入口`dsh-stdio`)完全不渲染失败原因:`reason.kind === 'error'``turn/end` 只打印下一个 `> ` 提示符,同样的失败在 `demo:repl` 里就是纯粹的沉默。
## 决策
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-06-24-workspace-context.md
2026-06-24-workspace-context.md: 53a580aee50752b7c6daeff5caa42ba6409c8885
2026-06-24-workspace-context.md: e7a3724847b9dc8cfad11e87b2c96a3ef442bcba
2026-06-24-workspace-context.zh.md: 39c3f52da10b7299301d10bd8b78330cbae8e19c
@@ -78,7 +78,7 @@ There is intentionally no watcher. Detection occurs at the next successful struc
## Consequences
Workspace guidance is isolated per session and shared by the demo front doors, Web Host, and every tool presentation mode. Initial, nested, and changed instructions are durable and replayable. The generic session/agent context contract carries typed source data through inbox-staged and durably entered user messages without flattening entries.
Workspace guidance is isolated per session and shared by the demo entry points, Web Host, and every tool presentation mode. Initial, nested, and changed instructions are durable and replayable. The generic session/agent context contract carries typed source data through inbox-staged and durably entered user messages without flattening entries.
Repository text remains untrusted input. Lower-authority user-role framing, explicit precedence language, and delimiter escaping reduce risk but do not eliminate prompt injection. Following a candidate symlink to its target widens that surface to off-tree content, so the permission and sandbox layers that confine `ctx.fs` to trusted roots are the boundary that treats workspace files as data rather than authority (the [instruction-symlink follow note](2026-07-21-follow-instruction-symlinks.md) owns the residual risk).
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-16-harness-level-loop.md
2026-07-16-harness-level-loop.md: e84a5738a55988d3b968eac829d9daaf10d7e304
2026-07-16-harness-level-loop.zh.md: d7cda56d66d4456ac5c3c7d55bb238e85fcacbc5
2026-07-16-harness-level-loop.md: e5dd94fbf76e27f0843666f29622969635ef2fc3
2026-07-16-harness-level-loop.zh.md: 773b5ff1f4b07206d0d817c29774435ce24407b4
@@ -70,7 +70,7 @@ The human UX follows the compact Codex shape in the [public OpenAI Codex TUI dis
The model receives only `get_goal`, `create_goal`, and `update_goal`. It may create a goal when a direct human request clearly asks for substantial multi-round work, and it may infer that intent in any language. It must not turn routine one-turn work into a goal. Code requires a direct human message in the current live root-agent turn; semantic interpretation remains model judgment. An autonomous goal round may report `complete` or `blocked` for the exact current goal round but cannot edit, pause, resume, or replace the human objective.
TUI mounts the shared command registry and complete goal stack by default and exposes `/goal` through one producer. ACP mounts the goal domain, model tools, and same-session driver but deliberately omits the human command plane. Every effective registered command is discoverable and invocable through every composed command adapter; a plugin incompatible with an application omits its command producer from that composition rather than relying on registry-level surface masks. The UI-less agent spine is opt-in so one-shot callers do not silently become multi-round operations. The headless CLI and JSON-RPC front doors do not consume the command plane; ordinary human text can still authorize model goal tools when that stack is composed.
TUI mounts the shared command registry and complete goal stack by default and exposes `/goal` through one producer. ACP mounts the goal domain, model tools, and same-session driver but deliberately omits the human command plane. Every effective registered command is discoverable and invocable through every composed command adapter; a plugin incompatible with an application omits its command producer from that composition rather than relying on registry-level surface masks. The UI-less agent spine is opt-in so one-shot callers do not silently become multi-round operations. The headless CLI and JSON-RPC entry points do not consume the command plane; ordinary human text can still authorize model goal tools when that stack is composed.
### Fresh-agent Ralph execution
@@ -70,7 +70,7 @@ Goal Round 驱动器为每个特定的实时 agent 至多拥有一个待定预
模型只接收 `get_goal``create_goal``update_goal`。当直接人类请求清楚要求大量多 Round 工作时,模型可以创建目标,并且可以从任何语言推断该意图。它不得把日常单 Turn 工作变成目标。代码要求当前实时根 agent Turn 中有一条人类直接发送的消息;语义解释仍是模型判断。自治目标 Round 可以为确切的当前 Goal Round 报告 `complete``blocked`,但不能编辑、暂停、恢复或替换人类目标。
TUI 默认挂载共享命令注册表和完整目标栈,并通过一个生产方暴露 `/goal`。ACPAgent Client Protocol)挂载目标领域、模型工具和同会话驱动器,但有意省略人类命令平面。每条有效已注册命令都能被每个已组合的命令适配器发现和调用;若插件与某应用不兼容,该应用组合会省略其命令生产方,而不是依赖注册表层面的表面掩码。无 UI 的 agent 主干要求显式选择加入,以免单次调用方静默变成多 Round 操作。无头 CLI(命令行界面)与 JSON-RPC 前端不消费命令平面;挂载目标栈后,普通人类文本仍可授权模型目标工具。
TUI 默认挂载共享命令注册表和完整目标栈,并通过一个生产方暴露 `/goal`。ACPAgent Client Protocol)挂载目标领域、模型工具和同会话驱动器,但有意省略人类命令平面。每条有效已注册命令都能被每个已组合的命令适配器发现和调用;若插件与某应用不兼容,该应用组合会省略其命令生产方,而不是依赖注册表层面的表面掩码。无 UI 的 agent 主干要求显式选择加入,以免单次调用方静默变成多 Round 操作。无头 CLI(命令行界面)与 JSON-RPC 运行入口不消费命令平面;挂载目标栈后,普通人类文本仍可授权模型目标工具。
### 全新 agent Ralph 执行
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.md
2026-07-16-persistent-pty-sessions.md: f27d799574e61d92123c9ca4e31c5c2a9d3229b4
2026-07-16-persistent-pty-sessions.zh.md: 0c23619faf00794c2c4e7b3f85ef961faddd99e0
2026-07-16-persistent-pty-sessions.md: fb9cd06bade7bc357baa738f0d9dd03b7f5b7936
2026-07-16-persistent-pty-sessions.zh.md: 55a5848c1ab1e8c2cd3b29f2d4748ea4abbe088c
@@ -152,7 +152,7 @@ The package ships concise tool guidance explaining persistent state, owner isola
**Include TUI sequences and BEL handling.** Rejected. The source prototype treats those paths as timing-sensitive and still records unresolved alternate-screen and interaction failures. Line-oriented PTY use proves the core value without making those unverified behaviors foundational.
**Use an out-of-process daemon immediately.** Rejected for the initial in-process capability because current persistent front doors already keep a Cordis context alive. A daemon becomes justified by cross-process restoration or multi-client attachment, both deferred here.
**Use an out-of-process daemon immediately.** Rejected for the initial in-process capability because current long-lived entry points already keep a Cordis context alive. A daemon becomes justified by cross-process restoration or multi-client attachment, both deferred here.
## Verification
@@ -152,7 +152,7 @@ plugins:
**包含 TUI sequence 与 BEL 处理。**拒绝。源 prototype 将这些路径视为 timing-sensitive,且仍记录未解决的 alternate-screen 和交互失败。行式 PTY 已能证明核心价值,无需把未经验证的行为放进基础层。
**立即采用进程外 daemon。**初始的进程内功能不采用,因为当前持久 front door 已能维持 Cordis context。跨进程恢复或多客户端 attach 会让 daemon 变得合理,但两者都已推迟。
**立即采用进程外 daemon。**初始的进程内功能不采用,因为当前长驻的运行入口已能维持 Cordis context。跨进程恢复或多客户端 attach 会让 daemon 变得合理,但两者都已推迟。
## 验证
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-19-human-goal-command.md
2026-07-19-human-goal-command.md: 5fdd80f7423b80e84e58f7379130ee59a2e8a723
2026-07-19-human-goal-command.zh.md: 89d29497abfc758ff8373d272aa8620ca1bcfcc2
2026-07-19-human-goal-command.md: d68e4025a4d37d07211f15ddfc6069bc7c637124
2026-07-19-human-goal-command.zh.md: dbda35c731ccdc7aff2d4213a3df8b78070319df
@@ -68,5 +68,5 @@ The producer suite uses the real command registry, goal service, agent registry,
- The portable command contract has no modal editor or confirmation interaction; inline edit and explicit clear are intentional until a general cross-surface interaction primitive exists.
- `/goal` does not accept a per-command round cap. Deployment config owns the default, and the authorized model tool can edit a cap after direct human instruction.
- TUI renders portable plain text rather than a continuously updated goal status widget. Reconnectable command output and adapter-specific status indicators are deferred.
- The ACP automation server, headless CLI, and JSON-RPC front doors do not consume the command registry.
- The ACP automation server, headless CLI, and JSON-RPC entry points do not consume the command registry.
- The command observes and mutates state but does not certify completion or blockers. Evaluator-backed certification remains deferred to a separate policy layer with an explicit authority and isolation contract.
@@ -68,5 +68,5 @@ TUI 应用包作出相反的产品选择。它默认让 `goals` 使用所有者
- 可移植命令约定没有模态编辑器或确认交互;在出现通用跨界面交互原语之前,行内编辑与明确清除是有意选择。
- `/goal` 不接受逐命令 Round 上限。部署配置拥有默认值;得到直接人类指示后,已授权模型工具可以编辑上限。
- TUI 渲染可移植纯文本,而不是持续更新的目标状态组件。可重连命令输出和适配器专用状态指示器予以延期。
- ACP 自动化服务器、无头 CLI 与 JSON-RPC 前端不消费命令注册表。
- ACP 自动化服务器、无头 CLI 与 JSON-RPC 运行入口不消费命令注册表。
- 该命令观察并改变状态,但不认证完成或阻塞。基于评估器的认证延期到具有明确权限与隔离约定的独立策略层。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-19-plugin-command-registration.md
2026-07-19-plugin-command-registration.md: c8f0f2772a41948e9eb257a16f40194518c568f9
2026-07-19-plugin-command-registration.zh.md: a8f7a3d1e7947aeb41344a72106cee55d249010c
2026-07-19-plugin-command-registration.md: 76feba84492bd6b245246a6f1fb1e8f68d555684
2026-07-19-plugin-command-registration.zh.md: e89cb79ba1e15dfefa527fe7bc37c09c3449f5b4
@@ -12,7 +12,7 @@ A shared mechanism must remain a UI concern rather than a model tool or agent-lo
## Decision
`@deepseek-ai/dsh-commands` in `packages/interaction/commands/` is the product command registry. The TUI app bundle mounts it beside its consuming front door; the [automation-only ACP app](../simplification/2026-07-23-acp-automation-only-protocol.md) and the executor-less, UI-less agent spine omit it. TUI injects the service, while command producers depend only on the registry and any domain they operate.
`@deepseek-ai/dsh-commands` in `packages/interaction/commands/` is the product command registry. The TUI app bundle mounts it beside its consuming front end; the [automation-only ACP app](../simplification/2026-07-23-acp-automation-only-protocol.md) and the executor-less, UI-less agent spine omit it. TUI injects the service, while command producers depend only on the registry and any domain they operate.
### Registry contract
@@ -50,7 +50,7 @@ TUI tests exercise all migrated built-ins, live plugin discovery, help/autocompl
- **Keep adapter-local switches** — rejected because optional plugins cannot contribute discovery and behavior without editing the TUI.
- **Represent human commands as model tools** — rejected because discovery and direct invocation are human UI behavior; routing through the model adds latency, token cost, and reinterpretation.
- **Put the registry in the core agent spine** — rejected because UI-less front doors do not consume it, while TUI can compose it explicitly.
- **Put the registry in the core agent spine** — rejected because UI-less entry points do not consume it, while TUI can compose it explicitly.
- **Make `dsh-agent-loop` inject commands** — rejected because the loop does not execute or discover human commands. Agent-scoped producers declare the UI dependency in a child plugin instead.
- **Attach adapter masks to each definition** — rejected because support is a composition fact, not command-domain state. Every composed adapter exposes a registered command; an incompatible plugin omits registration in that deployment.
- **Send unknown slash input to the model** — rejected because typoed or unavailable direct actions must fail predictably rather than change execution planes.
@@ -68,4 +68,4 @@ TUI tests exercise all migrated built-ins, live plugin discovery, help/autocompl
- Input metadata is limited to an unstructured text hint. Typed forms, argument schemas, and completion providers remain command-owned or require a later registry or consumer extension.
- Generic command output is live-only and is not reconstructed after TUI restart.
- Registry cancellation stops awaiting immediately, but external work stops only when a handler cooperates with its signal.
- The ACP automation server, headless CLI, and JSON-RPC SDK front doors do not expose the command plane; only TUI consumes it.
- The ACP automation server, headless CLI, and JSON-RPC SDK entry points do not expose the command plane; only TUI consumes it.
@@ -12,7 +12,7 @@ TUI 拥有斜杠命令。如果命令名、帮助文本、自动补全、分派
## 决策
位于 `packages/interaction/commands/``@deepseek-ai/dsh-commands` 是产品命令注册表。TUI 应用 bundle(组合包)把它挂载在消费该服务的入口旁;[仅面向自动化的 ACPAgent Client Protocol)应用](../simplification/2026-07-23-acp-automation-only-protocol.md)和无执行器、无 UI 的智能体 spine(主干)都省略该服务。TUI 注入该服务,命令生产者只依赖注册表及其操作的领域。
位于 `packages/interaction/commands/``@deepseek-ai/dsh-commands` 是产品命令注册表。TUI 应用 bundle(组合包)把它挂载在消费该服务的前端旁;[仅面向自动化的 ACPAgent Client Protocol)应用](../simplification/2026-07-23-acp-automation-only-protocol.md)和无执行器、无 UI 的智能体 spine(主干)都省略该服务。TUI 注入该服务,命令生产者只依赖注册表及其操作的领域。
### 注册表约定
@@ -50,7 +50,7 @@ TUI 测试覆盖全部迁移后的内置命令、实时插件发现、帮助与
- **保留适配器本地 switch**——不予采纳,因为可选插件无法贡献发现与行为,除非修改 TUI。
- **把人类命令表示为模型工具**——不予采纳,因为发现与直接调用属于人类 UI 行为;经由模型路由会增加延迟、token 成本和重新解释。
- **把注册表放入核心智能体主干**——不予采纳,因为无 UI 前端不消费它,而 TUI 可以显式组合它。
- **把注册表放入核心智能体主干**——不予采纳,因为无 UI 运行入口不消费它,而 TUI 可以显式组合它。
- **让 `dsh-agent-loop` 注入 commands**——不予采纳,因为循环不执行也不发现人类命令。智能体作用域生产者改为在子插件中声明 UI 依赖。
- **为每个定义附加适配器掩码**——不予采纳,因为支持能力是组合事实,而不是命令领域状态。每个已组合适配器都暴露已注册命令;不兼容插件不会在该部署中注册。
- **把未知斜杠输入发送给模型**——不予采纳,因为输入错误或不可用的直接操作必须可预测地失败,而不能改变执行平面。
@@ -68,4 +68,4 @@ TUI 测试覆盖全部迁移后的内置命令、实时插件发现、帮助与
- 输入元数据仅限非结构化文本提示。类型化表单、参数模式和补全提供器仍由命令拥有,或需要后续注册表或消费方扩展。
- 通用命令输出仅实时存在,TUI 重启后不会重建。
- 注册表取消会立即停止等待,但外部工作只有在处理器配合信号时才会停止。
- ACP 自动化服务器、无头 CLI 与 JSON-RPC SDK 前端不暴露命令平面;只有 TUI 消费它。
- ACP 自动化服务器、无头 CLI 与 JSON-RPC SDK 运行入口不暴露命令平面;只有 TUI 消费它。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-21-local-instruction-overlay.md
2026-07-21-local-instruction-overlay.md: 3c7b2141b0515b5e667be4add6ad765e26c88cd8
2026-07-21-local-instruction-overlay.md: fb5f916d426595a80bbbaa4192e4a3975b922b8a
2026-07-21-local-instruction-overlay.zh.md: c97ed04607f497d829da0e904c248836252c73f7
@@ -26,7 +26,7 @@ The base and local candidates in one directory must stay independent across base
**Keep it opt-in through `instructionFileCandidates`.** Rejected: one directory has a single winner, so a `.local.` name added to that list shadows the base file rather than supplementing it. The packages guidance to keep opt-ins out of shipped defaults is outweighed here by strong prior art and the user-facing expectation that `.local.` files are always read.
**Default at the product `cordis.yml` level instead of the plugin schema.** Rejected: it would enable `.local.` only for whichever front door remembered to opt in, splitting behavior across TUI/ACP/headless and duplicating a value that belongs beside the existing candidate default.
**Default at the product `cordis.yml` level instead of the plugin schema.** Rejected: it would enable `.local.` only for whichever entry point remembered to opt in, splitting behavior across TUI/ACP/headless and duplicating a value that belongs beside the existing candidate default.
**Reuse the bare directory as the scope key for base and local files.** Rejected: base and local files in one directory would collide in every scope-keyed map, so a change to one would suppress or overwrite the other. A distinct scope key per candidate keeps them independent without widening the persisted metadata shape.
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-24-web-session-model-selector.md
2026-07-24-web-session-model-selector.md: 78017b14a806f8e609a85e340094cd2a349d47e1
2026-07-24-web-session-model-selector.zh.md: 87a342721f5e47f5bcedfafb578eb6916101d2f5
2026-07-24-web-session-model-selector.md: e6a96ac62f69a3bd312f61cc920caa259d2dc5b0
2026-07-24-web-session-model-selector.zh.md: 5a0245359d69ac6e59a20dc3276b9411c4b25e23
@@ -6,7 +6,7 @@ English | [中文](2026-07-24-web-session-model-selector.zh.md)
## Problem
The Web conversation needs a visible, mutable session model selection sourced from the Host. Copying TUI presentation or hardcoding DeepSeek models in the browser would split model discovery and step-boundary semantics across front doors. A switch made while a response is running also needs one atomic boundary: prompt variables and request routing cannot observe different selections.
The Web conversation needs a visible, mutable session model selection sourced from the Host. Copying TUI presentation or hardcoding DeepSeek models in the browser would split model discovery and step-boundary semantics across front ends. A switch made while a response is running also needs one atomic boundary: prompt variables and request routing cannot observe different selections.
## Decision
@@ -6,13 +6,13 @@ Status: implemented
## 问题
Web 对话需要一项由 Host 提供、可见且可更改的会话模型选择。如果照搬 TUI 的呈现方式,或在浏览器中硬编码 DeepSeek 模型,就会让模型发现逻辑和步骤边界语义分散到不同前中。响应运行期间发生的切换还需要一个原子边界:提示词变量与请求路由不能观测到不同的选择。
Web 对话需要一项由 Host 提供、可见且可更改的会话模型选择。如果照搬 TUI 的呈现方式,或在浏览器中硬编码 DeepSeek 模型,就会让模型发现逻辑和步骤边界语义分散到不同前中。响应运行期间发生的切换还需要一个原子边界:提示词变量与请求路由不能观测到不同的选择。
## 决策
Web Host 为每个新建或恢复的 Agent 安装 `ModelSelection`。如果会话已经使用过模型,提供方/模型/推理(reasoning)选择来自最新的 `request/header`;否则来自 `ctx.agentDefaultModel``session.selectModel` 会赋值会话级选择,提示词组装则将它与请求路由一并捕获,因此运行中步骤发生的切换会应用于下一个组装步骤。下一个实际采用的选择通过完整的 `request/header` 快照持久化;尚未进入请求的选择则仅保存在当前进程中。
会话 RPC 领域公开 `session.models` 模型目录与 `session.selectModel`。该目录从 LLM(大语言模型)注册表动态构建,并按提供方分组;每个已列出模型的精确元数据还会加入由适配器持有的推理强度 ID、名称、说明和可选默认值。各提供方的目录与精确元数据会按提供方并发加载,且彼此独立失败,因此成功加载的分组仍可与可重试的失败记录一同使用。模型是否位于目录仅供参考:`session.models.current` 独立返回,即使不在任何分组中也仍然可以路由,但提供方停止公布该模型后,Host 不会合成未列出行。两个前对这一状态给出不同回答:TUI 把未列出的当前模型渲染为独立一行,Web 则显示未设置状态的触发器标签并要求选择替代模型。Web 是编辑目录的 surface,因此缺席的目录行代表一项待作出的选择;TUI 只从现有行中选择。显示未设置标签的 Web composer 仍可以使用当前可路由选择发送消息。精确解析决定提供方/模型组合与显式推理强度是否可用。选择操作通过 `resolveCallConfig` 拒绝不支持的推理强度 ID,并在赋值该选择前具体化适配器配置的默认值。
会话 RPC 领域公开 `session.models` 模型目录与 `session.selectModel`。该目录从 LLM(大语言模型)注册表动态构建,并按提供方分组;每个已列出模型的精确元数据还会加入由适配器持有的推理强度 ID、名称、说明和可选默认值。各提供方的目录与精确元数据会按提供方并发加载,且彼此独立失败,因此成功加载的分组仍可与可重试的失败记录一同使用。模型是否位于目录仅供参考:`session.models.current` 独立返回,即使不在任何分组中也仍然可以路由,但提供方停止公布该模型后,Host 不会合成未列出行。两个前对这一状态给出不同回答:TUI 把未列出的当前模型渲染为独立一行,Web 则显示未设置状态的触发器标签并要求选择替代模型。Web 是编辑目录所在的前端,因此缺席的目录行代表一项待作出的选择;TUI 只从现有行中选择。显示未设置标签的 Web composer 仍可以使用当前可路由选择发送消息。精确解析决定提供方/模型组合与显式推理强度是否可用。选择操作通过 `resolveCallConfig` 拒绝不支持的推理强度 ID,并在赋值该选择前具体化适配器配置的默认值。
浏览器中的 `ModelService` 为每个实时会话持有一个 `ModelDirectory`。其快照包含当前完整的 `ModelSelection`、分组目录、提供方失败记录、操作错误,以及 `idle``loading``ready``selecting``error` 状态。挂载时会预先填充触发器标签,此后每次打开菜单都会刷新目录。目录与选择调用共用操作代次,防止较早响应覆盖较新结果;连接重置会先丢弃当前进程中的投影,再恢复 Host 选择。失败时保留先前的选择和可用分组。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-01-pwsh-tool-and-executor.md
2026-08-01-pwsh-tool-and-executor.md: 7206f8ffe6640f8499f8453c40ab5846b23112c6
2026-08-01-pwsh-tool-and-executor.zh.md: c59ba8e3e6c68d48d310861325c74dc6cec8e5c3
2026-08-01-pwsh-tool-and-executor.md: 855d8e5db8a78e7e798061724d89fde31b28e975
2026-08-01-pwsh-tool-and-executor.zh.md: dcf5c53cd6681be07adafef92d556b400c116736
@@ -6,30 +6,30 @@ English | [中文](2026-08-01-pwsh-tool-and-executor.zh.md)
## Problem
The harness spoke one shell dialect on every platform: `bash`. Windows hosts could run it only through WSL or Git-Bash shims, and the shipped `dsh-bash-local` executor is POSIX-only (`bash` hardcoded, process-group semantics POSIX). The Windows roadmap — defaulting hosts to `pwsh`, later pwsh TUI/GUI rendering — had no execution foundation: there was no PowerShell implementation of the bash executor seam and no model-facing tool that taught the PowerShell dialect. The bash tool itself is also far larger than a Windows-first profile needs: background tasks, sandbox escalation, and the persistent-PTY twin are all bash-shaped surface that a minimal `pwsh` tool should not carry.
The harness spoke one shell dialect on every platform: `bash`. Windows hosts could run it only through WSL or Git-Bash shims, and the shipped `dsh-bash-local` executor is POSIX-only (`bash` hardcoded, process-group semantics POSIX). The Windows roadmap — defaulting hosts to `pwsh`, later pwsh TUI/GUI rendering — had no execution foundation: there was no PowerShell implementation of the bash executor seam and no model-facing tool that taught the PowerShell dialect. The bash tool is also larger than a Windows-first profile strictly needs — the persistent-PTY twin in particular is bash-shaped surface the `pwsh` tool still does not carry. The original minimal profile also left out background tasks and sandbox escalation: background arrived with the [parity decision](2026-08-02-pwsh-tool-bash-parity.md), and the sandbox surface (denial rendering plus `sandbox_permissions` escalation) arrived with the [Windows ACL sandbox decision](2026-08-08-windows-acl-restricted-token-sandbox.md) — the minimal tool was sized for the danger-full-access Windows posture, and that premise ended when the sandbox PR re-enabled confinement and approval on Windows.
## Decision
Two new packages under `packages/bash/`:
- **`@deepseek-ai/dsh-pwsh-local`** — a local implementation of the `ctx.bash` executor seam over `ctx.subprocess`, mirroring `dsh-bash-local` call-for-call: `resolve()` defaults and caps from config, `run()` fuses the config-clamped timeout with the caller's signal through one deadline, `start()` returns a consuming background handle whose processes belong to the subprocess service. The command string rides as ONE argv element to `pwsh -NoLogo -NoProfile -NonInteractive -Command`, so PowerShell parses it and no shell-quoting layer exists. Executable resolution (`resolvePwshPath`) is a pure function of `(configured, env, platform)`: explicit config first, then Windows probes PowerShell 7's install, PATH entries (quotes stripped), and Windows PowerShell 5.1, else a bare `pwsh` via PATH.
- **`@deepseek-ai/dsh-tool-pwsh`** — the model-facing tool over `ctx.bash`, PowerShell-dialect by contract, mirroring `dsh-tool-bash` call-for-call minus the sandbox surface: foreground and `run_in_background` execution through the generic task runtime, managed `DSH_*` environment through the shared [`dsh-bash-env`](../feature/2026-08-02-pwsh-tool-bash-parity.md) registry, and the bash marker/truncation rendering story (a clean exit produces no marker). The parity decision supersedes this note's minimal-profile tool description.
- **`@deepseek-ai/dsh-tool-pwsh`** — the model-facing tool over `ctx.bash`, PowerShell-dialect by contract, mirroring `dsh-tool-bash` call-for-call: foreground and `run_in_background` execution through the generic task runtime, managed `DSH_*` environment through the shared [`dsh-bash-env`](../feature/2026-08-02-pwsh-tool-bash-parity.md) registry, the bash marker/truncation rendering story (a clean exit produces no marker), and — since the Windows ACL sandbox decision — the sandbox denial rendering and `sandbox_permissions` escalation surface, plus the Windows-specific ConstrainedLanguage and named-pipe contracts in the tool description. The parity decision supersedes this note's minimal-profile tool description.
Windows vitest coverage is deliberately NOT part of this change: the repo's Windows CI lane owns build/static gates, and unit coverage runs on Linux, where both packages' suites run against a real `pwsh` (preinstalled on the GitHub-hosted runners) or self-skip when absent. The vitest `windowsUnsupportedPackages` exclusion narrows from `packages/bash/*` to the bash-requiring packages so the pwsh suites can also run natively on Windows dev machines.
The roadmap beyond this decision — defaulting Windows hosts to `pwsh` (bash off), and pwsh TUI/GUI rendering — is recorded separately as [a proposal](../../proposed/feature/2026-08-01-windows-pwsh-default.md).
The roadmap beyond this decision — defaulting Windows hosts to `pwsh` (bash off), and pwsh TUI/GUI rendering — is recorded separately as [the Windows pwsh default decision](2026-08-01-windows-pwsh-default.md).
## Alternatives considered
**Extend `dsh-bash-local` with a pwsh mode.** Rejected: the executor's identity is the shell it spawns; a second dialect inside one package doubles its config surface (`shell` switches) and its test matrix, and the two dialects' quirks (signal facts on Windows, quoting domains) belong to their own packages' documentation.
**Extend `dsh-tool-bash` with a dialect parameter.** Rejected: the bash tool's background/sandbox surface is bash-shaped; a `pwsh` mode would either hide it (conditional schema churn) or inherit it (surface the minimal profile explicitly rejects). The minimal twin keeps the model contract honest.
**Extend `dsh-tool-bash` with a dialect parameter.** Rejected: the model-visible contract is the dialect itself (paths, variables, exit facts differ), so a dialect parameter would either churn the schema conditionally or force one tool to teach two dialects; the separate twin keeps the model contract honest — and carries the shared surfaces (background, sandbox, rendering) by mirroring rather than by sharing an implementation.
**Wire the pwsh tool into the shipped CLI compositions now.** Rejected: mounting `tool-pwsh` + `pwsh-local` in `base.cordis.yml` would change the shipped roster before the Windows-default decision lands; this change ships the capability and its wiring points (`apps/cli` dependencies, tsconfig projects) without switching any default.
## Consequences
- The bash executor seam gains a second, Windows-native implementation with an identical request/spec contract, so model-facing consumers beyond `tool-pwsh` (hooks bridges, in-process plugins) can run PowerShell without dialect shims.
- `tool-pwsh` is the model-visible Windows-first shell tool: behaviorally interchangeable with the bash tool for foreground and background work (minus sandbox), with prompt guidance that states the marker contract precisely.
- `tool-pwsh` is the model-visible Windows-first shell tool: behaviorally interchangeable with the bash tool for foreground, background, and sandboxed work — including the same-turn `sandbox_permissions` escalation through `ctx.approval` — with prompt guidance that states the marker contract, the sandbox denial/escalation vocabulary, and the ConstrainedLanguage and named-pipe boundaries precisely.
- Windows semantics differ where the platform differs: forced termination reports exit 1 with no signal (so `signal`/`killed` status facts are POSIX-only), and PowerShell writes CRLF, which tests normalize.
- The CLI gains two workspace dependencies and two tsconfig projects without mounting either plugin — the composition decision stays with the Windows-default proposal.
@@ -6,30 +6,30 @@ Status: implemented
## 问题
harness 在每个平台只说一种 shell 方言:`bash`。Windows 主机只能通过 WSL 或 Git-Bash 垫片运行它,而交付的 `dsh-bash-local` 执行器仅限 POSIX(硬编码 `bash`,进程组语义是 POSIX 的)。Windows 路线图——让主机默认 `pwsh`,之后再做 pwsh TUI/GUI 渲染——没有执行基础:既没有 bash 执行器 seam 的 PowerShell 实现,也没有教模型 PowerShell 方言的面向模型工具。bash 工具本身也远大于 Windows 优先画像所需:后台任务、沙箱升级与持久 PTY 孪生是 bash 形状表面,最小化的 `pwsh` 工具不该背负
harness 在每个平台只说一种 shell 方言:`bash`。Windows 主机只能通过 WSL 或 Git-Bash 垫片运行它,而交付的 `dsh-bash-local` 执行器仅限 POSIX(硬编码 `bash`,进程组语义是 POSIX 的)。Windows 路线图——让主机默认 `pwsh`,之后再做 pwsh TUI/GUI 渲染——没有执行基础:既没有 bash 执行器 seam 的 PowerShell 实现,也没有教模型 PowerShell 方言的面向模型工具。bash 工具大于 Windows 优先画像的严格所需——尤其持久 PTY 孪生是 `pwsh` 工具至今仍不背负的 bash 形状表面。最初的最小画像也没有后台任务与沙箱升级:后台随 [parity 决策](2026-08-02-pwsh-tool-bash-parity.md) 到来,沙箱面(拒绝渲染加 `sandbox_permissions` 升级)随 [Windows ACL sandbox 决策](2026-08-08-windows-acl-restricted-token-sandbox.md) 到来——最小工具当初按 danger-full-access 的 Windows 姿态裁剪,这一前提在 sandbox PRPull Request)于 Windows 上重新启用隔离与审批时终结
## 决策
`packages/bash/` 下新增两个包:
- **`@deepseek-ai/dsh-pwsh-local`** —— `ctx.bash` 执行器 seam 的本地实现,基于 `ctx.subprocess`,逐调用镜像 `dsh-bash-local``resolve()` 从配置默认化并设上限,`run()` 通过一个 deadline 融合配置夹取的超时与调用方信号,`start()` 返回消费式后台句柄,其进程归属于 subprocess 服务。命令字符串作为 ONE argv 元素传给 `pwsh -NoLogo -NoProfile -NonInteractive -Command`,由 PowerShell 解析,不存在 shell 引号层。可执行文件解析(`resolvePwshPath`)是 `(configured, env, platform)` 的纯函数:先显式配置,再在 Windows 上探测 PowerShell 7 安装位置、PATH 条目(剥离引号)与 Windows PowerShell 5.1,否则经 PATH 解析裸 `pwsh`
- **`@deepseek-ai/dsh-tool-pwsh`** —— 基于 `ctx.bash` 的面向模型工具,约定是 PowerShell 方言,逐调用镜像 `dsh-tool-bash`、减去 sandbox 面:经通用任务运行时执行前台与 `run_in_background`,经共享 [`dsh-bash-env`](../feature/2026-08-02-pwsh-tool-bash-parity.md) 注册表管理 `DSH_*` 环境,以及 bash 的 marker/截断渲染故事(干净退出不产生 marker)。parity 决策取代了本 note 的最小画像工具描述。
- **`@deepseek-ai/dsh-tool-pwsh`** —— 基于 `ctx.bash` 的面向模型工具,约定是 PowerShell 方言,逐调用镜像 `dsh-tool-bash`:经通用任务运行时执行前台与 `run_in_background`,经共享 [`dsh-bash-env`](../feature/2026-08-02-pwsh-tool-bash-parity.md) 注册表管理 `DSH_*` 环境,bash 的 marker/截断渲染故事(干净退出不产生 marker),以及——自 Windows ACL sandbox 决策以来——沙箱拒绝渲染与 `sandbox_permissions` 升级面,外加工具描述中的 Windows 专属 ConstrainedLanguage 与 named-pipe 约定。parity 决策取代了本 note 的最小画像工具描述。
Windows vitest 覆盖率刻意不属本次改动:仓库的 Windows CI 通道负责构建/静态门禁,单元覆盖在 Linux 上运行,两个包的套件在那里以真实 `pwsh` 运行(GitHub 托管 runner 预装)或缺失时自行跳过。vitest 的 `windowsUnsupportedPackages` 排除从 `packages/bash/*` 收窄为真正需要 bash 的包,使 pwsh 套件也能在 Windows 开发机上原生运行。
本决策之后的路线图——让 Windows 主机默认 `pwsh`(关闭 bash)与 pwsh TUI/GUI 渲染——另行记录为[提案](../../proposed/feature/2026-08-01-windows-pwsh-default.md)。
本决策之后的路线图——让 Windows 主机默认 `pwsh`(关闭 bash)与 pwsh TUI/GUI 渲染——已落地为 [Windows 默认 pwsh 决策](2026-08-01-windows-pwsh-default.md)。
## 备选方案
**给 `dsh-bash-local` 增加 pwsh 模式。** 否决:执行器的身份就是它 spawn 的 shell;在一个包内塞第二种方言会翻倍配置面(`shell` 开关)与测试矩阵,且两种方言的怪癖(Windows 上的信号实情、引号域)应各自归入自己包的文档。
**给 `dsh-tool-bash` 增加方言参数。** 否决:bash 工具的后台/沙箱表面是 bash 形状的;`pwsh` 模式要么隐藏它(条件 schema 翻动,要么继承它(把最小画像明确拒绝的表面带进来)。最小孪生让模型约定保持诚实
**给 `dsh-tool-bash` 增加方言参数。** 否决:模型可见约定本身就是方言(路径、变量、退出事实都不同),因此方言参数要么让 schema 按条件翻动,要么逼一个工具教两种方言;独立的孪生让模型约定保持诚实——并以镜像而非共享实现的方式携带共享表面(后台、沙箱、渲染)
**现在就接入交付的 CLI 组合。** 否决:在 Windows 默认决策落地前把 `tool-pwsh` + `pwsh-local` 挂进 `base.cordis.yml` 会改变交付清单;本改动交付能力与接线点(`apps/cli` 依赖、tsconfig 工程),不切换任何默认。
## 后果
- bash 执行器 seam 有了第二个、Windows 原生的实现,请求/规范约定一致,因此 `tool-pwsh` 之外的面向模型消费方(hooks 桥、进程内插件)无需方言垫片即可运行 PowerShell。
- `tool-pwsh` 是模型可见的 Windows 优先 shell 工具:在前台后台工作(减 sandbox上与 bash 工具行为可互换,提示词指导精确陈述 marker 约定
- `tool-pwsh` 是模型可见的 Windows 优先 shell 工具:在前台后台与沙箱化工作上与 bash 工具行为可互换——包括经 `ctx.approval` 的同轮次 `sandbox_permissions` 升级——提示词指导精确陈述 marker 约定、沙箱拒绝/升级词汇,以及 ConstrainedLanguage 与 named-pipe 边界
- Windows 语义在平台差异处不同:强制终止报告退出码 1 且无信号(因此 `signal`/`killed` 状态实情仅限 POSIX),PowerShell 输出 CRLF,测试做归一化。
- CLI 增加两个 workspace 依赖与两个 tsconfig 工程,但不挂载任一插件——组合决策留给 Windows 默认提案。
@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-09-headless-direct-core-front-door.md
2026-08-09-headless-direct-core-front-door.md: f4604329a9276448a0021bb749b09e8c1b82e3c1
2026-08-09-headless-direct-core-front-door.zh.md: aaa1289894bf3c69b39aa863493dffdc3437ad01
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md
2026-08-01-windows-pwsh-default.md: f0da86e52bcdd53a10b60164d7cc12261cfc5c49
2026-08-01-windows-pwsh-default.zh.md: 41a6429eab8f86a8960ac4aa372aeacfda4661c4
@@ -0,0 +1,44 @@
# Agent Note: Windows defaults to pwsh
Status: implemented
English | [中文](2026-08-01-windows-pwsh-default.zh.md)
## Problem
The harness's shipped execution profile is bash-first on every platform. Windows hosts must install a bash shim (WSL or Git-Bash) or fall back to the POSIX-only `dsh-bash-local` behavior (hardcoded `bash -c` argv, process-group semantics); the model-facing bash tool teaches the bash dialect. The Windows-native foundation shipped in the [pwsh executor and tool decision](2026-08-01-pwsh-tool-and-executor.md) — a PowerShell implementation of the `ctx.bash` seam and a parity `pwsh` tool — but shipped compositions still mounted the bash stack on Windows, so a Windows host without a shim could not run the shipped shell.
## Decision
Windows hosts booting a shipped profile (`dsh web`, `dsh --profile headless`, one-shot tasks) get the PowerShell stack by default; POSIX hosts are unchanged.
- **The platform layer is a data file, not a roster rewrite.** `@deepseek-ai/dsh-base` ships [`windows.cordis.patch.yml`](../../../../packages/bundle/base/windows.cordis.patch.yml) alongside its universal `cordis.patch.yml`: it disables `bash-sandbox`/`tool-bash` (the POSIX-only executor and its dialect tool) and inserts `pwsh-local`/`tool-pwsh`. Windows has no OS sandbox runner (landlock/bwrap/seatbelt are POSIX-only), so the layer drops the sandbox stack entirely — `sandbox`, `sandbox-policy`, and `fs-sandbox` are disabled and the unconfined `dsh-fs-local` provides `ctx.fs` — and degrades to danger-full-access: `permission`/`ui-permission` leave the roster (dsh-permission requires a confining executor — presets bundle a sandbox mode the unconfined executor cannot honor; see its constructor guard — and the client knob would advertise a boundary that does not exist), and the `approval` service is disabled — nothing in the Windows roster asks for approval, so the model is never told approval exists or that asks are auto-rejected. Keeping fs-only path rules would be theater: the unconfined shell can bypass them with one command, so the honest Windows posture is full access rather than a boundary only the fs tools pretend to enforce.
- **The launcher injects the layer by platform.** `apps/cli/src/windows-shell.ts` resolves it from the base bundle layer's `packageDir` between the bundle layers and the user layers on `win32` hosts, in every composition path (boot, config-only HMR recomposition, config dumps). Overriding the shipped default is a composition decision: a Windows host that prefers the bash stack — or confinement — re-enables the bash rows through its profile or home `cordis.patch.yml`. Custom profiles without the base bundle are skipped (they own their shell stack); a base bundle that ships no Windows shell patch fails loud.
- **Module resolution is restored for cold starts.** The profiles-rework CLI dropped the pwsh packages from `apps/cli`'s dependency closure, so `healProfilesModuleFallback` never linked them into `$DSH_HOME/profiles/node_modules` and a fresh Windows host could not resolve the inserted rows. `apps/cli` and `dsh-base` re-declare `dsh-pwsh-local`/`dsh-tool-pwsh`, and `dsh-base` also declares `dsh-fs-local`; the base bundle lists every row plugin as a dependency by house style.
The pwsh GUI rendering shipped earlier with the [pwsh UI presentation matches bash decision](2026-08-05-pwsh-ui-bash-parity.md); the [pwsh tool bash parity decision](2026-08-02-pwsh-tool-bash-parity.md) ships the tool's surface. Nothing in this decision changes POSIX behavior.
## Alternatives considered
**Default Windows to pwsh inside `dsh-bash-local` (one executor, dialect switch).** Rejected for the same reason the executor decision rejected a mode switch: the executor's identity is the shell it spawns, and platform-gated composition is a deployment choice, not an executor config.
**Ship the platform layer from `apps/cli` code instead of a bundle data file.** Rejected: the patch belongs next to the rows it replaces, in the bundle that owns them, so the shipped roster stays visible as composition data and dumps carry its provenance; the launcher contributes only the win32 gate.
**Keep `permission`/`ui-permission` on Windows.** Rejected: `dsh-permission` hard-requires `ctx.bash.sandboxMode` and fails loud at load over an unconfined executor; making it tolerate an unconfined shell would advertise presets the shell cannot honor.
**Keep fs path-rule confinement on Windows (`sandbox-policy` + `fs-sandbox` without OS runners).** Rejected: the shell is the model's primary tool and unconfined on Windows, so fs-only path rules are trivially bypassable and would overstate the boundary; the honest posture is full degradation to danger-full-access.
**Ship a `DSH_WINDOWS_SHELL` environment escape hatch.** Rejected: decisive behavior changes belong in composition config, which already overrides the platform layer row by id; a second override channel would split the single source of truth for roster decisions.
## Consequences
- A Windows host running a shipped `dsh` surface gets `pwsh` as its shell tool and PowerShell as the `ctx.bash` executor without configuration; `bash` is absent from the model-visible roster there (its tool row is disabled).
- Windows has no sandbox at all: the fs tools run unconfined (`dsh-fs-local`), the approval service is absent (nothing asks for approval, and the model is never told approval exists), and the permission switcher is gone. The model-visible posture is honest full access rather than a boundary the shell can bypass.
- POSIX hosts are unchanged: the platform layer never applies, and the bash stack remains the universal `cordis.patch.yml` rows.
- Windows hosts that prefer the bash stack (e.g. with WSL/Git-Bash on PATH) override the shipped default through their profile or home `cordis.patch.yml` — disabling `pwsh-local`/`tool-pwsh` and re-enabling `bash-sandbox`/`tool-bash` (both executors register the same `bash` service, so an incomplete recipe fails loud at load) — composition config is the one override channel.
## Verification
- Unit: `apps/cli/tests/windows-shell.spec.ts` pins the win32 default, the custom-profile skip, and the missing-patch failure with the platform injected, and composes the REAL shipped bundle layers (dsh-base + dsh-web-app resolved from the app installation) through the boot's patch algorithm to assert the win32 danger-full-access roster and the base-only-profile warning; `packages/bundle/base/tests/base.spec.ts` pins the shipped Windows patch file shape (disables, inserts, and the absent approval service).
- Keyless: a win32 `dsh --profile <name> --dump-config` shows the pwsh rows with `windows.cordis.patch.yml` provenance and the bash rows disabled; the POSIX dump (CI Linux) is unchanged.
- The real-composition smoke boots the web profile on win32 with the pwsh stack mounted (the exact roster this note describes).
@@ -0,0 +1,44 @@
# Agent Note: Windows 默认改用 pwsh
Status: implemented
[English](2026-08-01-windows-pwsh-default.md) | 中文
## 问题
harness 交付的执行画像在每个平台都是 bash 优先。Windows 主机必须安装 bash 垫片(WSL 或 Git-Bash),或退回到仅 POSIX 的 `dsh-bash-local` 行为(硬编码 `bash -c` argv、进程组语义);面向模型的 bash 工具教的是 bash 方言。Windows 原生基础已随 [pwsh 执行器与工具决策](2026-08-01-pwsh-tool-and-executor.md) 交付——`ctx.bash` seam 的 PowerShell 实现与对等的 `pwsh` 工具——但交付组合在 Windows 上仍然挂载 bash 栈,没有垫片的 Windows 主机跑不了交付的 shell。
## 决策
启动交付 profile`dsh web``dsh --profile headless`、一次性任务)的 Windows 主机默认获得 PowerShell 栈;POSIX 主机不变。
- **平台层是数据文件,不是清单重写。** `@deepseek-ai/dsh-base` 随通用 `cordis.patch.yml` 一起交付 [`windows.cordis.patch.yml`](../../../../packages/bundle/base/windows.cordis.patch.yml):它禁用 `bash-sandbox`/`tool-bash`(仅 POSIX 的执行器及其方言工具)并插入 `pwsh-local`/`tool-pwsh`。Windows 上没有 OS 级 sandbox runnerlandlock/bwrap/seatbelt 均为 POSIX 专属),因此该层整体移除 sandbox 栈——`sandbox``sandbox-policy``fs-sandbox` 被禁用,由不限权的 `dsh-fs-local` 提供 `ctx.fs`——并完全退化为 danger-full-access`permission`/`ui-permission` 离开清单(dsh-permission 要求有限权能力的执行器——preset 捆绑的是无限制执行器无法兑现的 sandbox 模式;见其构造函数守卫——客户端旋钮会宣传一个并不存在的边界),`approval` 服务也被禁用——Windows 清单里没有任何动作需要审批,模型也不会被告知"审批存在"或"请求会被自动拒绝"。保留仅限 fs 的路径规则是摆设:不限权的 shell 一条命令即可绕过,因此诚实的 Windows 姿态是全权访问,而不是一个只有 fs 工具假装执行的边界。
- **启动器按平台注入该层。** `apps/cli/src/windows-shell.ts``win32` 主机上从 base bundle 层的 `packageDir` 解析它,置于 bundle 层与用户层之间,覆盖所有组合路径(启动、config-only HMR 重组合、配置转储)。覆盖交付默认是组合决策:偏好 bash 栈(或偏好有限权)的 Windows 主机通过其 profile 或 home 的 `cordis.patch.yml` 重新启用 bash 行。未挂 base bundle 的自定义 profile 被跳过(它们自己拥有 shell 栈);base bundle 缺 `windows.cordis.patch.yml` 时 fail loud。
- **冷启动的模块解析已恢复。** profiles 重构把 pwsh 包从 `apps/cli` 的依赖闭包中删掉了,`healProfilesModuleFallback` 因此从未把它们链接进 `$DSH_HOME/profiles/node_modules`,新 Windows 主机解析不到插入的行。`apps/cli``dsh-base` 重新声明 `dsh-pwsh-local`/`dsh-tool-pwsh``dsh-base` 还声明 `dsh-fs-local`;按仓库惯例,base bundle 把每个行插件都列为依赖。
pwsh GUI 渲染已随 [pwsh UI 呈现与 bash 对齐决策](2026-08-05-pwsh-ui-bash-parity.md) 先行交付;[pwsh 工具与 bash 对齐决策](2026-08-02-pwsh-tool-bash-parity.md) 交付了工具表面。本决策不改变任何 POSIX 行为。
## 备选方案
**在 `dsh-bash-local` 内部让 Windows 默认 pwsh(一个执行器,方言开关)。** 否决,理由与执行器决策否决模式开关相同:执行器的身份就是它 spawn 的 shell,而按平台门控的组合是部署选择,不是执行器配置。
**从 `apps/cli` 代码而非 bundle 数据文件交付平台层。** 否决:patch 应放在它替换的行旁边、属于拥有这些行的 bundle,让交付清单作为组合数据保持可见、转储带有出处;启动器只贡献 win32 门控。
**在 Windows 上保留 `permission`/`ui-permission`。** 否决:`dsh-permission` 硬性要求 `ctx.bash.sandboxMode`,在无限制执行器上加载即 fail loud;让它容忍无限制 shell 会宣传 shell 无法兑现的 preset。
**在 Windows 上保留 fs 路径规则限制(无 OS runner 的 `sandbox-policy` + `fs-sandbox`)。** 否决:shell 是模型的主工具且在 Windows 上不限权,仅限 fs 的路径规则一行命令即可绕过,会夸大边界;诚实的姿态是完全退化到 danger-full-access。
**交付 `DSH_WINDOWS_SHELL` 环境变量逃生门。** 否决:决定性的行为变更应集中在组合配置中,而组合配置已能按行 id 覆盖平台层;第二条覆盖通道会分裂清单决策的单一事实来源。
## 后果
- 运行交付版 `dsh` 表面的 Windows 主机无需配置即获得 `pwsh` 作为 shell 工具、PowerShell 作为 `ctx.bash` 执行器;那里的模型可见清单中没有 `bash`(其工具行被禁用)。
- Windows 上没有任何沙箱:fs 工具不限权运行(`dsh-fs-local`)、`approval` 服务不存在(没有任何动作需要审批,模型也不会被告知审批存在)、权限切换器消失。模型可见的姿态是诚实的全权访问,而不是一个 shell 可以绕过的边界。
- POSIX 主机不变:平台层永不生效,bash 栈仍是通用 `cordis.patch.yml` 的行。
- 偏好 bash 栈的 Windows 主机(例如 PATH 上有 WSL/Git-Bash 时)通过其 profile 或 home 的 `cordis.patch.yml` 覆盖交付默认——禁用 `pwsh-local`/`tool-pwsh` 并重新启用 `bash-sandbox`/`tool-bash`(两个执行器注册同一个 `bash` 服务,配方不完整会在加载时 fail loud)——组合配置是唯一的覆盖通道。
## 验证
- 单元:`apps/cli/tests/windows-shell.spec.ts` 以平台注入固定 win32 默认、自定义 profile 跳过与缺文件失败,并通过启动所用的 patch 算法组合真实交付的 bundle 层(从应用安装解析的 dsh-base + dsh-web-app)断言 win32 danger-full-access 清单与 base-only profile 警告;`packages/bundle/base/tests/base.spec.ts` 固定交付的 Windows patch 文件形状(禁用、插入与缺席的 approval 服务)。
- Keylesswin32 上的 `dsh --profile <name> --dump-config` 显示带 `windows.cordis.patch.yml` 出处的 pwsh 行、被禁用的 bash 行;POSIX 转储(CI Linux)不变。
- 真实组合冒烟在 win32 上启动 web profile,pwsh 栈挂载成功(即本笔记描述的确切清单)。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-02-pwsh-tool-bash-parity.md
2026-08-02-pwsh-tool-bash-parity.md: d61dd6f21223121973520014c44e2e5846e7387a
2026-08-02-pwsh-tool-bash-parity.zh.md: 120f66b1d9c713d5e3b71575186fc2194b0718a5
2026-08-02-pwsh-tool-bash-parity.md: 79a09cb4c9698660faff18cf9ae016bec0bce227
2026-08-02-pwsh-tool-bash-parity.zh.md: 3e01fd4447fbf047b7cbb3949e708094a6c06c09
@@ -10,13 +10,13 @@ The first Windows-native foundation shipped `dsh-tool-pwsh` as a deliberately mi
## Decision
`dsh-tool-pwsh` now mirrors `dsh-tool-bash` call-for-call, minus the sandbox surface, and its model-visible text describes exactly that behavior:
`dsh-tool-pwsh` now mirrors `dsh-tool-bash` call-for-call, and its model-visible text describes exactly that behavior:
- **Rendering adopts the bash story verbatim**: stdout, a marked `[stderr]` section, truncation notices with spill paths, `(no output)` for an empty body, and exit markers only for non-zero exits — a clean exit produces no marker. The description and the `tool:pwsh` prompt section state this precisely ("Non-zero exits are reported as `[exit code: N]` markers"), deliberately not copying the bash prompt's "every result" phrasing, which its own renderer contradicts.
- **`run_in_background` is wired through the generic task runtime** exactly like the bash tool: preflight, owner registration, `task_output`/`task_kill` control, and the same outcome mapping. `pwsh-local`'s already-mirrored `start()` handle backs it.
- **The `DSH_*` environment is shared, not duplicated**: `BashEnvRegistry` moved out of `dsh-tool-bash` into a new tool-independent `@deepseek-ai/dsh-bash-env` package (`ctx.bashEnv` + built-ins + the session-persistence contributor), and both shell tools inject it. Contributors apply to pwsh calls exactly as they do to bash calls; shared environment ownership therefore sits outside either model-facing shell tool.
- **Windows reality is pinned where bash has no analog**: every command runs under a UTF-8 output preamble so the Windows PowerShell 5.1 fallback cannot garble non-ASCII output through the UTF-8-decoding collector, and the prompts teach that Windows forced termination settles as exit 1 without a signal marker.
- **Out of scope, unchanged**: sandbox escalation (waits for a Windows-confining executor) and persistent PTY shells (backends are Linux/macOS-only; ConPTY is roadmap work). The pwsh-specific terminal card with an exit pill shipped separately in the [pwsh UI presentation matches bash](2026-08-05-pwsh-ui-bash-parity.md) decision.
- **Out of scope, unchanged**: persistent PTY shells (backends are Linux/macOS-only; ConPTY is roadmap work). Sandbox escalation shipped later with the [Windows ACL sandbox decision](2026-08-08-windows-acl-restricted-token-sandbox.md) — the pwsh tool now carries the sandbox denial rendering and the same-turn `sandbox_permissions` escalation surface, plus the Windows ConstrainedLanguage contract in its description. The pwsh-specific terminal card with an exit pill shipped separately in the [pwsh UI presentation matches bash](2026-08-05-pwsh-ui-bash-parity.md) decision.
## Alternatives considered
@@ -28,7 +28,7 @@ The first Windows-native foundation shipped `dsh-tool-pwsh` as a deliberately mi
## Consequences
- The bash and pwsh tools are now behaviorally interchangeable for foreground and background shell work (minus sandbox), and the pwsh prompt/description sentences are each backed by the renderer.
- The bash and pwsh tools are now behaviorally interchangeable for foreground, background, and sandboxed shell work (the sandbox surface arrived with the Windows ACL sandbox decision), and the pwsh prompt/description sentences are each backed by the renderer — the reviewer's grep-against-code check passes.
- Parity ran BOTH ways once: the pwsh tool's structured foreground abort (`HarnessError('tool call aborted', TOOL_ABORTED)` with name `AbortError`) was backported to the bash tool, replacing its uncoded `Error('command aborted')` — a model-visible/logged change pinned by exact-shape tests on both sides and by the cancel-tool-calls fixture.
- `@deepseek-ai/dsh-bash-env` is a new shipped package; `dsh-tool-bash`'s `dshHome` config moved there, so compositions mounting the shell tools must also mount `bash-env` (the spine bundles do).
- Windows-only semantics (CRLF normalization, forced-termination exit-1/signal-null, POSIX-only self-signal) remain pinned by tests as before.
@@ -10,13 +10,13 @@ Status: implemented
## 决策
`dsh-tool-pwsh` 现在逐调用镜像 `dsh-tool-bash`减去 sandbox 面,其模型可见文本精确描述这一行为:
`dsh-tool-pwsh` 现在逐调用镜像 `dsh-tool-bash`,其模型可见文本精确描述这一行为:
- **渲染完全采用 bash 故事**:stdout、带标记的 `[stderr]` 段、带 spill 路径的截断通知、空体渲染 `(no output)`、退出 marker 仅限非零退出——干净退出不产生 marker。描述与 `tool:pwsh` prompt section 精确陈述这一点("Non-zero exits are reported as `[exit code: N]` markers"),刻意不复制 bash prompt 中与其自身渲染矛盾的 "every result" 措辞。
- **`run_in_background` 经通用任务运行时接线**,与 bash 工具完全一致:预检、owner 注册、`task_output`/`task_kill` 控制与相同的结果映射。其背后是 `pwsh-local` 早已镜像好的 `start()` 句柄。
- **`DSH_*` 环境共享而非复制**`BashEnvRegistry``dsh-tool-bash` 迁入新的工具无关包 `@deepseek-ai/dsh-bash-env``ctx.bashEnv` + 内置事实 + session-persistence contributor),两个 shell 工具都注入它。contributor 对 pwsh 调用与 bash 调用一视同仁;因此,共享环境的所有权不属于任何一个面向模型的 shell 工具。
- **Windows 现实在 bash 无对应处钉死**:每条命令都在 UTF-8 输出 preamble 下运行,使 Windows PowerShell 5.1 兜底无法经 UTF-8 解码的 collector 破坏非 ASCII 输出;prompt 教授 Windows 强制终止以无 signal 的 exit 1 结算。
- **范围外,不变**sandbox 升级(等待 Windows-confining 执行器)与持久 PTY shell(后端仅限 Linux/macOSConPTY 属路线图)。带退出 pill 的 pwsh 专属 terminal 卡已随 [pwsh UI 呈现与 bash 对齐](2026-08-05-pwsh-ui-bash-parity.md) 决策另行交付。
- **范围外,不变**:持久 PTY shell(后端仅限 Linux/macOSConPTY 属路线图)。sandbox 升级随 [Windows ACL sandbox 决策](2026-08-08-windows-acl-restricted-token-sandbox.md) 稍后交付——pwsh 工具现在携带 sandbox 拒绝渲染与同轮次 `sandbox_permissions` 升级面,外加其描述中的 Windows ConstrainedLanguage 契约。带退出 pill 的 pwsh 专属 terminal 卡已随 [pwsh UI 呈现与 bash 对齐](2026-08-05-pwsh-ui-bash-parity.md) 决策另行交付。
## 备选方案
@@ -28,7 +28,7 @@ Status: implemented
## 后果
- bash 与 pwsh 工具在前台后台 shell 工作(减 sandbox上行为可互换pwsh 的 prompt/描述句每句都有渲染器背书。
- bash 与 pwsh 工具在前台后台与沙箱化 shell 工作上行为可互换(sandbox 面随 Windows ACL sandbox 决策到来),pwsh 的 prompt/描述句每句都有渲染器背书——reviewer 的“拿代码 grep 对证”检查通过
- 对齐也反向发生过一次:pwsh 工具的结构化前台中止(`HarnessError('tool call aborted', TOOL_ABORTED)`name 为 `AbortError`)被回移到 bash 工具,取代其无码的 `Error('command aborted')`——这是模型可见/入日志的变更,由两侧的精确形状测试与 cancel-tool-calls fixture 钉住。
- `@deepseek-ai/dsh-bash-env` 成为新的交付包;`dsh-tool-bash``dshHome` 配置迁往那里,因此挂载 shell 工具的组合也必须挂载 `bash-env`spine bundle 已如此)。
- Windows 专属语义(CRLF 归一化、强制终止 exit-1/signal-null、仅 POSIX 的自信号)一如既往由测试钉住。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-05-pwsh-ui-bash-parity.md
2026-08-05-pwsh-ui-bash-parity.md: ba92f482e957acd00792a2a8053716047a4da27d
2026-08-05-pwsh-ui-bash-parity.zh.md: 693b3fc26e632718ea2135282d63daa32e63e1c5
2026-08-05-pwsh-ui-bash-parity.md: a59ae95ab64ae28babc913958a1a6ba424898210
2026-08-05-pwsh-ui-bash-parity.zh.md: 8e065640e56bf6d6b92e62cb746fa9dd53f6f2b5
@@ -6,7 +6,7 @@ English | [中文](2026-08-05-pwsh-ui-bash-parity.zh.md)
## Problem
The [pwsh tool bash parity decision](../../implemented/feature/2026-08-02-pwsh-tool-bash-parity.md) made `dsh-tool-pwsh` behaviorally interchangeable with `dsh-tool-bash` for execution, markers, and background tasks, but explicitly deferred the human-visible half: a completed pwsh foreground call presented as a generic `console`-fenced card while the bash tool's completed call presented as a terminal card with a parsed exit-status pill. The roadmap that owned this gap ([Windows defaults to pwsh](../../proposed/feature/2026-08-01-windows-pwsh-default.md)) named "pwsh TUI/GUI rendering" as stage 2, but the TUI package was removed, leaving the Web surface as the only UI the gap affects.
The [pwsh tool bash parity decision](../../implemented/feature/2026-08-02-pwsh-tool-bash-parity.md) made `dsh-tool-pwsh` behaviorally interchangeable with `dsh-tool-bash` for execution, markers, and background tasks, but explicitly deferred the human-visible half: a completed pwsh foreground call presented as a generic `console`-fenced card while the bash tool's completed call presented as a terminal card with a parsed exit-status pill. The roadmap that owned this gap ([Windows defaults to pwsh](../../implemented/feature/2026-08-01-windows-pwsh-default.md)) named "pwsh TUI/GUI rendering" as stage 2, but the TUI package was removed, leaving the Web surface as the only UI the gap affects.
## Decision
@@ -6,7 +6,7 @@ Status: implemented
## Problem
[pwsh 工具与 bash 对齐决策](../../implemented/feature/2026-08-02-pwsh-tool-bash-parity.md) 让 `dsh-tool-pwsh` 在执行、marker 与后台任务上行为可互换,但明确推迟了面向人类的一半:完成的 pwsh 前台调用呈现为通用 `console` 围栏卡片,而 bash 工具的完成调用呈现为带解析退出状态 pill 的 terminal 卡。拥有此缺口的路线图([Windows 默认改用 pwsh](../../proposed/feature/2026-08-01-windows-pwsh-default.md))把 "pwsh TUI/GUI 渲染" 列为阶段 2,但 TUI 包已被移除,使 Web 表面成为该缺口唯一影响的 UI。
[pwsh 工具与 bash 对齐决策](../../implemented/feature/2026-08-02-pwsh-tool-bash-parity.md) 让 `dsh-tool-pwsh` 在执行、marker 与后台任务上行为可互换,但明确推迟了面向人类的一半:完成的 pwsh 前台调用呈现为通用 `console` 围栏卡片,而 bash 工具的完成调用呈现为带解析退出状态 pill 的 terminal 卡。拥有此缺口的路线图([Windows 默认改用 pwsh](../../implemented/feature/2026-08-01-windows-pwsh-default.md))把 "pwsh TUI/GUI 渲染" 列为阶段 2,但 TUI 包已被移除,使 Web 表面成为该缺口唯一影响的 UI。
## Decision
@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-06-web-queue-steer-all-gesture.md
2026-08-06-web-queue-steer-all-gesture.md: e546f68647dfc9b91ce4699cef4a64694ebc4f76
2026-08-06-web-queue-steer-all-gesture.zh.md: fb36852f66a86408af12df59001230b2024eccaf
@@ -0,0 +1,33 @@
# Agent Note: Steer the whole Web queue with an empty-draft Cmd/Ctrl+Enter
Status: implemented
English | [中文](2026-08-06-web-queue-steer-all-gesture.zh.md)
## Problem
While a primary session runs, the Web queue accumulates messages the user typed with plain Enter (or queued while the busy-Enter preference was Queue). Flushing them into the current turn required clicking the per-row 插话发送 button once per message; an empty composer draft had no keyboard gesture at all — the input machine rejects empty drafts, so Enter and Cmd/Ctrl+Enter were both no-ops. With several queued messages, steering them one by one is the obvious multi-click friction, and the empty-draft accelerated chord is the natural slot for "steer everything".
## Decision
Empty-draft Cmd/Ctrl+Enter now steers every still-pending `queued`-placement inbox row into the running turn, in FIFO order, on a primary session that reports running. The gesture decodes in `InputBar.onKeyDown`: accelerated Enter with a trimmed-empty draft, `running`, no subagent address, and at least one `queued` row calls the new `ComposerKeyboard.steerQueue()` verb instead of `submit()`. `SessionInputShell.steerQueue()` delegates to a hub-wired choreography that re-reads the authoritative `session/queue` snapshot, filters `placement: 'queued'` (pending steering rows are already in the turn), and applies the queue dock's exact strict-steer operation — `session.updateQueue(itemId, { kind: 'steer' })` — sequentially, so FIFO ordering is guaranteed at the host. A `steer-unavailable` (turn closed mid-flush) or `queue-item-not-found` (row claimed meanwhile) converges silently; any other failure surfaces one composer notice (`插话发送失败,请重试。`). No wire, on-disk, or agent-loop change: the host already owns the strict-steer boundary.
The gesture is strictly the accelerated chord. Plain Enter with an empty draft stays a no-op even under the busy-Enter Steer preference, draft content outranks the queue (accelerated Enter steers only the draft), and idle or subagent sessions keep the existing empty-draft no-op because steering has no live turn to enter.
The same computed availability gate drives discovery: while the draft is empty, the input is unlocked and not in a transient machine lock, the command menu is closed, an ordinary primary session is running, and at least one row remains `queued`, the textarea placeholder advertises that Cmd/Ctrl+Enter steers all queued messages. An owner-supplied placeholder still takes precedence, and the steer hint deliberately outranks the plan-mode placeholder while available (the gesture genuinely works in that window).
## Consequences
One keyboard gesture now replaces N clicks while keeping a single strict-steer path and a single authority for convergence. The per-row button and the gesture are the same host operation, so races and failure semantics stay identical. The gesture and its placeholder share one presentation-layer gate, while the hub re-checks the snapshot at execution time, so the client gate remains advisory and the host remains authoritative.
## Related
The per-row 插话发送 action and its strict-steer boundary are owned by [Steer a queued Web message into the active turn](../feature/2026-07-30-web-queue-steer-action.md); this note only adds the whole-queue keyboard gesture on top of that decision.
## Alternatives considered
- **Intercepting inside the input machine.** Rejected: the machine is queue-agnostic by design (the wiring layer overlays the queue projection) and cannot distinguish the accelerated chord from plain Enter, which must stay a no-op.
- **Steering via `session.prompt(mode: 'steer')` per row.** Rejected: that mints new messages instead of transferring the pending occurrences and would split the dock's immutable-message contract; `updateQueue({ kind: 'steer' })` already atomically transfers the exact occurrence.
- **Firing all row steers concurrently.** Rejected: arrival order at the host is not guaranteed, and steering order is model-visible; sequential awaits preserve FIFO.
- **A new host RPC for steer-all.** Rejected: the existing per-item operation is idempotent enough — each row is one strict steer, and mid-flush closure converges silently — so a protocol change buys nothing.
- **A send-button tooltip.** Rejected: the primary button is Stop while an ordinary session is running, which is the only window where the whole-queue gesture is available. The empty-draft placeholder occupies that exact window and can describe the keyboard action directly.
@@ -0,0 +1,33 @@
# Agent Note: 空输入时 Cmd/Ctrl+Enter 将 Web 排队消息全部插话
Status: implemented
[English](2026-08-06-web-queue-steer-all-gesture.md) | 中文
## Problem
主会话运行时,用户用普通 Enter(或在 busy-Enter 偏好为 Queue 时)输入的消息会在 Web 队列里累积。把它们灌进当前轮次需要逐条点击「插话发送」按钮;而输入框草稿为空时没有任何键盘手势——输入机对空草稿直接拒绝,Enter 与 Cmd/Ctrl+Enter 都是空操作。排队消息一多,逐条插话是明显的多点摩擦,空草稿 + 加速 Enter 正是「全部插话」的自然位置。
## Decision
空草稿的 Cmd/Ctrl+Enter 现在会把仍在排队(`placement: 'queued'`)的 Inbox 行按 FIFO 顺序全部插话进运行中的轮次,仅限报告 running 的主会话。手势在 `InputBar.onKeyDown` 解码:加速 Enter + 去空白后为空草稿 + `running` + 无 subagent 地址 + 至少一条 `queued` 行时,改走新的 `ComposerKeyboard.steerQueue()` 动词而不是 `submit()``SessionInputShell.steerQueue()` 委托给 hub 编排的流程:重新读取权威的 `session/queue` 快照,过滤 `placement: 'queued'`pending steering 行已经在本轮内),并逐条顺序执行 Queue 面板的严格 steer 操作 `session.updateQueue(itemId, { kind: 'steer' })`,从而在 host 侧保证 FIFO 顺序。`steer-unavailable`flush 中途轮次关闭)或 `queue-item-not-found`(行已被占用)静默收敛;其他失败弹出一条 composer 通知(「插话发送失败,请重试。」)。无 wire、磁盘或 agent-loop 改动:严格 steer 边界本来就在 host 侧。
该手势严格限定为加速组合键。空草稿 + 普通 Enter 仍然无操作(即使 busy-Enter 偏好为 Steer);草稿内容优先于队列(加速 Enter 只插话当前草稿);idle 或 subagent 会话保持原有空草稿无操作,因为没有可插入的运行中轮次。
同一套计算得出的可用性门控也负责提示该手势:当草稿为空、输入框未锁定且不处于瞬态机器锁(adjudicating/submitting)、命令菜单未打开、普通主会话正在运行且至少一行仍为 `queued` 时,文本框 placeholder 会提示 Cmd/Ctrl+Enter 将全部排队消息插话发送。owner 提供的 placeholder 仍然优先;可用时 steer 提示会刻意优先于 plan 模式 placeholder(该窗口内手势确实可用)。
## Consequences
一个键盘手势替代 N 次点击,同时保持单一严格 steer 路径与单一收敛权威。逐条按钮与手势是同一个 host 操作,竞态与失败语义完全一致。手势及其 placeholder 共用一个呈现层门控;hub 在执行时会重新读取快照,因此客户端门控仍只是建议性的,host 仍是权威。
## Related
逐条「插话发送」动作及其严格 steer 边界由 [Steer a queued Web message into the active turn](../feature/2026-07-30-web-queue-steer-action.md) 记录;本笔记只在其之上增加整队列键盘手势。
## Alternatives considered
- **在输入机内拦截。** 已拒绝:输入机按设计不感知队列(队列投影由 wiring 层叠加),且无法区分加速 Enter 与必须保持空操作的普通 Enter。
- **逐条用 `session.prompt(mode: 'steer')` 插话。** 已拒绝:那会铸造新消息而不是转移 pending 行,破坏 dock 的不可变消息契约;`updateQueue({ kind: 'steer' })` 已经原子地转移了确切的那条。
- **并发触发所有行。** 已拒绝:host 到达顺序无法保证,而插话顺序对模型可见;顺序 await 保证 FIFO。
- **为 steer-all 新增 host RPC。** 已拒绝:现有逐条操作已足够幂等——每行一次严格 steer,中途关闭静默收敛——协议改动没有收益。
- **发送按钮 tooltip。** 已拒绝:普通会话运行时,主按钮是 Stop,这也是整队列手势唯一可用的窗口。空草稿时的 placeholder 恰好在该窗口显示,可以直接说明这项键盘操作。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-07-default-model-follows-the-picker.md
2026-08-07-default-model-follows-the-picker.md: ed7e7a424d2cacadea890506fd9150ffdf7a993c
2026-08-07-default-model-follows-the-picker.zh.md: 523c6f917dedf21c726ce5631226ba545126c757
2026-08-07-default-model-follows-the-picker.md: 2a3ada55486345c0f58f0767bed5a93ecba04b88
2026-08-07-default-model-follows-the-picker.zh.md: 08fecc6ec9b177f6424ada3172c67018ac72baea
@@ -6,13 +6,13 @@ English | [中文](2026-08-07-default-model-follows-the-picker.zh.md)
## Problem
A session model picker and a deployment default are two layers of the same preference. If the picker affects only its addressed session, the next blank session can select a different model with no user-facing way to align the default. If the default lives inside a Host gateway, direct Agent front doors cannot share it without depending on Host or duplicating state.
A session model picker and a deployment default are two layers of the same preference. If the picker affects only its addressed session, the next blank session can select a different model with no user-facing way to align the default. If the default lives inside a Host gateway, direct Agent entry points cannot share it without depending on Host or duplicating state.
Reasoning effort makes the persistence shape significant: a model selection without an effort must clear a stored effort, or the next Agent may apply an effort that its selected model does not accept.
## Decision
`AgentDefaultModelService` provides `ctx.agentDefaultModel` and registers `{provider, model, reasoningEffort?}` as the `agent-default-model` Settings section. Its `{provider, model}` composition entry is the base layer and `settings.yaml` supplies the user layer. The service is front-door-neutral, so direct creation and ApiProxy-backed creation share one default ([headless direct core front door](../architecture/2026-08-09-headless-direct-core-front-door.md)). `workspaceRoot` remains ApiProxy config because it is a Host launcher fact rather than model state.
`AgentDefaultModelService` provides `ctx.agentDefaultModel` and registers `{provider, model, reasoningEffort?}` as the `agent-default-model` Settings section. Its `{provider, model}` composition entry is the base layer and `settings.yaml` supplies the user layer. The service is entry-point-neutral, so direct creation and ApiProxy-backed creation share one default ([headless direct core entry point](../architecture/2026-08-09-headless-direct-core-entry-point.md)). `workspaceRoot` remains ApiProxy config because it is a Host launcher fact rather than model state.
`reasoningEffort` belongs to the Settings section but not to the plugin config. Settings layers merge by field, so a configured effort would survive a user selection that omits it. `saveSelection()` instead writes the complete user section; absence therefore clears a stored effort. A deployment-wide effort default belongs to the adapter profile, which resolves it per model.
@@ -6,13 +6,13 @@ Status: implemented
## 问题
会话模型选择器与部署默认值是同一项偏好的两个层次。如果选择器只影响其所在会话,下一个空白会话可能选择不同模型,用户却没有途径使默认值与选择器一致。如果默认值位于 Host 网关内部,直接创建 Agent 的前门只有依赖 Host 或复制状态才能共享它。
会话模型选择器与部署默认值是同一项偏好的两个层次。如果选择器只影响其所在会话,下一个空白会话可能选择不同模型,用户却没有途径使默认值与选择器一致。如果默认值位于 Host 网关内部,直接创建 Agent 的入口只有依赖 Host 或复制状态才能共享它。
推理强度使持久化形态成为约定的一部分:不含强度的模型选择必须清除已存强度,否则下一个 Agent 可能会采用所选模型不接受的强度。
## 决定
`AgentDefaultModelService` 提供 `ctx.agentDefaultModel`,并把 `{provider, model, reasoningEffort?}` 注册为 `agent-default-model` Settings 分节。其 `{provider, model}` 组合条目是 base 层,`settings.yaml` 提供用户层。该服务不偏向特定前门,因此直接创建与 ApiProxy 支撑的创建共享同一个默认值([headless 直接 core 前门](../architecture/2026-08-09-headless-direct-core-front-door.md))。`workspaceRoot` 仍是 ApiProxy 配置,因为它是 Host 启动器事实,而不是模型状态。
`AgentDefaultModelService` 提供 `ctx.agentDefaultModel`,并把 `{provider, model, reasoningEffort?}` 注册为 `agent-default-model` Settings 分节。其 `{provider, model}` 组合条目是 base 层,`settings.yaml` 提供用户层。该服务不偏向特定入口,因此直接创建与 ApiProxy 支撑的创建共享同一个默认值([headless 直接 core 入口](../architecture/2026-08-09-headless-direct-core-entry-point.md))。`workspaceRoot` 仍是 ApiProxy 配置,因为它是 Host 启动器事实,而不是模型状态。
`reasoningEffort` 属于 Settings 分节,但不属于插件配置。Settings 层按字段合并,因此已配置的强度会在用户选择省略它时继续存在。`saveSelection()` 写入完整的用户分节;缺席值由此清除已存强度。部署级强度默认值属于适配器 profile,并由它按模型解析。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-08-dsh-run-headless-command.md
2026-08-08-dsh-run-headless-command.md: 566eeb5b2a09a0d07d72a68e4a5f449d2822e708
2026-08-08-dsh-run-headless-command.zh.md: 177410e783a37026940829da5f81343ddc61cb29
2026-08-08-dsh-run-headless-command.md: ed095f4077a23e51bffb647d24eed19ba09e11ed
2026-08-08-dsh-run-headless-command.zh.md: 89d54e35573f14786e05d648f2b42891ca27a043
@@ -22,7 +22,7 @@ dsh run [--profile <name>] [--patch <path>...] <task...>
`RunInvocation` is a distinct `DshInvocation` member. The generic profile invocation carries no task state and accepts no positional arguments. Both dispatch paths use `runProfile`: profile boot omits `task`, while `run` supplies it. A one-shot profile without `headless-runner` fails through the composed-row check, and profile boot containing that row without a task points to `dsh run --profile <name> "<task>"`.
The [profile plugin bundle decision](../architecture/2026-08-05-profile-plugin-bundles.md) owns composition. [Headless is a direct core front door](../architecture/2026-08-09-headless-direct-core-front-door.md) owns the execution contract: one fresh persisted Session, final assistant text on stdout, completed/non-completed exit mapping, empty stderr on success, no listening port, and bounded signal shutdown after Agent quiescence and Session flush.
The [profile plugin bundle decision](../architecture/2026-08-05-profile-plugin-bundles.md) owns composition. [Headless is a direct core entry point](../architecture/2026-08-09-headless-direct-core-entry-point.md) owns the execution contract: one fresh persisted Session, final assistant text on stdout, completed/non-completed exit mapping, empty stderr on success, no listening port, and bounded signal shutdown after Agent quiescence and Session flush.
The `run` verb belongs only to one-shot task execution. Application-file launch requires a distinct command name.
@@ -22,7 +22,7 @@ dsh run [--profile <name>] [--patch <path>...] <task...>
`RunInvocation` 是单独的 `DshInvocation` 成员。通用 profile 调用不携带任务状态,也不接受位置参数。两条分派路径都使用 `runProfile`profile 启动省略 `task`,而 `run` 提供该字段。缺少 `headless-runner` 的一次性 profile 会触发组合行检查;如果启动的 profile 包含该行却未提供任务,错误会指向 `dsh run --profile <name> "<task>"`
[profile 插件组合包决策](../architecture/2026-08-05-profile-plugin-bundles.md)负责组合。[Headless 是直接 core 前门](../architecture/2026-08-09-headless-direct-core-front-door.md)负责执行约定:一个新的持久化会话、stdout 上的最终 assistant 文本、completed/非 completed 的退出状态映射、成功时为空的 stderr、无监听端口,以及 Agent 完全停稳且会话 flush 后的有界信号关闭。
[profile 插件组合包决策](../architecture/2026-08-05-profile-plugin-bundles.md)负责组合。[Headless 是直接 core 入口](../architecture/2026-08-09-headless-direct-core-entry-point.md)负责执行约定:一个新的持久化会话、stdout 上的最终 assistant 文本、completed/非 completed 的退出状态映射、成功时为空的 stderr、无监听端口,以及 Agent 完全停稳且会话 flush 后的有界信号关闭。
`run` 动词只负责一次性任务执行。应用文件启动需要不同的命令名。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-08-user-explicit-skill-invocation.md
2026-08-08-user-explicit-skill-invocation.md: 74d9f01f191005db6d3d283a3c56f5ee664447f8
2026-08-08-user-explicit-skill-invocation.zh.md: 0f7c9e1261dda796d988c17ddf7199b74be4133e
2026-08-08-user-explicit-skill-invocation.md: a7c2c15703af318cb4112f2d3dfda698bc5e3bc2
2026-08-08-user-explicit-skill-invocation.zh.md: a8d685e5eb12766bebddebb7f5993579cf08531e
@@ -10,14 +10,14 @@ A `disable-model-invocation: true` skill is user-only by design: it never enters
## Decision
User-explicit invocation is a host-side pre-step injection, uniform for every user-invocable skill and every front end:
User-explicit invocation is a host-side pre-step injection, uniform for every user-invocable skill and every entry point:
- `dsh-tool-skill` registers a second `agent/pre-step` listener (beside its catalog listener, the same seam `workspace-instructions` and the runtime-context snapshot ride): it scans the step's claimed messages for whitespace-bounded `/name` tokens — anywhere in the text, the same word-boundary shape the transcript chip decoration uses — collects first-seen-deduplicated names, loads each through `ctx.skills.get`, checks `isUserInvocable` on the loaded definition (the single lookup that produces what is injected), renders it with the shared `renderSkillContent`, and appends the injections after every other injection of the step: background first (workspace rules, runtime policy, catalog), the material the model must act on last, closest to its answer. Registration order pins the placement — the gesture listener registers before the catalog listener, so the waterfall hands it the catalog-bearing list to extend.
- Precision is closed-set matching, exactly like slash commands: `/goal` resolves against the command registry, `/name` against the workspace's user-invocable skill directory; a miss stays ordinary prose, so nothing is ever guessed. Only `source.kind === 'user'` messages are scanned — external text cannot forge a gesture. Paths (`/usr/bin`), fractions (`5/8`), and prefixed tokens (`foo/name`) all break the boundary.
- The client keeps the [plain-text-reference decision](../architecture/2026-07-25-web-input-machine-and-slash-pipeline.md): a menu pick lands the literal `/name ` and the prompt ships it verbatim; ui-skill implements no adjudication hooks and no reference codec. `skill.list` (now the domain's only RPC) serves every user-invocable skill with `modelInvocable` so menus mark user-only entries. A name shared with a host command resolves to the command — adjudication claims the line client-side before it becomes a prompt.
- The injection is a `user`-role message carrying the `skill-invocation` source (`{ name, form: 'instructions' }`), so `user/message` logging, the context-injection transcript row (labelled with the skill name), and replay all come free; `renderSkillContent` lives in the `dsh-skill` seam, shared verbatim with the `skill` tool result, and the catalog's closing sentence tells the model to follow an injected block instead of re-loading it.
Peer-product survey (Pi, OpenCode, Claude Code, Kimi Code, Codex, DeepSeek-Reasonix — local checkouts) was unanimous that user-explicit triggering is programmatic injection with zero model participation; the final shape is closest to Codex's core-side `$name` mention scanning, which likewise frees every front end from implementing recognition.
Peer-product survey (Pi, OpenCode, Claude Code, Kimi Code, Codex, DeepSeek-Reasonix — local checkouts) was unanimous that user-explicit triggering is programmatic injection with zero model participation; the final shape is closest to Codex's core-side `$name` mention scanning, which likewise frees every entry point from implementing recognition.
## Alternatives considered
@@ -10,14 +10,14 @@ Status: implemented
## 决策
用户显式调用是一次宿主侧的 pre-step 注入,对每一个用户可调用的 skill 和每一种前端一致:
用户显式调用是一次宿主侧的 pre-step 注入,对每一个用户可调用的 skill 和每一种运行入口一致:
- `dsh-tool-skill` 注册第二个 `agent/pre-step` 监听器(与其目录监听器并列,也是 `workspace-instructions` 与运行时上下文快照搭乘的同一 seam):它在该步骤已认领的消息中扫描以空白为界的 `/name` token——文本中任意位置均可,与 transcript(文本记录)chip 装饰所用的词边界形状相同——收集按首见去重的名称,逐个经 `ctx.skills.get` 加载,在已加载定义上检查 `isUserInvocable`(产生注入内容的正是这同一次查找),用共享的 `renderSkillContent` 渲染,并把注入追加在该步骤所有其他注入之后:背景在前(工作区规则、运行时策略、目录),模型必须着手处理的材料在最后、最贴近它的回答。注册顺序钉住了这一位置——手势监听器先于目录监听器注册,因此 waterfall(瀑布式事件)会把携带目录的列表交给它来扩展。
- 精确性来自封闭集合匹配,与斜杠命令完全一致:`/goal` 对照命令注册表解析,`/name` 对照工作区的用户可调用 skill 目录解析;未命中即保持为普通行文,因此绝不猜测。只扫描 `source.kind === 'user'` 的消息——外部文本无法伪造手势。路径(`/usr/bin`)、分数(`5/8`)与带前缀的 token`foo/name`)都会破坏该边界。
- 客户端沿用[纯文本引用决策](../architecture/2026-07-25-web-input-machine-and-slash-pipeline.md):菜单 pick 落下字面文本 `/name `,该文本随提示词原样提交;ui-skill 不实现任何裁决钩子,也没有引用 codec。`skill.list`(现在是该领域唯一的 RPC)提供每一个用户可调用的 skill 并携带 `modelInvocable`,供菜单标出仅限用户的条目。与宿主命令同名的名称解析为命令——客户端会在该行成为提示词之前完成裁决并将其认领。
- 注入是一条携带 `skill-invocation` 来源(`{ name, form: 'instructions' }`)的 `user` 角色消息,因此 `user/message` 落账、上下文注入的 transcript 行(以 skill 名称标注)与回放全部免费获得;`renderSkillContent` 位于 `dsh-skill` seam,由注入和 `skill` 工具结果共用,二者内容逐字相同,目录的结尾一句会告诉模型遵循注入块而不是重新加载。
同类产品调研(Pi、OpenCode、Claude Code、Kimi Code、Codex、DeepSeek-Reasonix——本地检出)一致表明:用户显式触发都是模型零参与的程序化注入;最终形态最接近 Codex 核心侧的 `$name` mention 扫描——它同样让每一种前端免于自行实现识别。
同类产品调研(Pi、OpenCode、Claude Code、Kimi Code、Codex、DeepSeek-Reasonix——本地检出)一致表明:用户显式触发都是模型零参与的程序化注入;最终形态最接近 Codex 核心侧的 `$name` mention 扫描——它同样让每一种运行入口免于自行实现识别。
## 考虑过的替代方案
@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.md
2026-08-08-windows-acl-restricted-token-sandbox.md: 7e8f229269233d9ac9baa65241ca02a4cf4c3f7c
2026-08-08-windows-acl-restricted-token-sandbox.zh.md: eeb346b228b3559f487448e5d4ec525b7bb89525
@@ -0,0 +1,43 @@
# Agent Note: Windows sandbox rung: raw ACL restricted tokens over mxc and AppContainer
Status: implemented
English | [中文](2026-08-08-windows-acl-restricted-token-sandbox.zh.md)
## Problem
The [sandbox decision](2026-07-06-sandbox.md) leaves `PLATFORM_CHAINS.win32` empty, so shipped Windows profiles degrade to danger-full-access because no confining executor exists. The win32 rung must confine the two file-effect modes the sandbox vocabulary promises — `read-only` (zero writes) and `workspace-write` (writes under the workspace root plus a backend-defined temp area) — while leaving reads, network, and process visibility alone, because every mode permits reading.
## Decision
Implement the rung directly on the raw ACL mechanism: duplicate the caller's token into a `WRITE_RESTRICTED` token (`CreateRestrictedToken` with `WRITE_RESTRICTED` + `DISABLE_MAX_PRIVILEGE` + `LUA_TOKEN`) whose restricting SIDs include a write SID (`S-1-4-x-y`); the write SID's Write ACEs on the workspace and temp roots are the entire write allowlist, because `WRITE_RESTRICTED` intersects write accesses only and reads keep the caller's full ambient access. The mechanism is the one huoyaoyuan/windows-acl-restrict-poc (`10e4dfb`) demonstrates; this port checks every API call and fails closed (the POC fail-opened on every ignored return value). The write SID is the PER-WORKSPACE identity, derived deterministically from the canonical workspace path (`workspaceWriteSid` — sha256 → `S-1-4-x-y`) and stored NOWHERE: the workspace-root ACE therefore materializes once per workspace per machine — the standing ACE is the cross-session reuse cache, and every later provision hits the exact-ACE skip (idempotent re-grant skips the eager full-tree re-propagation — no garbage collection) — instead of once per session, which is what the earlier per-session random SID paid a full tree propagation per session for. The seam derives the session's PRIVATE temp subdirectory from the session id + workspace (sha256, 16 hex — stored nowhere, so no tamper surface exists) and creates it exclusively; it is removed on provider dispose, and a crash leaves it as `%TEMP%` litter whose next resume fails loudly at the exclusive creation until temp hygiene reclaims it. The seam materializes the workspace ACE STANDING (never revoked — the cache) and the temp ACE REVOCABLY (revoked on provider dispose, so an inheritable ACE never outlives its session's temp directory on the ambient temp root). The token's restricting list is the keep-alive group plus the write SID only under workspace-write: read-only = [logon SID, Everyone] and workspace-write = [logon SID, Everyone, write SID]. The keep-alive invariants are logon SID + Everyone (early DLL init dies with 0xC0000142 and CNG crashes pwsh with 0xE0434352 without them). Read-only carries no write SID: a standing grant ACE from an earlier workspace-write period stays INERT (the pass-2 check grants only what the list carries, so read-only remains strictly zero-grant across a `/permission` downgrade or a crash-resumed session, while the standing ACE keeps the re-upgrade free). Authenticated Users is absent from BOTH lists — the WMI namespace security check fails (0x80041003), so CIM is unavailable in every confined mode, and the C:\-root tree-creation escape (standing `AU:(AD)` + `AU:(OI)(CI)(IO)(M)` ACEs) is closed in both; INTERACTIVE/LOCAL are likewise absent from both (the Public tree writes are denied — pinned by the runner's Public-probe regression). Workspace-write children see a PRIVATE per-session temp subdirectory (`<temp>\dsh-<16 hex>` derived from the session id + workspace — created exclusively, reparse points rejected, removed on provider dispose — TMP/TEMP rewritten by the runner — bwrap `--tmpfs /tmp` semantics). The restricted token's DEFAULT DACL is extended with a full-access write-SID ACE (`SetTokenInformation(TokenDefaultDacl)`): new objects created without an explicit security descriptor (anonymous pipes — CreatePipe, sync objects) then carry a restricting-SID ACE and pass the write pass-2 check at creation; NAMED pipes are exempt — their default security descriptor is the Win32 layer's user-mode default SD template (built by KernelBase — owner/SYSTEM/Admins full, Everyone/ANONYMOUS read-only), which the token cannot influence, so piped stdio capture stays denied for confined grandchildren (the POC-documented boundary, pinned by the runner suite). It ships as [`@deepseek-ai/dsh-sandbox-windows-acl`](../../../../packages/sandbox/sandbox-windows-acl/README.md) (backend plus the `./runner` argv-prefix entry), the `win32` chain rung of [`dsh-sandbox-local`](../../../../packages/sandbox/sandbox-local/README.md), and [`@deepseek-ai/dsh-pwsh-sandbox`](../../../../packages/bash/pwsh-sandbox/README.md) as the confining executor; the Windows platform layer re-enables the full permission surface (sandbox/sandbox-policy/permission/approval/fs-sandbox) over the confined pwsh stack.
## How the restriction works (why no new identity)
The identity routes restrict by *who* runs the child; this rung restricts by *token derivation*. An identity route (landstrip's restricted-user, AppContainer) runs the child under a fresh account or container SID that starts with zero ACEs on the host's files — everything, reads included, defaults to denied, and every path the child may touch must then be opened back up by writing ACEs for that identity: the wholesale DACL mutation that disqualified both alternatives. The restricted token keeps the caller's own SID and logon session: [`CreateRestrictedToken`](https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-createrestrictedtoken) derives a token that adds the restricting SIDs and the `WRITE_RESTRICTED` flag, so Windows performs the access check twice — once against the normal SIDs, once against the restricting SIDs — and grants write-class access only where both checks pass. Reads pass on the normal check alone (the caller's SIDs already carry read access everywhere the caller can read), which is why this rung needs no read grants and no new account; writes must additionally clear the orphan-SID check, which only the workspace and temp ACEs satisfy. `DISABLE_MAX_PRIVILEGE | LUA_TOKEN` synthesize the limited-user effect of a fresh account token-side, so even an elevated caller derives a filtered token. The same primitive could restrict reads (`SidsToDisable` turning SIDs deny-only), but a read-restricted token would need per-path read grants — reintroducing exactly the cost the identity routes pay — and the sandbox vocabulary never requires read confinement.
## Alternatives considered
### Why not mxc (Microsoft xContainer)?
Two disqualifiers. First, the OS floor is too new: the [mxc OS-version policy](https://github.com/microsoft/mxc/blob/main/docs/process-container/os-version-support.md) sets the product floor at Windows 11 24H2 (build 26100), and the BaseContainer tier (T1, `Experimental_CreateProcessInSandbox`) exists only on 25H2+ (build 26600+) with the OS feature enabled — on every supported release at or below 25H2 the filesystem policy falls back to T3, AppContainer plus host-side DACL ACE augmentation. Second, supporting arbitrary-path reads under either tier means granting read access by writing ACLs over every path the child may read: a model that reads the whole workspace and arbitrary files would require wholesale host DACL mutation — a standing side effect and a cost a write-only restriction does not need.
### Why not AppContainer?
An AppContainer token carries no ambient read access: every readable path must be pre-granted through capabilities or explicit ACEs, so arbitrary-path reads — the harness's read model — are unsupported without the same wholesale grants. The restricted token needs no read grants at all: it intersects write access only.
### Why not landstrip?
The [landstrip evaluation](../../rejected/feature/2026-07-26-evaluate-landstrip-for-windows-sandbox-rung.md) was rejected before implementation (not battle-tested; the in-house launcher plan won), and its Windows backend is AppContainer-shaped, inheriting the same arbitrary-read problem.
## Consequences
Bought: write-only confinement with no new OS floor (`CreateRestrictedToken` predates the mxc releases by two decades), reads/network/process visibility untouched exactly as the mode vocabulary requires, and fail-closed errors carrying the API name and the exact Win32 code. Cost: no read-side or network isolation; console isolation unavailable (hidden-console children die with `STATUS_DLL_INIT_FAILED`; children share the host console); standing ACE mutations on the granted roots (caller-owned directories; workspace ACEs stand forever by design — the reuse cache, invisible residue when a workspace is renamed — temp ACEs revoked by provider dispose together with the derived private temp directory — a crash leaves both behind and the next resume fails loudly at the exclusive creation until temp hygiene reclaims the directory); grant materialization is an EAGER full-tree propagation (`SetNamedSecurityInfoW` walks every descendant immediately — tens of seconds on large workspaces), paid once per workspace per machine by the per-workspace identity; CIM is unavailable in BOTH confined modes (AuthUsers dropped from both lists — the WMI namespace security check fails, and `Get-ComputerInfo` silently returns incomplete results) as the price of closing the C:\-root tree-creation escape in both; FAT-class (non-ACL) targets outside the granted roots remain writable under both modes (no security descriptors to intersect — a legacy residue treated as unsupported, warn-only, documented in the README); NULL-DACL directories are not identity-preserving under a grant+revoke round-trip (documented edge, the POC shares it); `whoami` and token-inspection cmdlets fail under the restricted token (diagnostic noise, documented); and BOTH confined modes run `pwsh` in ConstrainedLanguage mode — the restricted token trips PowerShell's lockdown detection, so `Add-Type`, non-core .NET statics (`[System.IO.*]::`, `[math]::`), COM objects, and reflection fail with "only core types" errors while `-f` formatting, property access, and core cmdlets/types keep working, and the language mode cannot be lifted back to FullLanguage from inside — taught to the model in the pwsh tool description and documented in the package README's Known Limitations; BOTH confined modes also deny named-pipe opens — libuv's piped-stdio spawns fail with EPERM (the POC-documented "no output redirection" boundary; inherited/ignored stdio and anonymous pipes work) — documented in the package README's Known Limitations and taught to the model in the pwsh tool description.
## Testing
The product-visible Windows roster flip is win32-only, so the keyless snapshot fixtures — which must replay on macOS/Linux — cannot cover it; the bundle composition specs ([`base.spec.ts`](../../../../packages/bundle/base/tests/base.spec.ts), [`windows-shell.spec.ts`](../../../../apps/cli/tests/windows-shell.spec.ts)) plus the win32 real-runner suites (`packages/sandbox/sandbox-windows-acl/tests/`, `packages/bash/pwsh-sandbox/tests/`) are the substitute evidence, and the CI Windows lane owns the assembled signal. The grant machinery is pinned cross-platform by `packages/sandbox/sandbox-local/tests/acl-grants.spec.ts` (the derived private-temp identity — deterministic per session + workspace, distinct across sessions — one-shot materialization, exclusive temp creation with reparse-point rejection and self-cleanup on failure, clean-restart re-grant of the same derived directory, the standing-vs-revocable lifecycle across dispose and the mode-switch cycle, and the derived-SID argv contract — with the Win32 surface mocked) and on win32 by `workspace-sid.spec.ts` (derivation determinism/shape/distinctness), `grant.spec.ts` (real-DACL materialization: revocable paths revoke on dispose, standing paths survive it), the `acl.spec.ts` idempotent-grant fast-path and standing-ACE-after-dispose contract, the `failure-paths.spec.ts` suspension-orphan regression (AssignProcessToJobObject failure terminates the child), and the `runner.spec.ts` `--write-sid` contract (caller-owned grants, private temp subdir through TMP/TEMP, both-mode CIM-denial probes, the mode-downgrade regression — a standing grant ACE is inert under read-only and effective again on re-upgrade — the ambient-writable Public-probe regression (a C:\Users\Public subdirectory write is denied under both modes), and the ConstrainedLanguage pins in both modes, plus the grandchild-stdio matrix pins — inherited/ignored stdio spawns succeed while piped capture is DENIED in both modes). The runner-failure classification is exit-gated on 127 (a confined command that merely prints the `windows-acl-run:` signature on a non-127 exit is never misclassified as "the command did not run" — pinned in the pwsh-sandbox helper suite).
## Related
The [pwsh executor decision](2026-08-01-pwsh-tool-and-executor.md) owns the pwsh-sandbox/tool-pwsh dialect split this rung consumes.
@@ -0,0 +1,43 @@
# Agent Note: Windows sandbox rung: raw ACL restricted tokens over mxc and AppContainer
Status: implemented
[English](2026-08-08-windows-acl-restricted-token-sandbox.md) | 中文
## Problem
[沙盒决策](2026-07-06-sandbox.md)把 `PLATFORM_CHAINS.win32` 留空,交付的 Windows profile 因为没有可用的隔离执行器而退化为 danger-full-access。win32 档必须实现沙盒词汇表承诺的两个文件效果模式——`read-only`(零写入)与 `workspace-write`(仅工作区根目录加后端定义的临时区域可写)——同时保持读、网络与进程可见性不受影响,因为所有模式都允许读取。
## Decision
直接基于原始 ACL 机制实现该档:把调用者令牌复制为 `WRITE_RESTRICTED` 受限令牌(`CreateRestrictedToken``WRITE_RESTRICTED` + `DISABLE_MAX_PRIVILEGE` + `LUA_TOKEN`),其 restricting SIDs 中包含写入 SID`S-1-4-x-y`);工作区与临时目录上写入 SID 的 Write ACE 就是全部写入白名单,因为 `WRITE_RESTRICTED` 只对写访问做交集检查,读保持调用者的完整环境访问。该机制来自 huoyaoyuan/windows-acl-restrict-poc`10e4dfb`)的演示;本移植检查每一个 API 调用并 fail-closedPOC 因忽略返回值而 fail-open)。写入 SID 是**按工作区**的身份,由规范工作区路径确定性派生(`workspaceWriteSid`——sha256 → `S-1-4-x-y`),且**任何地方都不存储**:工作区根目录 ACE 因此每台机器每个工作区只物化一次——常驻 ACE 就是跨会话复用缓存,此后每次供给都命中精确 ACE 跳过(幂等重授权跳过急切的全树重传播——不做垃圾回收)——而不是每会话一次,这正是先前每会话随机 SID 每个会话都要付一次全树传播的代价。seam 从会话 id + 工作区派生会话的**私有**临时子目录(sha256、16 位 hex——任何地方都不存储,因此不存在篡改面)并独占创建;它在提供方 dispose 时移除,崩溃则把它留作 `%TEMP%` 垃圾,其下一次恢复会在独占创建处大声失败,直到临时目录卫生机制将其回收。seam 把工作区 ACE **常驻**物化(绝不撤销——就是缓存),把临时 ACE **可回收**物化(提供方 dispose(资源释放)时撤销,因此可继承 ACE 不会在环境临时根目录上比其会话的临时目录活得更久)。令牌的 restricting list 是保活组加上仅 workspace-write 下的写入 SIDread-only = [登录 SID、Everyone]workspace-write = [登录 SID、Everyone、写入 SID]。保活不变式是登录 SID + Everyone(没有它们,早期 DLL init 会以 0xC0000142 死亡,CNG 会让 pwsh 以 0xE0434352 崩溃)。Read-only 不含写入 SID:先前 workspace-write 时期留下的常驻授权 ACE 保持**失效**(pass-2 检查只授予列表所携带的内容,因此 read-only 在 `/permission` 降级或崩溃后恢复的会话中始终保持严格零授权,而常驻 ACE 让重新升级保持零成本)。Authenticated Users 在**两种**列表中都缺席——WMI namespace 安全校验失败(0x80041003),因此 CIM 在每一种受限模式下都不可用,且 C:\-root 建树逃逸(驻留的 `AU:(AD)` + `AU:(OI)(CI)(IO)(M)` ACE)在两种模式下都被关闭;INTERACTIVE/LOCAL 同样在两种列表中都缺席(Public 树的写入被拒绝——由 runner 的 Public-probe 回归钉住)。Workspace-write 子进程看到的是私有的每会话临时子目录(`<temp>\dsh-<16 hex>`——由会话 id + 工作区派生、独占创建、拒绝 reparse point、提供方 dispose 时移除——TMP/TEMP 由 runner 重写——bwrap `--tmpfs /tmp` 语义)。受限令牌的**默认 DACL** 被扩展一条写入 SID 全权 ACE(`SetTokenInformation(TokenDefaultDacl)`):此后不带显式安全描述符创建的新对象(匿名管道——CreatePipe、同步对象)自带 restricting SID ACE,创建时的写 pass-2 检查通过;**named pipe 例外**——其默认安全描述符是 Win32 层在用户态安装的默认 SD 模板(由 KernelBase 构建——owner/SYSTEM/Admins 全权、Everyone/ANONYMOUS 只读),令牌无法影响,因此受限孙进程的管道 stdio 捕获保持拒绝(POC 记载的边界,由 runner 套件钉住)。它以 [`@deepseek-ai/dsh-sandbox-windows-acl`](../../../../packages/sandbox/sandbox-windows-acl/README.md)(后端加 `./runner` argv 前缀入口)、[`dsh-sandbox-local`](../../../../packages/sandbox/sandbox-local/README.md) 的 `win32` 链档、以及作为隔离执行器的 [`@deepseek-ai/dsh-pwsh-sandbox`](../../../../packages/bash/pwsh-sandbox/README.md) 交付;Windows 平台层在受限 pwsh 栈之上重新启用完整权限面(sandbox/sandbox-policy/permission/approval/fs-sandbox)。
## How the restriction works (why no new identity)
身份路线靠"**谁**在跑子进程"来限制,本档靠"令牌派生"来限制。身份路线(landstrip 的 restricted-user、AppContainer)用全新账户或容器 SID 运行子进程,该身份在宿主的文件上从零条 ACE 开始——一切访问(包括读)默认拒绝,子进程要碰的每条路径都必须事后为那个身份补写 ACE 才能放行:这正是让两个备选方案出局的全盘 DACL 改造。受限令牌保留调用者自己的 SID 与 logon session[`CreateRestrictedToken`](https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-createrestrictedtoken) 派生一个加入 restricting SIDs 与 `WRITE_RESTRICTED` 标志的令牌,于是 Windows 做两次访问检查——一次按正常 SID,一次按 restricting SIDs——只有两次都放行,写类访问才被授予。读只凭正常检查即可通过(调用者的 SID 在其可读范围内本来就携带读权限),所以本档不需要任何读授权、也不需要新账户;写还必须额外通过孤儿 SID 检查,而只有工作区与临时目录的 ACE 能满足它。`DISABLE_MAX_PRIVILEGE | LUA_TOKEN` 在令牌侧合成了新账户的受限用户效果,即使提升过的调用者派生的也是过滤令牌。同一原语其实也能限制读(`SidsToDisable` 把 SID 变为 deny-only),但受限读的令牌需要逐路径的读授权——恰好重新引入身份路线付出的代价——而沙盒词汇表从不要求读隔离。
## Alternatives considered
### 为什么不选 mxcMicrosoft xContainer)?
两个否决理由。其一,OS 版本要求太新:[mxc 的 OS 版本支持文档](https://github.com/microsoft/mxc/blob/main/docs/process-container/os-version-support.md)把产品下限设在 Windows 11 24H2build 26100),而 BaseContainer 档(T1`Experimental_CreateProcessInSandbox`)只在 25H2+build 26600+)且启用 OS feature 时存在——在 25H2 及以下的所有受支持版本上,文件系统策略都会回退到 T3,即 AppContainer 加宿主侧 DACL ACE 改造。其二,在任一档下支持任意路径读都意味着要为子进程可读的每个路径写 ACL 授予读权限:模型要读整个工作区和任意文件,就需要全盘改写宿主 DACL——对只做写限制的需求而言,这是不必要的驻留副作用与代价。
### 为什么不选 AppContainer
AppContainer 令牌没有环境读访问:每个可读路径都必须预先通过 capability 或显式 ACE 授予,因此任意路径读——harness 的读模型——在不做同样的全盘授予时无法支持。受限令牌完全不需要读授予:它只对写访问做交集。
### 为什么不选 landstrip
[landstrip 评估](../../rejected/feature/2026-07-26-evaluate-landstrip-for-windows-sandbox-rung.md)在实现前已被否决(未经实战检验;自建 launcher 方案胜出),且其 Windows 后端是 AppContainer 形态,继承同样的任意路径读问题。
## Consequences
所得:仅写隔离、不引入新的 OS 版本下限(`CreateRestrictedToken` 比 mxc 的版本早二十年)、读/网络/进程可见性完全不受影响(与模式词汇表一致)、fail-closed 错误携带 API 名与精确 Win32 错误码。所失:无读侧或网络隔离;控制台隔离不可用(隐藏控制台子进程以 `STATUS_DLL_INIT_FAILED` 死亡;子进程共享宿主控制台);被授权根目录上有驻留 ACE 改动(目录须为调用者所有;工作区 ACE 按设计永久常驻——复用缓存,工作区改名时成为不可见残留——临时 ACE 由提供方 dispose 连同派生的私有临时目录一起回收——崩溃会把两者都留下,下一次恢复会在独占创建处大声失败,直到临时目录卫生回收该目录);授权物化是急切的全树传播(`SetNamedSecurityInfoW` 立即遍历每个后代——在大型工作区上耗时数十秒),因按工作区身份,每台机器每个工作区只付一次;CIM 在**两种**受限模式下都不可用(AuthUsers 从两种列表中被移除——WMI namespace 安全校验失败,`Get-ComputerInfo` 静默返回不完整结果),作为关闭两种模式下 C:\-root 建树逃逸的代价;位于被授权根目录之外的 FAT 类(无 ACL)目标在两种模式下仍可写(没有可做交集的安全描述符——作为历史残留处理:不支持、仅警告、已在 README 中记录);NULL DACL 目录在 grant+revoke 往返下不保持身份(记录在案的边角,POC 亦有此行为);`whoami` 与令牌检查 cmdlet 在受限令牌下失败(诊断噪音,已记录);且**两种**受限模式都以 ConstrainedLanguage 模式运行 `pwsh`——受限令牌触发 PowerShell 的锁定检测,因此 `Add-Type`、非核心 .NET 静态调用(`[System.IO.*]::``[math]::`)、COM 对象与反射都会以“only core types”错误失败,而 `-f` 格式化、属性访问与核心 cmdlet/类型继续工作,语言模式也无法从内部提升回 FullLanguage——已在 pwsh 工具描述中教给模型,并记录在包 README 的 Known Limitations 中;**两种**受限模式同样拒绝 named-pipe 打开——libuv 的管道 stdio spawn 以 EPERM 失败(POC 记载的“无法重定向输出”边界;继承/忽略的 stdio 与匿名管道可用)——记录在包 README 的 Known Limitations 中,并在 pwsh 工具描述中教给模型。
## Testing
产品可见的 Windows 阵容切换仅存在于 win32,而 keyless 快照夹具必须在 macOS/Linux 上可重放,因此无法覆盖它;替代证据是 bundle 组合 spec[`base.spec.ts`](../../../../packages/bundle/base/tests/base.spec.ts)、[`windows-shell.spec.ts`](../../../../apps/cli/tests/windows-shell.spec.ts))加上 win32 真实 runner 套件(`packages/sandbox/sandbox-windows-acl/tests/``packages/bash/pwsh-sandbox/tests/`),组装态信号由 CI 的 Windows lane 负责。授权机制在跨平台侧由 `packages/sandbox/sandbox-local/tests/acl-grants.spec.ts` 钉住(派生的私有临时身份——按会话 + 工作区确定性、跨会话相异——一次性物化、独占临时目录创建并拒绝 reparse point、失败时自我清理、干净重启时对同一派生目录的重新授权、dispose 与模式切换循环中的常驻/可回收生命周期,以及派生 SID 的 argv 契约——mock 掉 Win32 表面),win32 侧由 `workspace-sid.spec.ts`(派生的确定性/形态/相异性)、`grant.spec.ts`(真实 DACL 物化:可回收路径在 dispose 时撤销、常驻路径存活)、`acl.spec.ts` 的幂等授权快速路径与 dispose 后常驻 ACE 契约、`failure-paths.spec.ts` 的 suspension-orphan 回归(AssignProcessToJobObject 失败会终止子进程)与 `runner.spec.ts``--write-sid` 契约(调用者所有目录的授权、经 TMP/TEMP 的私有临时子目录、两种模式下的 CIM 拒绝探针、模式降级回归——驻留授权 ACE 在 read-only 下失效并在重新升级后再度生效——环境可写 Public-probe 回归(对 C:\Users\Public 子目录的写入在两种模式下都会被拒绝),以及两种模式下对 ConstrainedLanguage 的钉定,加上孙进程 stdio 矩阵钉定——继承/忽略的 stdio spawn 成功,而管道捕获在两种模式下都被**拒绝**)钉住。runner 失败分类以 127 退出码为门槛(受限命令仅仅在非 127 退出时打印 `windows-acl-run:` 签名,也绝不会被误分类为"命令未运行"——由 pwsh-sandbox helper 套件钉住)。
## Related
[pwsh 执行器决策](2026-08-01-pwsh-tool-and-executor.md)拥有本档所消费的 pwsh-sandbox/tool-pwsh 方言划分。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-05-uniform-agent-note-format.md
2026-07-05-uniform-agent-note-format.md: 06082251c1b96c90ed470d84224662e00e29791b
2026-07-05-uniform-agent-note-format.md: c05d81c700b81f0d619173a261f405b1cc039df1
2026-07-05-uniform-agent-note-format.zh.md: 3daa686b64b31ee2638b25dd4c42e5d8172f1d97
@@ -23,7 +23,7 @@ The whole corpus was normalized in the same change that defined the format — t
- **A bare `# <title>` H1** — rejected: the `Agent Note: ` prefix self-describes the genre when a file is read outside its tree, and the format gate prevents it from drifting.
- **`## What we give up` as the implemented closer** (the README's own phrase for what an Agent Note records) — rejected: it names only costs, and an honest consequences section records what the trade-off bought as well.
- **Convention without a gate** (write the contract down, enforce by review) — rejected: the slop checklist already outlawed spec-speak in `implemented/` by convention, and nineteen files show what convention alone achieves here.
- **A standalone `FORMAT.md` contract file** — rejected because one front door carrying layout, classification, and format is easier to discover and maintain than two contract files.
- **A standalone `FORMAT.md` contract file** — rejected because one entry point carrying layout, classification, and format is easier to discover and maintain than two contract files.
## Consequences
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-19-remove-generated-agent-note-index.md
2026-07-19-remove-generated-agent-note-index.md: ee85ec0757d5924f5784c43a50003eb96e0a9531
2026-07-19-remove-generated-agent-note-index.md: 652ac72afe69284240667e61af1f3fbfcb182ddb
2026-07-19-remove-generated-agent-note-index.zh.md: 6e1967fbde0c6ac59bbc3a184dad68c989efadbc
@@ -12,7 +12,7 @@ The centralized chronological list adds little discovery value beyond browsing t
## Decision
The lifecycle/class filesystem tree is the Agent Note inventory. [README.md](../../README.md) remains the curated front door and contract, while ordinary tree navigation and repository search provide discovery.
The lifecycle/class filesystem tree is the Agent Note inventory. [README.md](../../README.md) remains the curated entry point and contract, while ordinary tree navigation and repository search provide discovery.
`scripts/agent-note-tree.ts` owns the closed lifecycle/class sets and structural walker. `verify-agent-note-classification` validates that tree and rejects the legacy homes and a root `INDEX.md`; it does not render or freshness-check a centralized list.
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-22-product-first-root-readme.md
2026-07-22-product-first-root-readme.md: 32542a45019d64ed1826d4eb21e68c67c3c3d52e
2026-07-22-product-first-root-readme.md: 00f6084da9e83135c881abfef21b0b249cd4e30a
2026-07-22-product-first-root-readme.zh.md: 8ef6f4b99ca2c935183a225b6357d2d128edb3b0
@@ -6,7 +6,7 @@ English | [中文](2026-07-22-product-first-root-readme.zh.md)
## Problem
The root README is the repository's product front door. Its product-first structure and established voice remain useful, but concrete entry points and capability claims drift as the runtime grows. Rewriting sections whose facts remain correct increases the review surface and discards language that already works.
The root README is the repository's product entry point. Its product-first structure and established voice remain useful, but concrete entry points and capability claims drift as the runtime grows. Rewriting sections whose facts remain correct increases the review surface and discards language that already works.
## Decision
@@ -26,7 +26,7 @@ Detailed package and service inventories remain at their owning documentation. T
**Use a long marketing page with screenshots, badges, and duplicated tutorials.** Rich media can demonstrate a stable product journey, but it ages separately from commands and source contracts. The root stays compact and links to runnable examples and owned guides.
**Project the root README as the documentation website home page.** A single landing page avoids two narratives, but the website's user guide and the repository's product/developer front door have different navigation and maintenance needs.
**Project the root README as the documentation website home page.** A single landing page avoids two narratives, but the website's user guide and the repository's product/developer entry point have different navigation and maintenance needs.
## Consequences
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.md
2026-08-03-package-anchored-subsystem-pages.md: f429f3d41c1f152e83faeb12c379d221627e767f
2026-08-03-package-anchored-subsystem-pages.zh.md: 3a56fa39357591197606a28b49cf0eac8f58963e
2026-08-03-package-anchored-subsystem-pages.md: 2a47f35e9d3755286bed7d42f59fd21e30f9148d
2026-08-03-package-anchored-subsystem-pages.zh.md: 53216a430b979f0612f256aaec9774b88fe14bbd
@@ -14,7 +14,7 @@ Every `docs/subsystems/` page anchors to the package or package group that decla
Every type a generated signature references must resolve somewhere in the folder: the agent ownership vocabulary moved from the generator's `TYPE_LINK_EXEMPTIONS` into `LINK_MAP → core.md`, so exemptions are reserved for genuinely service-local or vendored shapes. Each pasted declaration has one home (`SessionEvent` lives on [session.md](../../../../docs/subsystems/session.md); core.md summarizes and links).
Every `packages/<group>/README.md` pair is a thin front door in one shape: a why-first intro paragraph, a package table (Package / Role / ctx key), and a closing pointer to the owning subsystems page. Load-bearing prose that outgrows that shape relocates to the owning subsystems page rather than being deleted.
Every `packages/<group>/README.md` pair is a thin entry point in one shape: a why-first intro paragraph, a package table (Package / Role / ctx key), and a closing pointer to the owning subsystems page. Load-bearing prose that outgrows that shape relocates to the owning subsystems page rather than being deleted.
The [subsystems README](../../../../docs/subsystems/README.md) indexes every page in the folder on both language sides; `scripts/project-doc-site.spec.ts` enforces one table row per page, so a page added by a later PR (or absorbed in a merge) cannot silently miss the index.
@@ -14,7 +14,7 @@ Status: implemented
生成签名引用的每个类型都必须能在目录中某处解析:agent 所有权词汇从生成器的 `TYPE_LINK_EXEMPTIONS` 移入 `LINK_MAP → core.md`,因此豁免只留给真正服务本地或 vendored 的形状。每个粘贴的声明只有一个家(`SessionEvent` 位于 [session.md](../../../../docs/subsystems/session.md)core.md 概括并链接)。
每个 `packages/<group>/README.md` 配对都是统一形状的轻薄门面:一段以「为什么」开头的介绍、一张包表格(包 / 角色 / ctx 键)、一个指向拥有方子系统页面的收尾指针。超出该形状的承重散文迁移到拥有方子系统页面,而非删除。
每个 `packages/<group>/README.md` 配对都是统一形状的精简入口:一段以「为什么」开头的介绍、一张包表格(包 / 角色 / ctx 键)、一个指向拥有方子系统页面的收尾指针。超出该形状的承重散文迁移到拥有方子系统页面,而非删除。
[子系统 README](../../../../docs/subsystems/README.md) 在两个语言侧索引目录中的每一页;`scripts/project-doc-site.spec.ts` 强制每页一行表格,因此后续 PR 新增(或合并吸收)的页面无法悄悄缺席索引。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-07-23-acp-automation-only-protocol.md
2026-07-23-acp-automation-only-protocol.md: 29da3025251f9826c5780493bb2725b114a40601
2026-07-23-acp-automation-only-protocol.md: 56c433acf59d3a0c4b5c8b6605e422d3c4efede2
2026-07-23-acp-automation-only-protocol.zh.md: bf326ce9f6fea8c55b842bc61e3d8521032ea4d1
@@ -46,7 +46,7 @@ Protocol and lifecycle tests pin stop-reason and prompt codecs, version negotiat
## Consequences
ACP has a narrow contract suitable for agents and automation, while TUI and Web own human interaction and presentation. The package has fewer injected services, dependencies, protocol branches, and lifecycle states, and it no longer claims compatibility as a general editor front door.
ACP has a narrow contract suitable for agents and automation, while TUI and Web own human interaction and presentation. The package has fewer injected services, dependencies, protocol branches, and lifecycle states, and it no longer claims compatibility as a general editor entry point.
Automation clients receive complete committed text rather than token deltas or structured tool UI. They inspect durable logs or another API when they need reasoning, tool traces, titles, or richer state. Fresh-session-only operation also means callers that need durable browsing or resume use a host API rather than ACP.
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-08-04-remove-tui-package.md
2026-08-04-remove-tui-package.md: f048a04db02b582d038ebf549999c0fda50e30d3
2026-08-04-remove-tui-package.md: 19cc7d1a89a55bb57a69b9fce301f48f89384acd
2026-08-04-remove-tui-package.zh.md: be18cbd33cd4a2bb592de4e7986b2786da272d0f
@@ -8,7 +8,7 @@ English | [中文](2026-08-04-remove-tui-package.zh.md)
Removing the implicit `dsh` terminal application left `@deepseek-ai/dsh-tui` without a shipped composition. The package still carried a terminal renderer, interactive command and question adapters, extension overlays, snapshot fixtures, a patched `pi-tui` dependency, and SDK scaffolding that advertised TUI as a supported application interface. Keeping that surface required maintaining a product-sized frontend whose only remaining consumer was the project generator itself.
The package also made the repository's supported application inventory misleading. Current runnable products use Web, ACP, JSON-RPC, or one-shot CLI front doors, while the SDK continued to offer a terminal choice that no example or product command exercised.
The package also made the repository's supported application inventory misleading. Current runnable products use Web, ACP, JSON-RPC, or one-shot CLI entry points, while the SDK continued to offer a terminal choice that no example or product command exercised.
## Decision
@@ -34,6 +34,6 @@ Repository searches and generated catalogs contain no TUI package, dependency pa
## Consequences
DeepSeek Harness has no terminal UI package or generated TUI application. Existing imports, `cordis.yml` rows, SDK `--interface=tui` requests, and projects that depend on the package fail instead of being translated. Web remains the shipped interactive surface; ACP, JSON-RPC, and one-shot CLI remain the non-Web front doors.
DeepSeek Harness has no terminal UI package or generated TUI application. Existing imports, `cordis.yml` rows, SDK `--interface=tui` requests, and projects that depend on the package fail instead of being translated. Web remains the shipped interactive surface; ACP, JSON-RPC, and one-shot CLI remain the non-Web entry points.
The provider-neutral command, user-interaction, approval, tool-presentation, PTY, and session-projection capabilities remain available to other hosts. Reintroducing a terminal frontend requires a named product or deployment, an explicit package boundary, a concrete interaction provider, and assembled lifecycle and transcript acceptance for that frontend.
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-08-08-remove-cli-demo.md
2026-08-08-remove-cli-demo.md: c1153f5e9fcc89585e926f8f088829c849d1f6c1
2026-08-08-remove-cli-demo.md: 403e01f94c976d2d17eb391830721b31675cd6a9
2026-08-08-remove-cli-demo.zh.md: 7f11e0c17a15454b99b32d14ea6eda177f4b01f6
@@ -6,7 +6,7 @@ English | [中文](2026-08-08-remove-cli-demo.zh.md)
## Problem
After [`dsh run`](../feature/2026-08-08-dsh-run-headless-command.md) became the product one-shot command, `@deepseek-ai/dsh-cli-demo` remained a second application package for the same job. It carried another executable, argument grammar, app composition, cancellation lifecycle, text/JSON/stream-JSON output contract, built artifact, documentation surface, and test suite. The two front doors also assembled different trees, so a successful demo did not prove the shipped `headless` profile and users had to choose between overlapping commands.
After [`dsh run`](../feature/2026-08-08-dsh-run-headless-command.md) became the product one-shot command, `@deepseek-ai/dsh-cli-demo` remained a second application package for the same job. It carried another executable, argument grammar, app composition, cancellation lifecycle, text/JSON/stream-JSON output contract, built artifact, documentation surface, and test suite. The two entry points also assembled different trees, so a successful demo did not prove the shipped `headless` profile and users had to choose between overlapping commands.
The replay suites still need canonical session events to pin assembled backend behavior. That testing need does not require a published command or compatibility contract.
@@ -1,6 +0,0 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/proposed/feature/2026-08-01-windows-pwsh-default.md
2026-08-01-windows-pwsh-default.md: 64d713aecda60d8747199aff5334a80bf1867d92
2026-08-01-windows-pwsh-default.zh.md: 74ba6c43dea984384a9b7dd2675b681ee025d6ca
@@ -1,39 +0,0 @@
# Agent Note: Windows defaults to pwsh (roadmap)
Status: proposed
English | [中文](2026-08-01-windows-pwsh-default.zh.md)
## Problem
The harness's shipped execution profile is bash-first on every platform. Windows hosts must install a bash shim (WSL or Git-Bash) or fall back to the POSIX-only `dsh-bash-local` behavior; the model-facing bash tool teaches the bash dialect, and the TUI/Web surfaces render terminal output in bash-shaped expectations. The first Windows-native foundation shipped in the [pwsh executor and tool decision](../../implemented/feature/2026-08-01-pwsh-tool-and-executor.md): a PowerShell implementation of the `ctx.bash` seam and a parity `pwsh` tool — but nothing yet defaults Windows hosts to them.
## Proposal
Two follow-up stages, each independently shippable. The bash-tool parity twin shipped with the [pwsh tool bash parity decision](../../implemented/feature/2026-08-02-pwsh-tool-bash-parity.md): `tool-pwsh` now mirrors `tool-bash` for foreground and background work minus the sandbox surface, shares the `DSH_*` environment through `dsh-bash-env`, and carries a keyless application snapshot of its assembled surface.
1. **Windows default composition** — the shipped CLI compositions mount `dsh-pwsh-local` as the `ctx.bash` executor and `dsh-tool-pwsh` as the model-facing shell tool on Windows hosts (bash unmounted there), while POSIX hosts keep the bash stack. This is a composition/roster decision in `base.cordis.yml` and the surface overlays, gated by platform; it makes the shipped Windows experience PowerShell-native end to end.
2. **pwsh GUI rendering** — the Web surface renders pwsh calls with the bash-shaped terminal presentation (terminal card with exit-status pill), the counterpart of the bash terminal cards. Shipped in the [pwsh UI presentation matches bash decision](../../implemented/feature/2026-08-05-pwsh-ui-bash-parity.md) with a keyless web lane; the TUI was removed, so no terminal twin remains. A PowerShell-aware presentation beyond bash parity (native path display, `$env:` facts) remains unclaimed.
The stages are ordered by dependency only where one exists: the rendering stage shipped first with the [pwsh UI presentation matches bash decision](../../implemented/feature/2026-08-05-pwsh-ui-bash-parity.md) because it is platform-independent and its keyless web lane runs on any host, while the Windows default composition remains the only unshipped stage. Nothing in this proposal changes POSIX behavior.
## Alternatives considered
**Default Windows to pwsh inside `dsh-bash-local` (one executor, dialect switch).** Rejected for the same reason the executor decision rejected a mode switch: the executor's identity is the shell it spawns, and platform-gated composition is a deployment choice, not an executor config.
**Ship the Windows default in the same change as the executor/tool.** Rejected: the roster change needs its own evidence (what breaks when the shipped Windows tree stops mounting bash, which tools depend on bash semantics), and it belongs to a composition decision with the approval/PTY surface visible.
**Keep bash on Windows via a shim and skip PowerShell defaults.** Rejected: it perpetuates the install-tax and the dialect mismatch the roadmap exists to remove; the shim is a deployment requirement, not a product behavior.
## Acceptance criteria
- A Windows host running the shipped `dsh` TUI/Web gets `pwsh` as its shell tool and PowerShell as the `ctx.bash` executor without configuration, and `bash` is absent from the model-visible roster there.
- POSIX hosts are byte-for-byte unaffected (same roster, same executor).
- The shipped-composition e2es assert the platform-gated roster on both families.
- Stage 1 lands with the keyless pwsh-tool snapshot already in place from the parity change; stage 2 landed with the web `pwsh-terminal` rendering lane (the TUI's removal left no terminal surface to snapshot).
## Risks
- **Bash-dependent composition rows** — any shipped plugin that assumes `bash` semantics (hook bridges executing shell hooks, workspace tooling) must be audited per stage; the audit may force a staged rollout rather than one switch.
- **Windows CI coverage gap** — unit coverage runs on Linux; Windows-only regressions in the pwsh stack surface through the Windows build/static lane and e2es, which must be extended per stage rather than assumed.
- **Rendering conventions** — the bash-shaped terminal twin shipped with the Web lane; a PowerShell-aware presentation beyond bash parity (native path display, `$env:` facts) remains a UI design decision with snapshot surface, deferred with stage 1.
@@ -1,39 +0,0 @@
# Agent Note: Windows 默认改用 pwsh(路线图)
Status: proposed
[English](2026-08-01-windows-pwsh-default.md) | 中文
## 问题
harness 交付的执行配置在每个平台都是 bash 优先。Windows 主机必须安装 bash 垫片(WSL 或 Git-Bash),或退回到仅 POSIX 的 `dsh-bash-local` 行为;面向模型的 bash 工具教的是 bash 方言,TUI/Web 界面按照 bash 风格的预期渲染终端输出。第一块 Windows 原生基础已随 [pwsh 执行器与工具决策](../../implemented/feature/2026-08-01-pwsh-tool-and-executor.md) 交付:`ctx.bash` seam 的 PowerShell 实现与对等的 `pwsh` 工具——但还没有任何东西让 Windows 主机默认使用它们。
## 提案
两个阶段,各自可独立交付。bash 工具对等孪生已随 [pwsh 工具与 bash 对齐决策](../../implemented/feature/2026-08-02-pwsh-tool-bash-parity.md) 交付:`tool-pwsh` 现在除 sandbox 接口外,在前台与后台工作方面均与 `tool-bash` 对齐,通过 `dsh-bash-env` 共享 `DSH_*` 环境,并携带其组装后形态的 keyless 应用快照。
1. **Windows 默认组合**——交付的 CLI(命令行界面)组合在 Windows 主机上挂载 `dsh-pwsh-local` 作为 `ctx.bash` 执行器、`dsh-tool-pwsh` 作为面向模型的 shell 工具(那里不挂载 bash),POSIX 主机保持 bash 栈。这是 `base.cordis.yml` 与 surface 覆盖层里按平台门控的组合/清单决策;它让交付的 Windows 体验端到端 PowerShell 原生。
2. **pwsh GUI 渲染**——Web 界面使用 bash 风格的终端呈现来渲染 pwsh 调用(带胶囊状退出状态标签的终端卡片),与 bash 终端卡片相对应。已随 [pwsh UI 呈现与 bash 对齐决策](../../implemented/feature/2026-08-05-pwsh-ui-bash-parity.md) 及 keyless web 通道交付;TUI 已移除,不再有对应的终端界面。超出 bash 对齐的 PowerShell 感知呈现(原生路径显示、`$env:` 信息)仍无人认领。
各阶段仅在有依赖关系时排序:渲染阶段已随 [pwsh UI 呈现与 bash 对齐决策](../../implemented/feature/2026-08-05-pwsh-ui-bash-parity.md) 先行交付(平台无关,其 keyless web 通道可在任意宿主运行),而 Windows 默认组合仍是唯一未交付的阶段。本提案不改变任何 POSIX 行为。
## 备选方案
**在 `dsh-bash-local` 内部让 Windows 默认 pwsh(一个执行器,方言开关)。** 否决,理由与执行器决策否决模式开关相同:执行器的身份就是它 spawn 的 shell,而按平台门控的组合是部署选择,不是执行器配置。
**把 Windows 默认与执行器/工具一起交付。** 否决:清单变更需要自己的证据(交付的 Windows 树停挂 bash 后什么会坏、哪些工具依赖 bash 语义),并且它属于会在审批/PTY 界面上显现的组合决策。
**用垫片在 Windows 上保留 bash,跳过 PowerShell 默认。** 否决:这延续了安装税与路线图要消除的方言错配;垫片是部署要求,不是产品行为。
## 验收标准
- 运行交付版 `dsh` TUI/Web 的 Windows 主机无需配置即获得 `pwsh` 作为其 shell 工具、PowerShell 作为 `ctx.bash` 执行器,且那里的模型可见清单中没有 `bash`
- POSIX 主机逐字节不受影响(清单相同,执行器相同)。
- 交付组合 e2e 在两个平台族上断言按平台门控的清单。
- 阶段 1 落地时,parity 变更带来的 keyless pwsh 工具快照已经就位;阶段 2 已随 web `pwsh-terminal` 渲染通道落地(TUI 的移除意味着不再有可供快照测试的终端界面)。
## 风险
- **依赖 bash 的组合行**——任何假设 bash 语义的交付插件(执行 shell 钩子的钩子桥接、工作区工具)必须按阶段审计;审计可能迫使分阶段推出而非一次切换。
- **Windows CI 覆盖缺口**——单元覆盖在 Linux 上运行;pwsh 栈里仅 Windows 的回归通过 Windows 构建/静态通道与 e2e 暴露出来;这些覆盖必须按阶段扩展,不能想当然地认为已经具备。
- **渲染约定**——与 bash 风格一致的终端呈现已随 web 通道交付;超出 bash 对齐的 PowerShell 感知呈现(原生路径显示、`$env:` 信息)仍是一项需要快照覆盖的 UI 设计决策,随阶段 1 一起延期。
+3
View File
@@ -34,6 +34,8 @@
"@deepseek-ai/dsh-plan-mode": "workspace:^",
"@deepseek-ai/dsh-pty": "workspace:^",
"@deepseek-ai/dsh-pty-local": "workspace:^",
"@deepseek-ai/dsh-pwsh-local": "workspace:^",
"@deepseek-ai/dsh-pwsh-sandbox": "workspace:^",
"@deepseek-ai/dsh-session-reference": "workspace:^",
"@deepseek-ai/dsh-skill": "workspace:^",
"@deepseek-ai/dsh-skill-local": "workspace:^",
@@ -47,6 +49,7 @@
"@deepseek-ai/dsh-tool-fs": "workspace:^",
"@deepseek-ai/dsh-tool-fs-search": "workspace:^",
"@deepseek-ai/dsh-tool-goal": "workspace:^",
"@deepseek-ai/dsh-tool-pwsh": "workspace:^",
"@deepseek-ai/dsh-tool-ralph": "workspace:^",
"@deepseek-ai/dsh-tool-skill": "workspace:^",
"@deepseek-ai/dsh-tool-str-replace-editor": "workspace:^",
+7
View File
@@ -15,6 +15,7 @@ import {
type ConfigDumpLayer,
} from '@deepseek-ai/dsh-app-boot'
import { homePatchPath, prepareProfile, PROFILE_ROOT_FILENAME } from './profile-boot.ts'
import { resolveWindowsShellLayer } from './windows-shell.ts'
const NAME = 'dsh'
@@ -33,6 +34,12 @@ export function runDumpConfig(profile: string, defaultOnly: boolean, patches: re
label: layer.packageName,
patches: layer.patches,
}))
// The win32 shell platform layer rides between bundles and user layers,
// exactly where the boot applies it.
const windowsShellLayer = resolveWindowsShellLayer(process.platform, loaded.layers, NAME)
if (windowsShellLayer !== undefined) {
layers.push({ label: windowsShellLayer.label, patches: windowsShellLayer.patches })
}
if (!defaultOnly) {
if (existsSync(loaded.patchPath)) {
layers.push({ label: loaded.patchPath, patches: loaded.patches })
+17 -5
View File
@@ -35,6 +35,7 @@ const USER_PRESET_DIR = '.agent-presets'
import { DSH_ENVIRONMENT_KEY, type EnvironmentSnapshot } from '@deepseek-ai/dsh-environment'
import type { HeadlessIo } from '@deepseek-ai/dsh-headless'
import { createProcessShutdown, type ProcessShutdown } from './process-shutdown.ts'
import { resolveWindowsShellLayer } from './windows-shell.ts'
const NAME = 'dsh'
@@ -111,6 +112,8 @@ interface ComposedProfile {
profile: Profile
/** Bundle layers concatenated — the part below the user layers on a live reload. */
bundlePatches: PatchOptions[]
/** The win32 shell platform layer (the base bundle's `windows.cordis.patch.yml`), between bundles and user layers. */
windowsShellPatches: PatchOptions[]
/** The home-level user layer (`$DSH_HOME/cordis.patch.yml`), applied after the profile's own. */
homePatches: PatchOptions[]
/** Layers above the user layers on a live reload: --patch overlays, flag patches, the telemetry switch. */
@@ -125,12 +128,19 @@ interface ComposedProfile {
/** The full patch stack of one composed profile, in application order. */
function allPatches(composed: ComposedProfile): PatchOptions[] {
return [...composed.bundlePatches, ...composed.profile.patches, ...composed.homePatches, ...composed.overlayAndFlags]
return [
...composed.bundlePatches,
...composed.windowsShellPatches,
...composed.profile.patches,
...composed.homePatches,
...composed.overlayAndFlags,
]
}
/**
* Load `name` and compose its effective patch stack: bundle layers in
* `dsh.profile.bundles` order, the profile's user layer, the home-level user layer
* `dsh.profile.bundles` order, the win32 shell platform layer (when the host
* is Windows), the profile's user layer, the home-level user layer
* (`$DSH_HOME/cordis.patch.yml` — machine-local preferences that apply to
* every profile, so it outranks the per-profile layer), `--patch` overlays,
* then flag patches derived from the composed rows, then the telemetry
@@ -149,8 +159,9 @@ function composeProfile(
const homePatches = loadOptionalPatches(NAME, homePatchPath()) ?? []
const overlays = patchFiles.flatMap(file => loadOverlayPatches(NAME, resolve(file)))
const bundlePatches = profile.layers.flatMap(layer => layer.patches)
const windowsShellPatches = resolveWindowsShellLayer(process.platform, profile.layers, NAME)?.patches ?? []
const rows = new Map<string, { name?: string; config?: unknown }>()
for (const row of composeEntries([bundlePatches, profile.patches, homePatches, overlays])) {
for (const row of composeEntries([bundlePatches, windowsShellPatches, profile.patches, homePatches, overlays])) {
if (typeof row.id === 'string') rows.set(row.id, row)
}
const overlayAndFlags = [...overlays, ...deriveFlagPatches(rows)]
@@ -174,7 +185,7 @@ function composeProfile(
}
const telemetryPatch = resolveTelemetryPatch(process.env.DSH_TELEMETRY_DISABLED, rows.has(TELEMETRY_ROW_ID))
if (telemetryPatch !== undefined) overlayAndFlags.push(telemetryPatch)
return { profile, bundlePatches, homePatches, overlayAndFlags, rows }
return { profile, bundlePatches, windowsShellPatches, homePatches, overlayAndFlags, rows }
}
/** Options for {@link runProfile}. */
@@ -232,7 +243,7 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
shutdown.interrupt(code)
}
// Signals own teardown throughout the startup window, not only after boot()
// settles: an inserted front door can publish readiness before sibling rows
// settles: an inserted entry point can publish readiness before sibling rows
// finish mounting.
process.on('SIGTERM', () => { interrupt(options.task === undefined ? 0 : 143) })
process.on('SIGINT', () => { interrupt(130) })
@@ -253,6 +264,7 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
// removing the override could never revert the row to the bundle default.
const composeLive = (): PatchOptions[] => structuredClone([
...composed.bundlePatches,
...composed.windowsShellPatches,
...loadOptionalPatches(NAME, composed.profile.patchPath) ?? [],
...loadOptionalPatches(NAME, homePatchPath()) ?? [],
...composed.overlayAndFlags,
+52
View File
@@ -0,0 +1,52 @@
/**
* The Windows shell platform layer: on win32 hosts the shipped profile
* compositions swap the POSIX-only bash stack for the sandbox-confined
* PowerShell stack (`@deepseek-ai/dsh-pwsh-sandbox` +
* `@deepseek-ai/dsh-tool-pwsh`). The layer is the base bundle's
* `windows.cordis.patch.yml`, injected by the launcher between the bundle
* layers and the user layers so a user patch can still override it — the
* only override channel is composition config, like every other roster
* decision. POSIX hosts never receive the layer.
* @module @deepseek-ai/dsh/windows-shell
*/
import { join } from 'node:path'
import type { PatchOptions } from '@cordisjs/plugin-include'
import { loadOverlayPatches, type ProfileLayer } from '@deepseek-ai/dsh-app-boot'
/** The base bundle whose package carries the Windows shell patch. */
export const BASE_BUNDLE = '@deepseek-ai/dsh-base'
/** The Windows shell patch filename inside the base bundle package. */
export const WINDOWS_SHELL_PATCH_FILENAME = 'windows.cordis.patch.yml'
/** One Windows shell platform layer: its patch file and parsed patches. */
export interface WindowsShellLayer {
/** The patch file path, used as the config-dump provenance label. */
label: string
/** The parsed patch entries, applied after the bundle layers. */
patches: PatchOptions[]
}
/**
* Resolve the Windows shell platform layer for a profile composition.
* @param platform - the host platform (`process.platform` at call sites).
* @param layers - the profile's bundle layers, in application order.
* @param binName - the diagnostic prefix on thrown errors (`dsh`).
* @returns the pwsh layer on win32, else `undefined`. A custom profile that
* mounts no base bundle is skipped (it owns its shell stack); a base
* bundle whose Windows shell patch is missing fails loud in
* {@link loadOverlayPatches} — the shipped package always carries it, so
* a miss is a broken installation.
*/
export function resolveWindowsShellLayer(
platform: NodeJS.Platform,
layers: readonly ProfileLayer[],
binName: string,
): WindowsShellLayer | undefined {
if (platform !== 'win32') return undefined
const base = layers.find(layer => layer.packageName === BASE_BUNDLE)
if (base === undefined) return undefined
const label = join(base.packageDir, WINDOWS_SHELL_PATCH_FILENAME)
return { label, patches: loadOverlayPatches(binName, label) }
}
+139
View File
@@ -0,0 +1,139 @@
import { afterEach, describe, expect, it } from 'vitest'
import { mkdtempSync, writeFileSync, rmSync, mkdirSync, readFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import type { ProfileLayer } from '@deepseek-ai/dsh-app-boot'
import { composeEntries, initProfile, loadProfile, PROFILES_DIR } from '@deepseek-ai/dsh-app-boot'
import {
BASE_BUNDLE,
resolveWindowsShellLayer,
WINDOWS_SHELL_PATCH_FILENAME,
} from '../src/windows-shell.ts'
const WINDOWS_PATCH = `- id: bash-sandbox
disabled: true
- insert:
- id: pwsh-sandbox
name: '@deepseek-ai/dsh-pwsh-sandbox'
`
/** One fake bundle layer rooted in a temp directory. */
function fakeLayer(packageName: string, dir: string): ProfileLayer {
return { packageName, packageDir: dir, patchPath: join(dir, 'cordis.patch.yml'), patches: [] }
}
/** A base bundle layer whose package carries the Windows shell patch. */
function baseLayerWithPatch(dir: string): ProfileLayer {
writeFileSync(join(dir, WINDOWS_SHELL_PATCH_FILENAME), WINDOWS_PATCH)
return fakeLayer(BASE_BUNDLE, dir)
}
describe('resolveWindowsShellLayer', () => {
let base: string
afterEach(() => { if (base !== undefined) rmSync(base, { recursive: true, force: true }) })
const tempBase = (): string => {
base = mkdtempSync(join(tmpdir(), 'dsh-windows-shell-'))
return base
}
it('never applies on POSIX hosts', () => {
expect(resolveWindowsShellLayer('linux', [baseLayerWithPatch(tempBase())], 'dsh')).toBeUndefined()
expect(resolveWindowsShellLayer('darwin', [baseLayerWithPatch(tempBase())], 'dsh')).toBeUndefined()
})
it('defaults Windows hosts to the pwsh platform layer', () => {
const layer = resolveWindowsShellLayer('win32', [baseLayerWithPatch(tempBase())], 'dsh')
expect(layer).toBeDefined()
expect(layer?.label.endsWith(WINDOWS_SHELL_PATCH_FILENAME)).toBe(true)
expect(layer?.patches).toEqual([
{ id: 'bash-sandbox', disabled: true },
{ insert: [{ id: 'pwsh-sandbox', name: '@deepseek-ai/dsh-pwsh-sandbox' }] },
])
})
it('skips custom profiles without a base bundle', () => {
const other = fakeLayer('@deepseek-ai/dsh-custom', tempBase())
expect(resolveWindowsShellLayer('win32', [other], 'dsh')).toBeUndefined()
})
it('fails loud when the base bundle ships no Windows shell patch', () => {
const base = tempBase()
mkdirSync(base, { recursive: true })
// The overlay loader owns the fail-loud contract: the caller named this
// file, so its absence is a misconfiguration, not "no overlay".
expect(() => resolveWindowsShellLayer('win32', [fakeLayer(BASE_BUNDLE, base)], 'dsh'))
.toThrow(/dsh: failed to read overlay .*windows\.cordis\.patch\.yml/)
})
})
describe('the shipped Windows composition (real bundle layers)', () => {
let home: string
afterEach(() => { if (home !== undefined) rmSync(home, { recursive: true, force: true }) })
// The app installation anchor, mirroring profile-boot.ts: the bundle layers
// resolve from the REAL dsh-base/dsh-web-app packages through it, so this
// suite composes the shipped patch files, not test fixtures.
const anchor = fileURLToPath(new URL('../package.json', import.meta.url))
it('composes the win32 confined roster through the real patch layers', () => {
home = mkdtempSync(join(tmpdir(), 'dsh-windows-home-'))
initProfile(join(home, PROFILES_DIR, 'web'), ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'])
const profile = loadProfile('dsh', 'web', anchor, home)
const warnings: string[] = []
const win32 = resolveWindowsShellLayer('win32', profile.layers, 'dsh')
expect(win32).toBeDefined()
const rows = composeEntries(
[...profile.layers.map(layer => layer.patches), win32!.patches],
message => warnings.push(message),
)
const byId = new Map(rows.map(row => [row.id, row]))
// Only the POSIX bash stack leaves the roster: the permission surface
// (sandbox/sandbox-policy/fs-sandbox, permission, approval) stays enabled
// exactly as on POSIX — the confined pwsh executor is what changes.
for (const id of ['bash-sandbox', 'tool-bash']) {
expect(byId.get(id)?.disabled, `row ${id}`).toBe(true)
}
for (const id of ['permission', 'ui-permission', 'sandbox', 'sandbox-policy', 'fs-sandbox', 'approval']) {
expect(byId.get(id)?.disabled, `row ${id}`).not.toBe(true)
}
for (const id of ['pwsh-sandbox', 'tool-pwsh']) {
expect(byId.has(id), `inserted row ${id}`).toBe(true)
}
// The launcher's cold-start module fallback BFS-links the apps/cli
// dependency closure into the profile's node_modules (the pwsh-local
// precedent), so every inserted bare plugin must resolve from there.
const cliManifest = JSON.parse(readFileSync(anchor, 'utf8')) as { dependencies?: Record<string, string> }
for (const name of ['@deepseek-ai/dsh-pwsh-sandbox', '@deepseek-ai/dsh-tool-pwsh']) {
expect(cliManifest.dependencies?.[name], `cold-start closure must reach ${name}`).toBeDefined()
}
// The patch touches only base-owned rows plus inserts, so the full web
// profile composes without any no-match warning.
expect(warnings).toEqual([])
})
it('leaves POSIX untouched and base-only profiles compose without warnings', () => {
home = mkdtempSync(join(tmpdir(), 'dsh-windows-home-'))
initProfile(join(home, PROFILES_DIR, 'web'), ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'])
const profile = loadProfile('dsh', 'web', anchor, home)
// POSIX: no platform layer, the bash stack stays enabled.
const posixRows = composeEntries(profile.layers.map(layer => layer.patches))
const posixById = new Map(posixRows.map(row => [row.id, row]))
expect(posixById.get('bash-sandbox')?.disabled).not.toBe(true)
expect(posixById.has('pwsh-local')).toBe(false)
expect(posixById.has('pwsh-sandbox')).toBe(false)
// A base-only custom profile (the DEFAULT_PROFILE_BUNDLES template): the
// patch touches only base-owned rows (bash-sandbox/tool-bash) plus its
// inserts, so the composition produces no no-match warning.
initProfile(join(home, PROFILES_DIR, 'base-only'), ['@deepseek-ai/dsh-base'])
const baseOnly = loadProfile('dsh', 'base-only', anchor, home)
const baseWarnings: string[] = []
const win32 = resolveWindowsShellLayer('win32', baseOnly.layers, 'dsh')
expect(win32).toBeDefined()
composeEntries(
[...baseOnly.layers.map(layer => layer.patches), win32!.patches],
message => baseWarnings.push(message),
)
expect(baseWarnings).toEqual([])
})
})
@@ -16,7 +16,7 @@
- paragraph: partial
- status: Deep diving...
- button "2 queued messages"
- textbox "Message the agent"
- textbox "Cmd/Ctrl+Enter steers all queued messages"
- button "Commands":
- img
- 'button "Access mode, current: Workspace Write"': Workspace Write
@@ -32,7 +32,7 @@
- tooltip "Save queued message"
- button "Cancel editing":
- img
- textbox "Message the agent"
- textbox "Cmd/Ctrl+Enter steers all queued messages"
- button "Commands":
- img
- 'button "Access mode, current: Workspace Write"': Workspace Write
@@ -31,7 +31,7 @@
- button "Clear goal":
- img
- button "2 queued messages"
- textbox "Message the agent"
- textbox "Cmd/Ctrl+Enter steers all queued messages"
- button "Commands":
- img
- 'button "Access mode, current: Workspace Write"': Workspace Write
@@ -25,7 +25,7 @@
- img
- button "Steer queued message":
- img
- textbox "Message the agent"
- textbox "Cmd/Ctrl+Enter steers all queued messages"
- button "Commands":
- img
- 'button "Access mode, current: Workspace Write"': Workspace Write
@@ -0,0 +1,35 @@
- banner:
- navigation "Session hierarchy":
- button "Use the ask_user_question tool to" [disabled]
- img
- text: 标准模式
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"
- text: Use the ask_user_question tool to ask me exactly one question with id "checkpoint", question "Ready to continue?", header "Checkpoint", and options labeled "Yes" and "No". After I answer, reply with one short sentence acknowledging my answer and stop. {{clock}}
- button "Copy":
- img
- button "Context injection @deepseek-ai/dsh-system-prompt":
- img
- img
- text: Context injection @deepseek-ai/dsh-system-prompt
- text: Running
- button "Think The user wants me to ask them a checkpoint question first, then continue with whatever they interject. Let me do exactly that.":
- img
- img
- text: Think The user wants me to ask them a checkpoint question first, then continue with whatever they interject. Let me do exactly that.
- status: Deep diving...
- text: "Interjection Interjection: include the word BANANA in your final reply."
- button "Copy":
- img
- text: "Interjection Interjection: include the word ORANGE in your final reply."
- button "Copy":
- img
- textbox "Message the agent"
- button "Commands":
- img
- 'button "Access mode, current: Workspace Write"': Workspace Write
- button "Select model, current DeepSeek-V4-Flash":
- text: DeepSeek-V4-Flash
- img
- button "Stop generating"
@@ -0,0 +1,47 @@
[
{
"kind": "chunks",
"chunks": [
{ "type": "block-start", "index": 0, "blockType": "reasoning" },
{ "type": "reasoning-delta", "index": 0, "text": "The user wants me to ask them a checkpoint question first, then continue with whatever they interject. Let me do exactly that." },
{ "type": "block-start", "index": 1, "blockType": "tool-call" },
{
"type": "tool-call-delta",
"index": 1,
"id": "call_00_steer_all",
"name": "ask_user_question",
"argumentsDelta": "{\"questions\": [{\"id\": \"checkpoint\", \"question\": \"Ready to continue?\", \"header\": \"Checkpoint\", \"options\": [{\"label\": \"Yes\"}, {\"label\": \"No\"}]}]}"
},
{
"type": "block-end",
"index": 0,
"block": {
"type": "reasoning",
"text": "The user wants me to ask them a checkpoint question first, then continue with whatever they interject. Let me do exactly that."
}
},
{
"type": "block-end",
"index": 1,
"block": {
"type": "tool-call",
"id": "call_00_steer_all",
"name": "ask_user_question",
"arguments": "{\"questions\": [{\"id\": \"checkpoint\", \"question\": \"Ready to continue?\", \"header\": \"Checkpoint\", \"options\": [{\"label\": \"Yes\"}, {\"label\": \"No\"}]}]}"
}
},
{ "type": "usage", "usage": { "inputTokens": 10, "outputTokens": 10, "cacheReadTokens": 0, "reasoningTokens": 0 } },
{ "type": "finish", "reason": { "kind": "tool-calls" } }
]
},
{
"kind": "chunks",
"chunks": [
{ "type": "block-start", "index": 0, "blockType": "text" },
{ "type": "text-delta", "index": 0, "text": "Got it: BANANA and ORANGE." },
{ "type": "block-end", "index": 0, "block": { "type": "text", "text": "Got it: BANANA and ORANGE." } },
{ "type": "usage", "usage": { "inputTokens": 10, "outputTokens": 10, "cacheReadTokens": 0, "reasoningTokens": 0 } },
{ "type": "finish", "reason": { "kind": "stop" } }
]
}
]
@@ -0,0 +1,45 @@
- banner:
- navigation "Session hierarchy":
- button "Use the ask_user_question tool to" [disabled]
- img
- text: 标准模式
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"
- text: Use the ask_user_question tool to ask me exactly one question with id "checkpoint", question "Ready to continue?", header "Checkpoint", and options labeled "Yes" and "No". After I answer, reply with one short sentence acknowledging my answer and stop. {{clock}}
- button "Copy":
- img
- button "Context injection @deepseek-ai/dsh-system-prompt":
- img
- img
- text: Context injection @deepseek-ai/dsh-system-prompt
- button "Think The user wants me to ask them a checkpoint question first, then continue with whatever they interject. Let me do exactly that.":
- img
- img
- text: Think The user wants me to ask them a checkpoint question first, then continue with whatever they interject. Let me do exactly that.
- button "Ask question 1/1 answered":
- img
- img
- text: Ask question 1/1 answered
- text: "Interjection Interjection: include the word BANANA in your final reply. {{clock}}"
- button "Copy":
- img
- text: "Interjection Interjection: include the word ORANGE in your final reply. {{clock}}"
- button "Copy":
- img
- paragraph: "Got it: BANANA and ORANGE."
- button "Copy":
- img
- button "Branch into a new conversation":
- img
- text: {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s
- textbox "Message the agent"
- button "Commands":
- img
- 'button "Access mode, current: Workspace Write"': Workspace Write
- button "Select model, current DeepSeek-V4-Flash":
- text: DeepSeek-V4-Flash
- img
- button "0% of context used"
- button "Send message" [disabled]
- text: 1 turns · 2 steps LLM {{duration}} · Tool call {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 0% Input 20 tok · Output 20 tok
+112
View File
@@ -34,6 +34,18 @@ const REPLAY_PACE_MS = 100
const PROMPT = 'Use the ask_user_question tool to ask me exactly one question with id "checkpoint", question "Ready to continue?", header "Checkpoint", and options labeled "Yes" and "No". After I answer, reply with one short sentence acknowledging my answer and stop.'
const STEER = 'Interjection: include the word BANANA in your final reply.'
// Empty-draft flush scenario: an override-only fixture. The whole-script
// replacement answers both model calls of a FRESH session (no recorded
// session.jsonl exists — call 0 keeps the turn open with a question-tool
// call, call 1 is the reply after both steerings drain).
const STEER_ALL_DIR = fileURLToPath(new URL('./snapshots/steer-all', import.meta.url))
const STEER_ALL_FIXTURE = join(STEER_ALL_DIR, 'session.jsonl')
const STEER_ALL_OVERRIDE = join(STEER_ALL_DIR, 'replay.override.json')
const STEER_ALL_MID = join(STEER_ALL_DIR, 'mid-steer.expected.md')
const STEER_ALL_SETTLED = join(STEER_ALL_DIR, 'settled.expected.md')
const STEER_ONE = 'Interjection: include the word BANANA in your final reply.'
const STEER_TWO = 'Interjection: include the word ORANGE in your final reply.'
/** Concatenated assistant text deltas — the model-visible reply body. */
function assistantText(events: SessionEvent[]): string {
return events
@@ -278,3 +290,103 @@ describe('web e2e: composer shortcut follows the swapped busy behavior', () => {
expect(tripwire.warnings).toEqual([])
}, 90_000)
})
describe('web e2e: empty-draft Cmd+Enter steers the whole queue', () => {
let scaffold: WebScaffold
let browser: Browser
let page: Page
let tripwire: ReturnType<typeof watchConsole>
const sessionEvents: SessionEvent[] = []
beforeAll(async () => {
// The scenario boots a fresh session against the override-only fixture;
// the replay.override.json sidecar replaces the derived script, so the
// (deliberately absent) session.jsonl is never read.
scaffold = await launchWebScaffold({
replayFixture: STEER_ALL_FIXTURE,
replayOverride: STEER_ALL_OVERRIDE,
paceMs: REPLAY_PACE_MS,
})
scaffold.ctx.on('session/event', (_session, event) => { sessionEvents.push(event) })
browser = await chromium.launch()
page = await newEnglishPage(browser)
tripwire = watchConsole(page)
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
await connectFreshWorkspace(page, scaffold.workspaceCwd)
await page.getByText('标准模式', { exact: true }).waitFor({ timeout: 10_000 })
}, 120_000)
afterAll(async () => {
await browser?.close()
await scaffold?.close()
})
it.skipIf(MODE === 'record')('queues two messages, then flushes both with an empty-draft Cmd+Enter', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-steer-all'))
const input = page.locator('textarea').first()
await input.waitFor({ timeout: 10_000 })
const settled = scaffold.whenTurnSettled(30_000)
// Call 0 streams a question-tool call; the fills must land inside the
// first replay window, before the question composer replaces the textarea.
await input.fill(PROMPT)
await input.press('Enter')
await input.fill(STEER_ONE)
await input.press('Enter')
await input.fill(STEER_TWO)
await input.press('Enter')
const dock = page.locator('[data-queue-dock]')
// Both messages queued: the two-row dock shows a collapsed count header,
// and Playwright text matching skips the hidden rows — expand the list,
// then assert each row's content.
await dock.getByText('2 queued messages').waitFor({ timeout: 10_000 })
await dock.getByRole('button').click()
await dock.getByText(STEER_ONE, { exact: true }).waitFor({ timeout: 10_000 })
await dock.getByText(STEER_TWO, { exact: true }).waitFor({ timeout: 10_000 })
expect(await page.locator('[data-pending-steering]').count()).toBe(0)
// Empty draft + Cmd+Enter: both queued rows steer in FIFO order, the dock
// empties, and the pending steering renders at the conversation tail.
await input.press('Meta+Enter')
await expect.poll(
() => page.locator('[data-pending-steering]').filter({ hasText: /BANANA|ORANGE/ }).count(),
{ timeout: 10_000 },
).toBe(2)
expect(await page.locator('[data-queue-dock]').count()).toBe(0)
// The reasoning row streams independently of the steering handoff; wait
// for it so the mid snapshot pins the assistant step, not the pre-render
// gap a fast machine can catch between steering acceptance and the block.
await page.locator('[data-variant="think"]').first().waitFor({ timeout: 10_000 })
const mid = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
await compareOrRefreshGolden(STEER_ALL_MID, mid, MODE)
// Answer the question; the step closes, the loop drains both steerings
// into one next-step request, and the final reply obeys both markers.
const composer = page.locator('[data-question-key]')
await composer.waitFor({ timeout: 30_000 })
await composer.getByRole('radio', { name: 'Yes' }).click()
await composer.getByRole('radio', { name: 'Yes' }).press('Enter')
await settled
const first = claimedMessages(sessionEvents, STEER_ONE)
const second = claimedMessages(sessionEvents, STEER_TWO)
expect(first).toHaveLength(1)
expect(second).toHaveLength(1)
expect(assistantText(sessionEvents)).toContain('BANANA')
expect(assistantText(sessionEvents)).toContain('ORANGE')
await expect.poll(() => page.getByText(STEER_ONE, { exact: true }).count(), { timeout: 15_000 }).toBe(1)
await expect.poll(() => page.getByText(STEER_TWO, { exact: true }).count(), { timeout: 15_000 }).toBe(1)
expect(await page.locator('[data-pending-steering]').count()).toBe(0)
const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
await compareOrRefreshGolden(STEER_ALL_SETTLED, snapshot, MODE)
expect(tripwire.pageErrors).toEqual([])
expect(tripwire.warnings).toEqual([])
}, 200_000)
it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => {
await assertFixtureInventory(STEER_ALL_DIR, [
'replay.override.json', 'mid-steer.expected.md', 'settled.expected.md',
])
})
})
+1 -1
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/architecture.md
architecture.md: 90d64fb0ac62020e13a7ce995c8e3273a5a9f906
architecture.md: ebf05397cb67cea336dd36a4d9416d43b6002d4e
architecture.zh.md: fec9a00484c495b0eed4773f262bb44543e304bd
+2 -2
View File
@@ -17,7 +17,7 @@ Harnesses are [Cordis](cordis-primer.md) contexts; packages contribute services,
| `ctx.systemPrompt` | `dsh-system-prompt` | ordered prompt sections, tool schemas, and variables |
| `ctx.tools` | `dsh-tools` | tool registry and [execution pipeline](tool-execution-pipeline.md) |
| `ctx.agents` | `dsh-agent` | live agents, delegated creation, `agent/*` events, process-local initiator scope |
| `ctx.agentDefaultModel` | [`dsh-agent-default-model`](../packages/core/agent-default-model/README.md) | Settings-backed model selection shared by Agent front doors |
| `ctx.agentDefaultModel` | [`dsh-agent-default-model`](../packages/core/agent-default-model/README.md) | Settings-backed model selection shared by Agent entry points |
| `ctx.agentLoop` | `dsh-agent-loop` | concrete `Agent` driver |
### Capability Services
@@ -164,7 +164,7 @@ Exceptions combine LLM Service Definition/Consumer roles, filesystem policy, web
### Bundles And Apps
`dsh-agent-spine-demo` bundles a spine and optional goals. App packages own CLI, ACP automation, and JSON-RPC front doors ([README](../packages/examples/agent-spine-demo/README.md), [acp/](../packages/acp/README.md), [interaction/](../packages/interaction/README.md)). `dsh-jsonrpc-agent` boots external `cordis.yml`; the Python SDK defaults when config is absent ([Python SDK](../python/README.md)). Thin deployments use swappable backends and optional tools ([examples/](../examples/AGENTS.md), [runnable wirings](cookbook/extension-cookbook.md#runnable-wirings), [graph atlas](graph-atlas.md)).
`dsh-agent-spine-demo` bundles a spine and optional goals. App packages own CLI, ACP automation, and JSON-RPC entry points ([README](../packages/examples/agent-spine-demo/README.md), [acp/](../packages/acp/README.md), [interaction/](../packages/interaction/README.md)). `dsh-jsonrpc-agent` boots external `cordis.yml`; the Python SDK defaults when config is absent ([Python SDK](../python/README.md)). Thin deployments use swappable backends and optional tools ([examples/](../examples/AGENTS.md), [runnable wirings](cookbook/extension-cookbook.md#runnable-wirings), [graph atlas](graph-atlas.md)).
### Agent Presets
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/capability-seams.md
capability-seams.md: 1646f5de10e815630add1f59fe65c5ac2ac109d1
capability-seams.zh.md: a523881c7a225f704fbd56f299f705ecd4926cf7
capability-seams.md: d5610b6bf6bcb39bfa146ada53ce0734a978b8cd
capability-seams.zh.md: 8a1b38d3600d673a8e0a99941e51d0973101a1c1

Some files were not shown because too many files have changed in this diff Show More