dsh-acp-enhanced 0.5.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README-zh.md CHANGED
@@ -51,7 +51,9 @@ ACP 线上。
51
51
  状态机为进行中 → 完成/失败
52
52
  - **Zed 文件与终端**:`zed_read_text_file` / `zed_write_text_file` / `zed_terminal` 把
53
53
  文件编辑放进 Zed 的"编辑文件"区(diff + 接受/拒绝)、命令跑在 Zed 真实终端
54
- - **原生表单提问**:`ask_user_question` → `elicitation/create` 表单,选项即点即答
54
+ - **原生表单提问**:`ask_user_question` → `elicitation/create` 表单,选项即点即答;
55
+ 选项带描述展示,每个带选项的问题附一个"自定义答案"输入框——选项都不合适时可自由输入,
56
+ 单选时自定义答案覆盖所选、多选时与所选并存(与 dsh 原生提问卡片语义一致)
55
57
  - **Plan 面板**:plan mode 开关 → Zed 底部"规划中"状态条
56
58
 
57
59
  ### 会话
@@ -70,7 +72,9 @@ ACP 线上。
70
72
  (列表以等宽代码块排版,一眼全见),其余(`/compact` `/goal` `/permission`
71
73
  `/plan`…)直通 harness 命令注册表,全部**不经过模型 turn** 即时执行。所有
72
74
  userInvocable 技能也会作为命令广播,`/ask-matt`、`/code-review`、`/tdd` 等能被
73
- 编辑器放行到达桥,技能正文按 dsh-tool-skill 的用户调用方式注入消息
75
+ 编辑器放行到达桥,技能正文按 dsh-tool-skill 的用户调用方式注入消息。斜杠命令
76
+ 旁粘贴的图片会作为命令附件随行(例如 `/goal` 目标的参考截图),与 Web 端
77
+ composer 的提交方式一致
74
78
 
75
79
  ### MCP
76
80
 
@@ -183,34 +187,107 @@ node scripts/acp-client.mjs # 官方默认路由,无需 env
183
187
  DSH_ACP_PROVIDER=... DSH_ACP_MODEL=... node scripts/acp-client.mjs # 自定义路由时再传
184
188
  ```
185
189
 
186
- ### 可选:web_search 走同一个网关
190
+ ### Web 搜索
187
191
 
188
- 若网关实现 OpenAI Responses 的 `web_search` 服务端工具,可把搜索也路由到网关(复用
189
- 同一凭据)。装子包并给 profile 的 `cordis.patch.yml` 追加两段:
192
+ bridge 自身不携带、也不推荐任何搜索 provider:模型侧 `web_search` 工具走 `web`
193
+ seam 的 `searchProvider`,往 profile 里挂任意 `ctx.web` provider 即可——带
194
+ `dsh.bundle` 的包用 `dsh plugin --profile acp-enhanced add <package>` 安装,普通包
195
+ 走用户层 `insert` 挂载(见下节)。你的 dsh 部署里有哪些 provider 是 profile 层的
196
+ 事,与 bridge 无关。
197
+
198
+ ### 管理 profile 的插件
199
+
200
+ dsh-acp-enhanced 跑在**独立的 profile** 里——`acp-enhanced`(由上面的安装命令创建于
201
+ `~/.dsh/profiles/acp-enhanced/`),与 `dsh web` 背后的 `web` profile 完全隔离,
202
+ 在这里增删改插件不会影响 web 侧的任何配置。
203
+
204
+ profile 的插件树由三层组合而成,后层修补前层:
205
+
206
+ 1. **bundle 层**:profile `package.json` 的 `dsh.profile.bundles`——模板自带的
207
+ `@deepseek-ai/dsh-base` 在前,随后是每个声明了 `dsh.bundle` 的已安装包(如
208
+ `dsh-acp-enhanced`),按数组顺序排列。
209
+ 2. **用户层**:`~/.dsh/profiles/acp-enhanced/cordis.patch.yml`——按 id 定位的行配置
210
+ 覆写、`disabled: true` 行禁用,以及 `insert` 挂载(无 `dsh.bundle` 的包——如手工
211
+ 挂载的自写 provider——就靠它装配)。
212
+ 3. **临时覆盖**:`dsh --profile acp-enhanced --patch extra.yml`。
213
+
214
+ 调整插件集:
190
215
 
191
216
  ```sh
192
- dsh plugin --profile acp-enhanced add dsh-web-search-openrouter
217
+ dsh plugin --profile acp-enhanced add <package> # 安装;声明 dsh.bundle 的包自动加入层栈
218
+ dsh plugin --profile acp-enhanced remove <package> # 卸载;自动退出层栈
219
+ dsh plugin --profile acp-enhanced update [package] # 更新一个/全部并 reconcile
220
+ dsh --profile acp-enhanced --dump-config # 查看组合后的完整树(标注每行来自哪一层)
193
221
  ```
194
222
 
195
- ```yaml
196
- - id: web
197
- config:
198
- searchProvider: openai-responses # 子包注册在 ctx.web 上的搜索 provider id(固定值)
199
-
200
- - insert:
201
- - id: web-search-openrouter
202
- name: 'dsh-web-search-openrouter'
203
- config:
204
- enabled: true
205
- baseURL: http://<gateway-host>:<port>/v1
206
- model: <your-model-id>
207
- apiKeyEnv: <KEY_ENV_NAME>
223
+ `dsh plugin` 本质是在 profile 目录里转发 pnpm,并在每次运行后按安装状态 reconcile
224
+ `dsh.profile.bundles`。两个值得知道的推论:
225
+
226
+ - **靠从 `bundles` 里删条目来禁用 bundle 是禁不住的**——包仍是已安装依赖,下一次
227
+ `dsh plugin` 运行会原样加回来。想不禁载地禁用某一行,请在用户层按**行 id**(不是
228
+ 包名,id 可在 `--dump-config` 输出里查)定位:
229
+
230
+ ```yaml
231
+ - id: mnemon
232
+ disabled: true
233
+ ```
234
+
235
+ - **无 `dsh.bundle` 的包自身不会装配**——它只作为普通依赖安装(带一次性警告),需要
236
+ 自己在用户层 `insert` 挂载;要改已有行的配置,用 `- id: <行>` + `config:` 覆写——
237
+ patch 条目是整行替换、不做合并。
238
+
239
+ 改动在**下一个**进程生效:Zed 为每个 agent 线程拉起一个全新的
240
+ `dsh --profile acp-enhanced`,编辑 profile 后新开 agent 线程(或重启 Zed)即可。
241
+
242
+ ## 兼容性
243
+
244
+ 同一个桥可运行在 **0.1.0-rc.6** 至 **0.1.2-rc.1** 的每一代 harness 上。0.1.2 线重写了本桥消费的多个
245
+ API,桥在运行期同时吸收各代——不分叉、不加版本开关:
246
+
247
+ | API | ≤ 0.1.1-rc.2(旧代) | ≥ 0.1.2-alpha.2(projection 代) | 桥的做法 |
248
+ |---|---|---|---|
249
+ | 会话的运行中 preset | `resolveSessionPreset({header, events})` 导出 | 导出已移除;`agentPreset` session projection | 自行折叠事件日志(最后一个 `agent-preset/selected` 胜出、header 兜底)——两代语义一致 |
250
+ | preset 解析失败 | `UnknownPresetError` / `PresetMountError` | `RemoteError`,错误码 `agent-preset/*` | `isPresetClientError`:RemoteError 按 `isDSHRemoteError` + `code` 鸭子类型识别;旧类按 `presetId` 结构识别(绝不跨副本 `instanceof`) |
251
+ | `permissionPresets.current(x)` | `current(events)` | `current(session)`(经 `permissionState`) | `currentPermissionMode` 每次调用前探测服务实例 |
252
+ | 会话事件日志读取 | 同步 `session.events` 数组 | `session.events` 已移除(0.1.2-rc.1);改为 `snapshotEvents()` / `ownEvents()` / `eventAt()` | `sessionEventsOf`:有 `snapshotEvents()` 用之,否则用活数组 |
253
+ | 注册表 `execute` 签名 | `execute(agent, line, signal)` | `execute(agent, line, images, signal)`(images 插在 line 与 signal 之间,0.1.1-rc.1 起) | `executeRegistryCommand` 按声明参数个数探测(`Remote` 装饰器不包裹方法) |
254
+ | `userQuestions` 注册 | `registerProvider({ask})` | `user-questions/request` Cordis waterfall(0.1.2-alpha.2 起) | 探测服务实例;waterfall 监听只应答本桥会话、其余经 `next()` 传递 |
255
+
256
+ 两条不变量保证其安全性(openma 的 `deepseek-harness-acp` 适配器独立得出了同样结论):**只值导入纯
257
+ helper**(`createUserMessage`、`ReasoningEffortId`、`SessionId`、`defineTool`…… 外来副本功能等价);**服务的代际问题按服务实例探测回答**——决定服务代际的是启动它的 CLI,不是本包的依赖范围。`dsh-agent-presets`
258
+ 按 *命名空间* 导入:0.1.2-alpha.1 删除了其命名导出,命名导入会在 ESM 链接期直接失败。
259
+
260
+ 从干净依赖树校验两代:
261
+
262
+ ```sh
263
+ node scripts/compat-check.mjs # 分别安装 0.1.0-rc.6 与 0.1.2-alpha.2+ 两套,逐一导入本桥
264
+ ```
265
+
266
+ ### 开发检出:仓库锁定 CLI + 独立 home
267
+
268
+ 启动器**从检出目录**(`link:` 安装)运行时,按以下顺序解析 dsh CLI:
269
+
270
+ 1. `$DSH_PATH` —— 显式指定的 dsh 二进制,或其 `node_modules/.bin/dsh` 内含 dsh 的目录
271
+ 2. 仓库锁定的 CLI —— `<repo>/node_modules/.bin/dsh`(本包的 `@deepseek-ai/dsh`
272
+ devDependency,当前 0.1.2-rc.1)
273
+ 3. 全局兜底 —— PATH / npx 缓存 / npm 前缀 里的 `dsh`(旧行为;未 `pnpm install` 的全新检出退化为它)
274
+
275
+ 命中 (1) 或 (2) 时,profile 在**独立 home**(`DSH_ACP_HOME`,默认 `~/.dsh-acp`)下启动:dsh
276
+ 每次启动都会把自身依赖闭包 heal 进 `$DSH_HOME/profiles/node_modules`——该目录被同 home 下所有
277
+ profile 共享、内容随最后启动的 CLI 翻转——因此第二个 CLI 代际不得与运行中的 `dsh web`
278
+ 等共享 home。此路径永不触碰默认 home。harness 注入到子进程的 `DSH_HOME=$HOME/.dsh`
279
+ (dsh 会向每个 agent/工具进程导出它)会被识别并覆盖而非沿用;只有指向默认 home 之外的
280
+ `DSH_HOME` 才被尊重;确要将锁定 CLI 跑在默认 home 上,请显式设 `DSH_ACP_HOME=$HOME/.dsh`。
281
+
282
+ 一次性引导独立 home(建**不含** `dsh-mnemon` 的 profile——它不支持 0.1.2-alpha harness——并逐字移植旧
283
+ profile 的用户层行、迁移凭据/设置、挂 0.1.2-alpha `standard` preset 所需的
284
+ `subagent-model-selection-settings` 宿主服务、关闭 DeepSeek 插件清单上报):
285
+
286
+ ```sh
287
+ scripts/init-acp-home.sh # 幂等;重跑不会覆盖你的文件
208
288
  ```
209
289
 
210
- > ⚠️ `searchProvider` 必须**精确等于** `openai-responses`——这是
211
- > `dsh-web-search-openrouter` 注册在 `ctx.web` 上的搜索 provider id,**不是**网关的
212
- > LLM provider id(即上面 `DSH_ACP_PROVIDER` 填的那个)。web 插件按 id 精确匹配,
213
- > 填错时配置期不会报错,直到首次搜索才抛 `WEB_PROVIDER_CONFIGURED_MISSING`。
290
+ 两代 harness 都把会话持久化在 `$DSH_HOME/sessions/<slug>/<id>/session.jsonl.zstd`,且新代可读旧代日志(已验证:历史回放与 preset 折叠跨代工作)。因此旧线程只需把会话历史拷到新 home——`scripts/init-acp-home.sh` 会打印这条命令(或加 `--copy-sessions`);默认不拷贝,因为默认 home 的目录里还有全部 web profile 会话。
214
291
 
215
292
  ## 故障排查
216
293
 
@@ -218,6 +295,9 @@ dsh plugin --profile acp-enhanced add dsh-web-search-openrouter
218
295
  |---|---|
219
296
  | `exec: dsh: not found`(status 127) | 用随附 `dsh-acp-zed.sh` 启动器(自定位 node/dsh) |
220
297
  | `no API key for provider route "xxx"` | 写入 `~/.dsh/.credentials.yaml`,或在 agent_servers 里设 `env.DEEPSEEK_API_KEY` |
298
+ | `SyntaxError: ... 'PresetMountError'` | 你在 0.1.2-alpha 宿主上运行 0.7.0 之前的桥副本——升级本包 |
299
+ | `modelSelectionSettings requires ... in the Host scope` | 0.1.2-alpha 宿主缺少 `subagent-model-selection-settings` 行——运行 `scripts/init-acp-home.sh`(或按脚本模板在用户层补 insert 行) |
300
+ | 宿主升级后旧线程变空白 | 会话存放在 `$DSH_HOME/sessions/<slug>/`;把旧 home 的历史拷进独立 home(`scripts/init-acp-home.sh --copy-sessions`)即可继续 |
221
301
  | 无法切换模型 | 保存的 `reasoning_effort` 默认值(或会话当前 effort)被带到新模型上。0.3.6 起本桥按模型记住上次使用的强度(随 profile 持久化):不被新模型支持的 effort 会被该模型记忆值替换——没有记忆则回退其默认值,再无默认则取第一个可选值,既不会切换失败也不会出现 "unknown"。另检查:是否选到了不可路由的"幽灵 provider"——本桥默认过滤(只广播 `config.provider` 的模型),确认 profile 的 provider 指向真实路由 |
222
302
  | 上下文用量不显示 | 选到了不可路由的"幽灵 provider";本桥默认过滤(只广播 `config.provider` 的模型),确认 profile 的 provider 指向真实路由 |
223
303
  | 需要详细诊断 | `ACP_DEBUG=1 dsh --profile acp-enhanced`(stderr 生命周期 trace) |
@@ -225,6 +305,7 @@ dsh plugin --profile acp-enhanced add dsh-web-search-openrouter
225
305
  ## 开发
226
306
 
227
307
  ```sh
308
+ node scripts/compat-check.mjs # 跨代链接检查(0.1.0-rc.6 + 0.1.2-alpha.2+ 临时安装)
228
309
  node scripts/acp-client.mjs # 端到端冒烟(需要 API key)
229
310
  node scripts/acp-client-tools.mjs # 客户端工具测试(模拟 Zed 的 fs/terminal/elicitation/plan)
230
311
  node scripts/acp-mcp-test.mjs # MCP 挂载测试(无模型调用)
@@ -233,8 +314,21 @@ node scripts/acp-resume-test.mjs # 会话恢复测试
233
314
  node scripts/codec-image-test.mjs # 图片编解码单元测试(无网络,假 store)
234
315
  node scripts/terminal-codec-test.mjs # 终端卡片编解码单元测试(无网络)
235
316
  node scripts/acp-image-e2e.mjs # 图片能力端到端(vision 模型段需 API key)
317
+ scripts/init-acp-home.sh # 引导/刷新独立 home(~/.dsh-acp)
236
318
  ```
237
319
 
320
+ harness 包的 devDependency 与锁定的 `@deepseek-ai/dsh` CLI 声明相同的 range(如
321
+ `^0.1.2-rc.1`),让仓库依赖树与全新 CLI 安装解析出同一个连贯家族——在此用精确 patch
322
+ 锁定、与 CLI 的 range 闭包混存会得到分裂闭包(同名包两个版本),profile 启动时报
323
+ export-not-found。改这些锁定后务必整体重建 lockfile(`rm -rf node_modules pnpm-lock.yaml
324
+ && pnpm install`):原地增量安装既会留下污染 profile heal 的残留 store 条目,还会保留
325
+ lockfile 里的陈旧 peer 解析——从 0.1.2-alpha.2 原地升到 0.1.2-rc.1 时,rc.1 各包的
326
+ snapshot 里仍挂着 `dsh-session-persistence@0.1.2-alpha.3`(旧代 peer),boot 与
327
+ session/new 全部通过,直到第一个 turn 才以 `TypeError: Cannot read properties of
328
+ undefined (reading 'length')`(PersistenceCoordinator)崩掉。`pnpm-workspace.yaml` 放行
329
+ 了 CLI 闭包的构建脚本(node-pty prebuild、koffi)——仓库 CLI 启动 profile 时它们就是
330
+ 运行时依赖。
331
+
238
332
  ## 已知限制
239
333
 
240
334
  不支持音频附件(不声明 audio 能力)、文本按块粒度流式、每会话同时一个 in-flight
package/README.md CHANGED
@@ -59,7 +59,10 @@ over the ACP wire.
59
59
  put file edits into Zed's "edited files" area (diff + accept/reject) and commands into a
60
60
  real Zed terminal
61
61
  - **Native form questions**: `ask_user_question` → `elicitation/create` form, click an
62
- option, no typing
62
+ option — or type a custom answer when none of them fit: options render with their
63
+ descriptions, each option-backed question gets a free-text "Custom answer" field, and a
64
+ custom answer replaces the single selection / accompanies a multi-select (same semantics
65
+ as dsh's native question card)
63
66
  - **Plan panel**: plan mode toggle → "planning" status bar in Zed
64
67
 
65
68
  ### Sessions
@@ -82,7 +85,9 @@ over the ACP wire.
82
85
  through the harness command registry — all executed **without a model turn**. Every
83
86
  user-invocable skill is advertised as a command too, so `/ask-matt`, `/code-review`,
84
87
  `/tdd`, … reach the bridge instead of being rejected by the editor, and the skill's
85
- instructions are injected into the message (dsh-tool-skill-style user invocation)
88
+ instructions are injected into the message (dsh-tool-skill-style user invocation).
89
+ Images pasted next to a slash line ride along as command attachments (e.g. reference
90
+ screenshots for a `/goal` objective), the same way the Web composer submits them
86
91
 
87
92
  ### MCP
88
93
 
@@ -151,6 +156,21 @@ Zed spawns agents with a minimal PATH, so use the shipped launcher
151
156
  > store it in `~/.dsh/.credentials.yaml` (`DEEPSEEK_API_KEY`) and the dsh credentials
152
157
  > service resolves it; the launcher also falls back to a running `dsh web` process's key.
153
158
 
159
+ Debugging a stalled turn (is it the model request or the tool?):
160
+
161
+ ```jsonc
162
+ "env": {
163
+ // ...existing vars...
164
+ "ACP_LOG": "/Users/you/.dsh/dsh-acp-enhanced.trace.jsonl" // append-only JSONL event trace
165
+ }
166
+ ```
167
+
168
+ Each line is one session event with wall-clock `time` (ms epoch); a turn that appears to
169
+ hang is attributable afterwards: a **model request stall** shows a long gap between
170
+ `step/start` and the first `assistant/chunk`, while a **tool-execution stall** shows a
171
+ long gap between `tool/call` and `tool/result` (the result line carries `elapsedMs`).
172
+ `prompt/settled` lines cover the full user-message round trip (stopReason + elapsed).
173
+
154
174
  Optional: pin the panel's default config options (all still changeable in the panel):
155
175
 
156
176
  ```jsonc
@@ -202,36 +222,125 @@ node scripts/acp-client.mjs # official default route, no env;
202
222
  DSH_ACP_PROVIDER=... DSH_ACP_MODEL=... node scripts/acp-client.mjs # only for a custom route
203
223
  ```
204
224
 
205
- ### Optional: route web_search through the same gateway
225
+ ### Web search
226
+
227
+ The bridge ships no search provider and takes no position on which one you use: the
228
+ model-facing `web_search` tool rides on the `web` seam's `searchProvider`, so mount any
229
+ `ctx.web` provider into the profile — a package with `dsh.bundle` via
230
+ `dsh plugin --profile acp-enhanced add <package>`, or a plain package via your user-layer
231
+ `insert` rows (see below). Which provider exists in your dsh deployment is a profile
232
+ concern, not a bridge one.
233
+
234
+ ### Managing the profile's plugins
235
+
236
+ dsh-acp-enhanced runs in its **own profile** — `acp-enhanced`, created at
237
+ `~/.dsh/profiles/acp-enhanced/` by the install command above — fully separate from the
238
+ `web` profile behind `dsh web`, so plugin changes here never affect your web setup.
239
+
240
+ The profile composes its plugin tree from three sources, each layer patching the ones
241
+ before it:
242
+
243
+ 1. **Bundle layers** — `dsh.profile.bundles` in the profile's `package.json`: the
244
+ template's `@deepseek-ai/dsh-base` first, then every installed package that declares
245
+ `dsh.bundle` (like `dsh-acp-enhanced`), in array order.
246
+ 2. **Your user layer** — `~/.dsh/profiles/acp-enhanced/cordis.patch.yml`: id-targeted
247
+ row config overrides, `disabled: true` row disables, and `insert` lists (how a
248
+ package without `dsh.bundle` — e.g. a hand-mounted custom provider — gets
249
+ mounted).
250
+ 3. **Per-run overlays** — `dsh --profile acp-enhanced --patch extra.yml`.
206
251
 
207
- If the gateway implements the OpenAI Responses `web_search` server tool, you can route
208
- search through it too (reusing the same credential). Install the sub-package and append
209
- two blocks to the profile's `cordis.patch.yml`:
252
+ Adjust the set with:
210
253
 
211
254
  ```sh
212
- dsh plugin --profile acp-enhanced add dsh-web-search-openrouter
255
+ dsh plugin --profile acp-enhanced add <package> # install; a dsh.bundle package auto-joins the layer stack
256
+ dsh plugin --profile acp-enhanced remove <package> # uninstall; auto-leaves the stack
257
+ dsh plugin --profile acp-enhanced update [package] # update one/all, then reconcile
258
+ dsh --profile acp-enhanced --dump-config # inspect the composed tree (per-layer provenance)
213
259
  ```
214
260
 
215
- ```yaml
216
- - id: web
217
- config:
218
- searchProvider: openai-responses # the search provider id this sub-package registers on ctx.web (fixed value)
219
-
220
- - insert:
221
- - id: web-search-openrouter
222
- name: 'dsh-web-search-openrouter'
223
- config:
224
- enabled: true
225
- baseURL: http://<gateway-host>:<port>/v1
226
- model: <your-model-id>
227
- apiKeyEnv: <KEY_ENV_NAME>
261
+ `dsh plugin` is a thin pnpm forwarder (run inside the profile directory) that
262
+ reconciles `dsh.profile.bundles` against the installed state after every run. Two
263
+ consequences worth knowing:
264
+
265
+ - **Disabling a bundle by deleting it from `bundles` does not stick** — the package is
266
+ still an installed dependency, and the next `dsh plugin` run appends it right back.
267
+ To disable a single row without uninstalling, target it in the user layer by its
268
+ **row id** (not the package name — find ids in the `--dump-config` output):
269
+
270
+ ```yaml
271
+ - id: mnemon
272
+ disabled: true
273
+ ```
274
+
275
+ - **A package without `dsh.bundle` loads nothing by itself** — it installs as a plain
276
+ dependency (with a one-time warning) and needs your own `insert` entry in the user
277
+ layer. To change an existing row's config, override it with `- id: <row>` + `config:`
278
+ — patch entries replace the whole row config, they do not merge.
279
+
280
+ Changes take effect in the **next** process: Zed spawns a fresh
281
+ `dsh --profile acp-enhanced` for every agent thread, so open a new agent thread (or
282
+ restart Zed) after editing the profile.
283
+
284
+ ## Compatibility
285
+
286
+ One bridge binary runs against every harness generation from **0.1.0-rc.6** through
287
+ **0.1.2-rc.1**. The 0.1.2 line rewrote three APIs this bridge consumes, and the
288
+ bridge absorbs every generation at runtime — no fork, no version flag:
289
+
290
+ | API | ≤ 0.1.1-rc.2 (legacy) | ≥ 0.1.2-alpha.2 (projection) | Bridge behavior |
291
+ |---|---|---|---|
292
+ | running preset of a session | `resolveSessionPreset({header, events})` export | export removed; `agentPreset` session projection | folds the log itself (last `agent-preset/selected` wins, header fallback) — identical semantics in both |
293
+ | preset resolution failure | `UnknownPresetError` / `PresetMountError` | `RemoteError`, codes `agent-preset/*` | `isPresetClientError`: RemoteError duck-typed by `isDSHRemoteError` + `code`, legacy classes identified structurally by `presetId` (never cross-copy `instanceof`) |
294
+ | `permissionPresets.current(x)` | `current(events)` | `current(session)` (via `permissionState`) | `currentPermissionMode` probes the service instance per call |
295
+ | session event log reads | synchronous `session.events` array | `session.events` removed (0.1.2-rc.1); `snapshotEvents()` / `ownEvents()` / `eventAt()` | `sessionEventsOf` reads `snapshotEvents()` when present, the live array otherwise |
296
+ | registry `execute` signature | `execute(agent, line, signal)` | `execute(agent, line, images, signal)` (images between line and signal, 0.1.1-rc.1+) | `executeRegistryCommand` probes the declared arity (the `Remote` decorator never wraps the method) |
297
+ | `userQuestions` registration | `registerProvider({ask})` | `user-questions/request` Cordis waterfall (0.1.2-alpha.2+) | probes the service instance; waterfall listener answers bridge-owned requests and delegates via `next()` |
298
+
299
+ Two invariants make this safe (same conclusions the openma `deepseek-harness-acp` adapter
300
+ reached independently): **value-import pure helpers only** (`createUserMessage`,
301
+ `ReasoningEffortId`, `SessionId`, `defineTool`, … — a foreign copy is functionally
302
+ equivalent), and **service-generation questions are answered by probing the service
303
+ instance**, because the booting CLI — not this package's dependency range — decides the
304
+ service generation. `dsh-agent-presets` is imported as a *namespace*: 0.1.2-alpha.1
305
+ removed its named exports, and a named import would fail at ESM link time.
306
+
307
+ Check both generations from a clean tree:
308
+
309
+ ```sh
310
+ node scripts/compat-check.mjs # installs 0.1.0-rc.6 + 0.1.2-alpha.2+ sets, imports the bridge from each
228
311
  ```
229
312
 
230
- > ⚠️ `searchProvider` must be **exactly** `openai-responses` — the search provider id
231
- > `dsh-web-search-openrouter` registers on `ctx.web`. It is **not** your gateway's LLM
232
- > provider id (the one you put in `DSH_ACP_PROVIDER` above). The `web` plugin matches it
233
- > exactly, so a wrong value produces no error at config time and only fails at the first
234
- > search with `WEB_PROVIDER_CONFIGURED_MISSING`.
313
+ ### Dev checkout: repo-pinned CLI, isolated home
314
+
315
+ When the launcher runs **from a checkout** (`link:` install), it resolves the dsh CLI in
316
+ this order:
317
+
318
+ 1. `$DSH_PATH` — an explicit dsh binary, or a directory whose `node_modules/.bin/dsh` holds one
319
+ 2. the repo-pinned CLI — `<repo>/node_modules/.bin/dsh` (this package's `@deepseek-ai/dsh`
320
+ devDependency, currently 0.1.2-rc.1)
321
+ 3. global fallback — `dsh` on PATH / npx cache / npm prefix (the legacy behavior; a fresh
322
+ clone without `pnpm install` degrades to it)
323
+
324
+ Whenever (1) or (2) wins, the profile boots under an **isolated home**
325
+ (`DSH_ACP_HOME`, default `~/.dsh-acp`): dsh heals its whole dependency closure into
326
+ `$DSH_HOME/profiles/node_modules` on every boot — a dir shared by every profile under that
327
+ home whose content flips to whichever CLI booted last — so a second CLI generation must not
328
+ share a home with e.g. a running `dsh web`. The default home is never touched by this path.
329
+ Harness-injected `DSH_HOME=$HOME/.dsh` in the child environment (dsh exports it into every
330
+ agent/tool process) is detected and overridden, not honored — only a `DSH_HOME` pointing
331
+ away from the default home is respected; to force the pinned CLI onto the default home,
332
+ set `DSH_ACP_HOME=$HOME/.dsh` deliberately.
333
+
334
+ Bootstrap the isolated home once (profile without `dsh-mnemon` — it does not support the
335
+ 0.1.2-alpha harness — plus your old profile's user rows ported verbatim and credentials/settings
336
+ migration, the `subagent-model-selection-settings` host service the 0.1.2-alpha `standard`
337
+ preset requires, and the DeepSeek plugin-package inventory reporter disabled):
338
+
339
+ ```sh
340
+ scripts/init-acp-home.sh # idempotent; re-runs never clobber your files
341
+ ```
342
+
343
+ Both harness generations persist sessions under `$DSH_HOME/sessions/<slug>/<id>/session.jsonl.zstd`, and the new generation reads old-generation logs (verified: history replay and the preset fold work cross-generation). Old threads therefore only need their session history copied to the new home — `scripts/init-acp-home.sh` prints the one-liner (or pass `--copy-sessions`); it copies nothing by default, because the default home's tree also holds every web-profile session.
235
344
 
236
345
  ## Troubleshooting
237
346
 
@@ -239,6 +348,9 @@ dsh plugin --profile acp-enhanced add dsh-web-search-openrouter
239
348
  |---|---|
240
349
  | `exec: dsh: not found` (status 127) | Use the shipped `dsh-acp-zed.sh` launcher (locates node/dsh itself) |
241
350
  | `no API key for provider route "xxx"` | Write `~/.dsh/.credentials.yaml`, or set `env.DEEPSEEK_API_KEY` on the agent_servers entry |
351
+ | `SyntaxError: ... 'PresetMountError'` | You are running a pre-0.7.0 bridge copy against a 0.1.2-alpha host — update this package |
352
+ | `modelSelectionSettings requires ... in the Host scope` | A 0.1.2-alpha host without the `subagent-model-selection-settings` row — run `scripts/init-acp-home.sh` (or add the insert row to your user layer, see the script) |
353
+ | Old threads start empty after a host upgrade | The sessions live under `$DSH_HOME/sessions/<slug>/`; copy the old home's history to the isolated home (`scripts/init-acp-home.sh --copy-sessions`) and the new host resumes them |
242
354
  | Cannot switch models | The saved `reasoning_effort` default (or the session's current effort) is carried onto the new model. Since 0.3.6 the bridge remembers the last effort per model (per-profile JSON): an unsupported carried effort is replaced by that model's remembered effort, else its own default, else its first offered effort — never an "unknown" dropdown, never a failed switch. Also check: a "phantom provider" route was picked — this bridge filters them by default (only `config.provider`'s models are advertised), so point the profile's provider at a real route |
243
355
  | Context usage missing | A "phantom provider" route was picked; this bridge filters them by default (only `config.provider`'s models are advertised) — point the profile's provider at a real route |
244
356
  | Need detailed diagnostics | `ACP_DEBUG=1 dsh --profile acp-enhanced` (stderr lifecycle trace) |
@@ -246,16 +358,33 @@ dsh plugin --profile acp-enhanced add dsh-web-search-openrouter
246
358
  ## Development
247
359
 
248
360
  ```sh
361
+ node scripts/compat-check.mjs # cross-generation link check (0.1.0-rc.6 + 0.1.2-alpha.2+ scratch installs)
249
362
  node scripts/acp-client.mjs # end-to-end smoke (needs an API key)
250
363
  node scripts/acp-client-tools.mjs # client-tool tests (mocks Zed fs/terminal/elicitation/plan)
251
364
  node scripts/acp-mcp-test.mjs # MCP mount test (no model calls)
252
365
  node scripts/acp-smoke-keyless.mjs # keyless boot smoke (CI)
253
366
  node scripts/acp-resume-test.mjs # session resume test
254
367
  node scripts/codec-image-test.mjs # image-codec unit tests (no network, fake store)
255
- node scripts/terminal-codec-test.mjs # terminal-card codec unit tests (no network)
368
+ node scripts/terminal-codec-test.mjs # terminal-card codec unit tests (no network)
256
369
  node scripts/acp-image-e2e.mjs # image capability e2e (vision-model leg needs an API key)
370
+ scripts/init-acp-home.sh # bootstrap/refresh the isolated home (~/.dsh-acp)
257
371
  ```
258
372
 
373
+ DevDependency pins for the harness packages use the same ranges the pinned
374
+ `@deepseek-ai/dsh` CLI declares (e.g. `^0.1.2-rc.1`), so the repo's tree and a fresh
375
+ CLI install resolve one coherent family — exact patch pins here mixed with the CLI's
376
+ range-resolved closure produce a split closure (two versions of one name) that breaks
377
+ profile boots with export-not-found errors. After changing those pins, regenerate the
378
+ whole lockfile (`rm -rf node_modules pnpm-lock.yaml && pnpm install`): an incremental
379
+ install both leaves stale store entries poisoning the profile heal AND retains stale
380
+ lockfile peer resolutions — bumping 0.1.2-alpha.2 → 0.1.2-rc.1 in place left
381
+ `dsh-session-persistence@0.1.2-alpha.3` (old-generation peers) wired into the rc.1
382
+ packages' snapshots, which passes boot and session/new and only breaks the first turn
383
+ with `TypeError: Cannot read properties of undefined (reading 'length')` from
384
+ PersistenceCoordinator.
385
+ `pnpm-workspace.yaml` approves the CLI closure's build scripts (node-pty prebuilds, koffi)
386
+ — they are runtime requirements when the repo CLI boots the profile.
387
+
259
388
  ## Known limitations
260
389
 
261
390
  Audio attachments are not supported (audio capability is not advertised), text streams at
package/lib/codec.js CHANGED
@@ -57,6 +57,44 @@ export function promptHasUnsupportedContent(prompt) {
57
57
  return prompt.some((block) => block.type !== 'text' && block.type !== 'resource_link')
58
58
  }
59
59
 
60
+ /**
61
+ * Flatten an ACP prompt's text blocks to the line a composer submits: text
62
+ * blocks concatenate verbatim in wire order, while images and resource links
63
+ * are composer attachments, not input-box text. This is the line slash
64
+ * commands are parsed from — an image pasted next to a slash line must neither
65
+ * prefix it nor pollute its arguments (the Web composer's `matchEnter`
66
+ * contract: line text plus a separate images payload).
67
+ * @param prompt - ACP `session/prompt` content, in wire order.
68
+ * @returns the concatenated text-block content.
69
+ */
70
+ export function acpPromptLineText(prompt) {
71
+ return (prompt ?? []).flatMap((block) => block?.type === 'text' && typeof block.text === 'string' ? [block.text] : []).join('')
72
+ }
73
+
74
+ /**
75
+ * Build the composer's image payload for a slash-command submission from an
76
+ * ACP prompt: one `{ data, mediaType, name? }` upload object per image block,
77
+ * in wire order — the registry `execute` images slot's exact shape (the
78
+ * composer serializes `{ type: 'image', mediaType, data, name }`, and the
79
+ * command service's `admitEncodedImages` reads `data`/`mediaType`/`name`).
80
+ * The service admits the payload into the attachment store itself; the store
81
+ * is content-addressed, so an image `convertPrompt` already saved resolves to
82
+ * the same reference and nothing is duplicated. An image whose media type is
83
+ * not a raster cannot reach a command (`convertPrompt` rejects it before
84
+ * dispatch), so a non-raster here is simply skipped.
85
+ * @param prompt - ACP `session/prompt` content, in wire order.
86
+ * @returns upload objects ready for `commands.execute`'s images slot.
87
+ */
88
+ export function acpPromptCommandImages(prompt) {
89
+ return (prompt ?? []).flatMap((block) => {
90
+ if (block?.type !== 'image' || typeof block.data !== 'string' || block.data.length === 0) return []
91
+ const mediaType = canonicalImageMediaType(block.mimeType)
92
+ if (mediaType === undefined) return []
93
+ const name = imageName(block.uri)
94
+ return [{ data: block.data, mediaType, ...name === undefined ? {} : { name } }]
95
+ })
96
+ }
97
+
60
98
  /** Raster media types the harness attachment seam admits (dsh-attachment). */
61
99
  const IMAGE_MEDIA_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp', 'image/gif'])
62
100