Procházet zdrojové kódy

Merge pull request #3510 from deepseek-harness/feat/remove-str-replace-editor-from-minimal

feat(minimal): make editor opt-in and align bash output
fz před 3 týdny
rodič
revize
0ba6c25807
89 změnil soubory, kde provedl 417 přidání a 1249 odebrání
  1. 2 2
      .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml
  2. 0 0
      .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md
  3. 0 0
      .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md
  4. 2 2
      .agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.i18n.yaml
  5. 2 0
      .agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md
  6. 2 0
      .agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.zh.md
  7. 2 2
      .agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.i18n.yaml
  8. 1 1
      .agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.md
  9. 1 1
      .agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.zh.md
  10. 2 2
      .agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.i18n.yaml
  11. 7 7
      .agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md
  12. 7 7
      .agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md
  13. 2 2
      .agents/notes/implemented/process/2026-08-10-npm-release-sequences.i18n.yaml
  14. 2 0
      .agents/notes/implemented/process/2026-08-10-npm-release-sequences.md
  15. 2 0
      .agents/notes/implemented/process/2026-08-10-npm-release-sequences.zh.md
  16. 6 0
      .agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.i18n.yaml
  17. 35 0
      .agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md
  18. 35 0
      .agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md
  19. 2 2
      .agents/notes/implemented/simplification/2026-09-05-base-default-file-editor.i18n.yaml
  20. 3 3
      .agents/notes/implemented/simplification/2026-09-05-base-default-file-editor.md
  21. 3 3
      .agents/notes/implemented/simplification/2026-09-05-base-default-file-editor.zh.md
  22. 2 2
      .agents/notes/implemented/testing/2026-09-04-session-open-performance-gate.i18n.yaml
  23. 2 0
      .agents/notes/implemented/testing/2026-09-04-session-open-performance-gate.md
  24. 2 0
      .agents/notes/implemented/testing/2026-09-04-session-open-performance-gate.zh.md
  25. 2 2
      .agents/notes/implemented/testing/2026-09-06-backend-continuation-performance.i18n.yaml
  26. 2 0
      .agents/notes/implemented/testing/2026-09-06-backend-continuation-performance.md
  27. 2 0
      .agents/notes/implemented/testing/2026-09-06-backend-continuation-performance.zh.md
  28. 2 2
      apps/cli/reference/README.i18n.yaml
  29. 2 2
      apps/cli/reference/README.md
  30. 2 2
      apps/cli/reference/README.zh.md
  31. 0 2
      apps/cli/tests/built-bin.e2e.ts
  32. 55 7
      apps/cli/tests/profiles/sdk/keyless-smoke.e2e.ts
  33. 15 16
      apps/cli/tests/web-agent-presets.e2e.ts
  34. 1 1
      apps/web/tests/agent-preset-authoring.e2e.ts
  35. 2 2
      apps/web/tests/expected/agent-preset-authoring/created.expected.md
  36. 1 1
      apps/web/tests/expected/agent-preset-authoring/damaged.expected.md
  37. 1 1
      apps/web/tests/expected/agent-preset-authoring/section.expected.md
  38. 1 1
      apps/web/tests/expected/agent-preset-selection/menu.expected.md
  39. 6 22
      apps/web/tests/minimal-preset.snapshot.ts
  40. 2 2
      benchmarks/agent-continuation/README.i18n.yaml
  41. 1 1
      benchmarks/agent-continuation/README.md
  42. 1 1
      benchmarks/agent-continuation/README.zh.md
  43. 5 2
      benchmarks/agent-continuation/profile-continuation.worker.ts
  44. 2 2
      docs/config-catalog.i18n.yaml
  45. 1 1
      docs/config-catalog.md
  46. 1 1
      docs/config-catalog.zh.md
  47. 2 2
      docs/user/guide/python-sdk.i18n.yaml
  48. 19 3
      docs/user/guide/python-sdk.md
  49. 19 3
      docs/user/guide/python-sdk.zh.md
  50. 2 2
      packages/bundle/base/README.i18n.yaml
  51. 1 1
      packages/bundle/base/README.md
  52. 1 1
      packages/bundle/base/README.zh.md
  53. 2 2
      packages/bundle/sdk-minimal/README.i18n.yaml
  54. 5 5
      packages/bundle/sdk-minimal/README.md
  55. 5 5
      packages/bundle/sdk-minimal/README.zh.md
  56. 1 14
      packages/bundle/sdk-minimal/cordis.patch.yml
  57. 1 3
      packages/bundle/sdk-minimal/package.json
  58. 0 2
      packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts
  59. 2 2
      packages/client/ui-agent-preset/src/client/locales.ts
  60. 4 7
      packages/client/ui-user-questions/src/index.ts
  61. 1 5
      packages/client/ui-user-questions/tests/node-plugin.client.spec.ts
  62. 6 2
      packages/lsp/lsp-stdio/tests/lifecycle.spec.ts
  63. 5 24
      packages/preset/agent-presets/presets/minimal/agent.cordis.yml
  64. 1 1
      packages/preset/agent-presets/presets/minimal/preset.yml
  65. 35 6
      packages/session/session-persistence-jsonl/tests/built-migration-worker.e2e.ts
  66. 4 1
      packages/session/session-persistence-jsonl/tsdown.config.ts
  67. 2 2
      packages/shell/tool-bash-persistent/README.i18n.yaml
  68. 2 2
      packages/shell/tool-bash-persistent/README.md
  69. 2 2
      packages/shell/tool-bash-persistent/README.zh.md
  70. 7 4
      packages/shell/tool-bash-persistent/src/index.ts
  71. 10 4
      packages/shell/tool-bash-persistent/tests/loader-composition.spec.ts
  72. 12 11
      packages/shell/tool-bash-persistent/tests/tools.spec.ts
  73. 0 6
      pnpm-lock.yaml
  74. 2 2
      python/sdk/README.i18n.yaml
  75. 1 1
      python/sdk/README.md
  76. 1 1
      python/sdk/README.zh.md
  77. 2 2
      python/sdk/examples/README.i18n.yaml
  78. 2 3
      python/sdk/examples/README.md
  79. 2 3
      python/sdk/examples/README.zh.md
  80. 1 1
      scripts/release/verify-packed-install.ts
  81. 6 46
      scripts/smoke-python-runtime.py
  82. 0 4
      scripts/snapshots/python-sdk-single-exe/minimal-in-history/prompt-history.json
  83. 6 433
      scripts/snapshots/python-sdk-single-exe/minimal/model-visible.json
  84. 3 430
      scripts/snapshots/python-sdk-single-exe/minimal/win-x64/model-visible.json
  85. 3 3
      snapshots/sdk/persistent-tools/notifications.expected.jsonl
  86. 3 3
      snapshots/sdk/persistent-tools/session.v3.jsonl
  87. 1 1
      snapshots/web/minimal-preset/session.v3.jsonl
  88. 1 86
      snapshots/web/minimal-preset/tool-schemas.expected.json
  89. 1 1
      snapshots/web/minimal-preset/ui.expected.md

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml

@@ -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-10-single-file-executable-sdk-runtime-distribution.md
-2026-07-10-single-file-executable-sdk-runtime-distribution.md: 5533c38d635d04c8799658fc1f6bcec8543d1dab
-2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: 05800082034f0d1bd5034fc5bb17f09356d84dd9
+2026-07-10-single-file-executable-sdk-runtime-distribution.md: 7792e24a5869be6b7bae7787a6a481f3075ba740
+2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: 857bfaec80da962fac0403f2239e0a5e71954194

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md


+ 2 - 2
.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.i18n.yaml

@@ -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-31-released-session-format-migrations.md
-2026-08-31-released-session-format-migrations.md: eb5eb14f28a6459bd388caa2ea106ec01d1ebbe6
-2026-08-31-released-session-format-migrations.zh.md: b06bfac059541e9c397ff46fe7d169690c5b1213
+2026-08-31-released-session-format-migrations.md: 286737730198ad895de2f03a48f9c74848839582
+2026-08-31-released-session-format-migrations.zh.md: 228b09e03916c1fa447398edb45508a7a6ab61e5

+ 2 - 0
.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md

@@ -94,6 +94,8 @@ The JSONL provider scans frame boundaries once, reuses one Zstandard decoder, pa
 
 Current encoding is record based. The provider serializes about 1 MiB of plaintext per main-thread slice, streams it through one Zstandard context with source-error propagation, writes compressed output in 4 MiB batches to an exclusively created same-directory temporary file, and syncs it before publication. A process-wide scheduler admits at most two full verification Workers and hands a released permit directly to the oldest waiter.
 
+The shipped `lib/worker.cjs` bundles its JavaScript workspace dependencies so each fresh verifier avoids resolving and compiling their runtime module graph. This is safe because the Worker communicates through plain request/result messages and shares no service or class identity with its host. The Host build applies the existing TypeScript and Typert transforms; the Client pass skips this Node-only package instead of replacing its worker with untransformed source. Native add-ons remain external. Verification, scheduler admission, termination, and durable publication still complete before writable open returns. The built-worker smoke copies the package manifest and worker into an isolated temporary package with ambient module paths removed, accepts a valid generation, and rejects an incorrect event count.
+
 Preparation forwards cancellation through source reads and observes it at the existing approximately 500 ms Decode yield boundary. Once `publish()` starts, encode, Worker verification, and publication do not receive caller cancellation and run to settlement; write open checks its caller signal again afterward. A published generation is never rolled back.
 
 The Stage pipeline ends at one prepared current artifact. [Historical Session read preparation](2026-09-05-read-only-session-migration-preparation.md) defines how read open consumes that artifact immediately while write open performs encode, verification, and publication before returning append access.

+ 2 - 0
.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.zh.md

@@ -94,6 +94,8 @@ JSONL provider 只扫描一次 frame boundary,复用一个 Zstandard decoder
 
 Current encode 以单条 record 为单位。Provider 在主线程每个 slice 序列化约 1 MiB plaintext,通过一个会传播 source error 的 Zstandard context 流式压缩,以 4 MiB batch 写入同目录排他创建的临时文件,并在 publication 前 sync。进程级 scheduler 最多允许两个完整 verification Worker 并行,并把释放的 permit 直接交给最早的 waiter。
 
+发布的 `lib/worker.cjs` 将 JavaScript workspace 依赖一起打包,使每个新 verifier 无需解析并编译它们的运行时模块图。Worker 只通过普通 request/result 消息通信,与 host 不共享 service 或 class identity,因此可以这样处理。Host build 应用现有 TypeScript 与 Typert 转换;Client pass 跳过这个 Node-only package,不会用未经转换的源代码覆盖 worker。Native add-on 保持 external。Verification、scheduler admission、termination 与 durable publication 仍在 writable open 返回前完成。Built-worker 冒烟测试把 package manifest 与 worker 复制到隔离的临时 package,移除环境中的模块搜索路径,接受有效 generation,并拒绝错误的 event count。
+
 Preparation 会把 cancellation 传给 source read,并在现有的约 500 ms Decode yield 边界观察它。`publish()` 一旦开始,encode、Worker verification 与 publication 不接收 caller cancellation,并运行到终态;write open 会在之后再次检查 caller signal。已经发布的 generation 绝不会回滚。
 
 Stage pipeline 终止于一份 prepared current artifact。[历史 Session 只读迁移准备](2026-09-05-read-only-session-migration-preparation.zh.md)定义 read open 如何立即消费该 artifact,以及 write open 如何在返回 append 权限前完成 encode、verification 与 publication。

+ 2 - 2
.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.i18n.yaml

@@ -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-31-even-out-shipped-tool-rosters.md
-2026-07-31-even-out-shipped-tool-rosters.md: df334bc0bd9d011ea2e2f4ee938fb29c4c4bfe2c
-2026-07-31-even-out-shipped-tool-rosters.zh.md: ddee380f836bf50a733541b2d3ea6dd8a74126a2
+2026-07-31-even-out-shipped-tool-rosters.md: caadb4601a56e9180ccb8d1eae571208941cbcba
+2026-07-31-even-out-shipped-tool-rosters.zh.md: 9503baf9f1f8f2567c3ccd81db685c74db189430

+ 1 - 1
.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.md

@@ -12,7 +12,7 @@ The result was a user-visible difference nobody had decided: the same model, ask
 
 ## Decision
 
-The rows that are not surface-specific move into [`base.cordis.yml`](../../../../packages/bundle/base/cordis.patch.yml), and three more join them: `tool-session-query`, `tool-str-replace-editor`, and `repeat-tool-reminder`. Web search moves there too; its [deployment decision](2026-07-31-web-default-search.md) owns the security boundary while the shared base owns its surface-neutral mount. Both surfaces assemble the same roster, including fixed `glob` and `grep` members because `dsh-tool-fs-search` spawns the [packaged ripgrep binary](../../archived/architecture/2026-08-01-packaged-ripgrep-search.md). Two later decisions narrow that roster: the [session-search decision](../../archived/feature/2026-08-02-session-search-not-shipped-default.md) keeps `tool-session-query` opt-in, and the [single-editor decision](../../archived/simplification/2026-08-10-default-presets-single-editor.md) keeps `tool-str-replace-editor` out of the general-purpose presets while retaining it in `minimal`.
+The rows that are not surface-specific move into [`base.cordis.yml`](../../../../packages/bundle/base/cordis.patch.yml), and three more join them: `tool-session-query`, `tool-str-replace-editor`, and `repeat-tool-reminder`. Web search moves there too; its [deployment decision](2026-07-31-web-default-search.md) owns the security boundary while the shared base owns its surface-neutral mount. Both surfaces assemble the same roster, including fixed `glob` and `grep` members because `dsh-tool-fs-search` spawns the [packaged ripgrep binary](../../archived/architecture/2026-08-01-packaged-ripgrep-search.md). Later decisions narrow that roster: the [session-search decision](../../archived/feature/2026-08-02-session-search-not-shipped-default.md) keeps `tool-session-query` opt-in, the [single-editor decision](../../archived/simplification/2026-08-10-default-presets-single-editor.md) removes `tool-str-replace-editor` from general-purpose presets, and the [persistent-shell-only decision](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.md) removes it from the minimal compositions.
 
 Two rows stay surface-specific. `tmux-context` is TUI-only because a browser surface has no terminal multiplexer to describe. `session-reference` is TUI-only because it drives the shared session-query index from the launcher's process-local path, and the browser sidebar reconciles that index on its own first search.
 

+ 1 - 1
.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.zh.md

@@ -12,7 +12,7 @@ Status: implemented
 
 ## 决策
 
-那些并非 surface 专属的行移入 [`base.cordis.yml`](../../../../packages/bundle/base/cordis.patch.yml),另有三行加入:`tool-session-query`、`tool-str-replace-editor` 和 `repeat-tool-reminder`。Web 搜索也一并移入;其[部署决策](2026-07-31-web-default-search.zh.md)负责安全边界,共享 base 则负责与 surface 无关的挂载。两个 surface 组装同一份清单,其中 `glob` 和 `grep` 是固定成员,因为 `dsh-tool-fs-search` 直接 spawn [打包的 ripgrep 二进制](../../archived/architecture/2026-08-01-packaged-ripgrep-search.md)。之后有两项决策收窄这份清单:[session-search 决策](../../archived/feature/2026-08-02-session-search-not-shipped-default.md)让 `tool-session-query` 保持需显式启用,[单一编辑器决策](../../archived/simplification/2026-08-10-default-presets-single-editor.md)让通用 preset 不提供 `tool-str-replace-editor`,但在 `minimal` 中保留它。
+那些并非 surface 专属的行移入 [`base.cordis.yml`](../../../../packages/bundle/base/cordis.patch.yml),另有三行加入:`tool-session-query`、`tool-str-replace-editor` 和 `repeat-tool-reminder`。Web 搜索也一并移入;其[部署决策](2026-07-31-web-default-search.zh.md)负责安全边界,共享 base 则负责与 surface 无关的挂载。两个 surface 组装同一份清单,其中 `glob` 和 `grep` 是固定成员,因为 `dsh-tool-fs-search` 直接 spawn [打包的 ripgrep 二进制](../../archived/architecture/2026-08-01-packaged-ripgrep-search.md)。后续决策收窄了这份清单:[session-search 决策](../../archived/feature/2026-08-02-session-search-not-shipped-default.md)让 `tool-session-query` 保持需显式启用,[单一 editor 决策](../../archived/simplification/2026-08-10-default-presets-single-editor.md)从通用 preset 移除 `tool-str-replace-editor`,[仅持久 shell 决策](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md)则从极简组合移除它。
 
 有两行仍是 surface 专属。`tmux-context` 只在 TUI,因为浏览器 surface 没有终端复用器可描述。`session-reference` 只在 TUI,因为它以 launcher 的进程本地路径驱动共享的 session-query 索引,而浏览器侧边栏会在自己的首次搜索里重建该索引。
 

+ 2 - 2
.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.i18n.yaml

@@ -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-11-minimal-profiles-bare-two-tool-runtime.md
-2026-08-11-minimal-profiles-bare-two-tool-runtime.md: 9e4d476c36ab6f920481b1281058d888710a2ef5
-2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md: 761c384f1656ce66d027eb2b13c9dbc4f20f3686
+2026-08-11-minimal-profiles-bare-two-tool-runtime.md: 8cfa4aa71e33317947d0661641df1e6e814f5e90
+2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md: f1f916bdde92fd104c3d879ff058b2e0474eef38

+ 7 - 7
.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md

@@ -1,4 +1,4 @@
-# Agent Note: Minimal profiles use the bare two-tool runtime
+# Agent Note: Minimal profiles use a bare runtime
 
 Status: implemented
 
@@ -12,23 +12,23 @@ The two launch paths also have different configuration owners. Web mounts a per-
 
 ## Decision
 
-The shipped Web minimal preset exposes persistent `bash` and `str_replace_editor`; the standalone profile exposes persistent `bash` on Linux/macOS or `pwsh` on Windows, plus the same editor. Both mount no context-compaction provider, suppress every `dsh-system-prompt` runtime-context contribution for fresh sessions, and run the editor against `@deepseek-ai/dsh-fs-local`. The Web preset isolates `ctx.fs` inside the agent entry and mounts `fs-local` beside the editor, so other Web agents retain the host filesystem provider. Its persona remains the fixed complete prompt owned by the earlier [minimal-preset composition decision](../../archived/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md) and applies runtime-context suppression only to that agent scope. The standalone spine forwards the same setting to its process-owned system-prompt service. The Web host retains its sandbox and approval services; the standalone profile mounts a danger-full-access sandbox policy and no approval service. Neither contributes model-facing policy context.
+The shipped Web minimal preset exposes persistent `bash`; the standalone profile exposes persistent `bash` on Linux/macOS or `pwsh` on Windows. Both mount no context-compaction or filesystem provider and suppress every `dsh-system-prompt` runtime-context contribution for fresh sessions. The [persistent-shell-only decision](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.md) removes the editor and its otherwise-unused `fs-local` provider from both compositions. The Web preset's persona remains the fixed complete prompt owned by the earlier [minimal-preset composition decision](../../archived/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md) and applies runtime-context suppression only to that agent scope. The standalone spine forwards the same setting to its process-owned system-prompt service. The Web host retains its sandbox and approval services; the standalone profile mounts a danger-full-access sandbox policy and no approval service. Neither contributes model-facing policy context.
 
-The standalone [`@deepseek-ai/dsh-sdk-minimal` bundle](../../../../packages/bundle/sdk-minimal/README.md) remains a complete JSON-RPC process composition behind `dsh --profile sdk-minimal`. It mounts SDK startup and JSON-RPC serving, the local PTY and subprocess services required by the platform-selected persistent shell, `fs-local`, that shell's tool consumer, the editor, and uncompressed JSONL persistence under `$DSH_HOME/sessions`. It does not mount `token-meter`, `compaction-basic`, `fs-sandbox`, or `fs-observation-policy`. The persistent shell consumes the profile's danger-full-access sandbox policy; the editor is not confined by that policy. [docs/architecture.md](../../../../docs/architecture.md) owns this bundle placement and its separation from `dsh-base`.
+The standalone [`@deepseek-ai/dsh-sdk-minimal` bundle](../../../../packages/bundle/sdk-minimal/README.md) remains a complete JSON-RPC process composition behind `dsh --profile sdk-minimal`. It mounts SDK startup and JSON-RPC serving, the local PTY and subprocess services required by the platform-selected persistent shell, that shell's tool consumer, and uncompressed JSONL persistence under `$DSH_HOME/sessions`. It does not mount `token-meter`, `compaction-basic`, `fs-local`, `fs-sandbox`, `fs-observation-policy`, or a filesystem tool. The persistent shell consumes the profile's danger-full-access sandbox policy. [docs/architecture.md](../../../../docs/architecture.md) owns this bundle placement and its separation from `dsh-base`.
 
 `DSH_SYSTEM_PROMPT` selects the standalone persona, and `DSH_CONTEXT_WINDOW` supplies fallback capacity for a model without exact catalog metadata. The SDK client's JSON-RPC `initialize` request is the sole runtime model selection. [`minimal.py`](../../../../python/sdk/examples/minimal.py) may read `DSH_MODEL` only as the command's default `model` argument; an explicit `--model` needs no matching child environment value. Endpoint and credential variables stay owned by the DeepSeek adapter's existing environment-resolution path.
 
 ## Verification
 
-The Web replay boots the complete Web host, creates the agent through the preset service, and asserts that the scoped filesystem is bare, no scoped compaction service exists, no system-prompt-owned runtime-context message was appended, and the assembled request contains exactly the fixed prompt and two tools. It then executes persistent Bash and the editor against the real scoped services.
+The Web replay boots the complete Web host, creates the agent through the preset service, and asserts that no scoped filesystem or compaction service exists, no system-prompt-owned runtime-context message was appended, and the assembled request contains exactly the fixed prompt and persistent Bash. It then executes persistent Bash against the real scoped services.
 
-The SDK keyless source test boots real `dsh --profile sdk-minimal`, completes a turn with an environment-selected prompt, and asserts the generated one-bundle manifest. The Python SDK bundled-runtime snapshot owns the assembled prompt, exact two-tool catalog, and absence of every system-prompt-owned runtime-context message. Packaged-runtime coverage initializes the standalone profile through each available carrier with environment-selected model, model capacity, and prompt values, then executes the selected persistent shell and editor. Cordis validation checks that both configurations resolve their declared plugins and configuration fields.
+The SDK keyless source test boots real `dsh --profile sdk-minimal`, completes a turn with an environment-selected prompt, and asserts the generated one-bundle manifest. The Python SDK bundled-runtime snapshot owns the assembled prompt, exact single-tool catalog, and absence of every system-prompt-owned runtime-context message. Packaged-runtime coverage initializes the standalone profile through each available carrier with environment-selected model, model capacity, and prompt values, then executes the selected persistent shell. Cordis validation checks that both configurations resolve their declared plugins and configuration fields.
 
 ## Alternatives considered
 
 **Keep `compaction-basic` mounted with a high threshold.** Rejected because even an inert-for-short-tests provider permits history replacement in longer sessions and leaves the minimal composition dependent on model-capacity metadata and the token meter.
 
-**Keep `fs-sandbox` in danger-full-access mode.** Rejected because the sandboxed provider still makes confinement and escalation part of the editor capability. The target runtime requires the bare local provider, whose lack of `sandboxMode` is composition truth.
+**Keep `fs-local` after removing the editor.** Rejected because neither minimal composition has another filesystem consumer. Retaining the provider would increase the runtime roster without adding a model-visible capability.
 
 **Use one Cordis leaf for Web and Python SDK startup.** Rejected because a Web preset contributes agent-scoped services to an existing multi-session host, while the Python SDK must launch a complete process containing the JSON-RPC server and its process-wide dependencies.
 
@@ -36,4 +36,4 @@ The SDK keyless source test boots real `dsh --profile sdk-minimal`, completes a
 
 ## Consequences
 
-Minimal sessions never summarize or replace earlier history and never add a runtime-context snapshot; callers must keep turns within the selected model's context capacity and must not rely on model-visible narration of standing sandbox or approval policy. The editor can address any absolute path visible to the runtime process, independently of the persistent shell's sandbox policy. The two launch paths share their model-facing tool, no-context, and no-compaction guarantees while retaining different prompt and model configuration appropriate to their owners. The Python SDK path communicates only through the bundled `dsh` stdio JSON-RPC profile.
+Minimal sessions never summarize or replace earlier history and never add a runtime-context snapshot; callers must keep turns within the selected model's context capacity and must not rely on model-visible narration of standing sandbox or approval policy. The two launch paths share their single-tool, no-filesystem, no-context, and no-compaction guarantees while retaining different prompt and model configuration appropriate to their owners. The Python SDK path communicates only through the bundled `dsh` stdio JSON-RPC profile.

+ 7 - 7
.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md

@@ -1,4 +1,4 @@
-# Agent Note: minimal profile 使用裸双工具运行时
+# Agent Note: minimal profile 使用裸运行时
 
 Status: implemented
 
@@ -12,23 +12,23 @@ Web `minimal` preset 与独立 JSON-RPC minimal 组合对外提供持久 `bash`
 
 ## 决策
 
-随附 Web minimal preset 对外提供持久 `bash` 与 `str_replace_editor`;独立 profile 在 Linux/macOS 上提供持久 `bash`,在 Windows 上提供 `pwsh`,并提供相同 editor。两者都不挂载上下文压缩提供方,为新建会话抑制每个 `dsh-system-prompt` runtime-context 贡献,并让编辑器使用 `@deepseek-ai/dsh-fs-local`。Web preset 在 agent entry 内隔离 `ctx.fs`,将 `fs-local` 与编辑器一起挂载,因此其他 Web agent 仍使用宿主文件系统提供方。其 persona 继续采用较早的 [minimal preset 组合决策](../../archived/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md)所拥有的固定 complete 提示词,并仅为该 agent 作用域实施 runtime-context 抑制。独立 spine 将同一设置转发给其进程拥有的 system-prompt 服务。Web 宿主保留沙箱与批准服务;独立 profile 挂载 danger-full-access 沙箱策略,不挂载批准服务。两者都不贡献面向模型的策略上下文。
+随附 Web minimal preset 对外提供持久 `bash`;独立 profile 在 Linux/macOS 上提供持久 `bash`,在 Windows 上提供 `pwsh`。两者都不挂载上下文压缩或文件系统提供方,并为新建会话抑制每个 `dsh-system-prompt` runtime-context 贡献。[仅持久 shell 决策](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md)从两份组合中移除了编辑器及其原本除此之外无人使用的 `fs-local` 提供方。Web preset 的 persona 继续采用较早的 [minimal preset 组合决策](../../archived/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md)所拥有的固定 complete 提示词,并仅为该 agent 作用域实施 runtime-context 抑制。独立 spine 将同一设置转发给其进程拥有的 system-prompt 服务。Web 宿主保留沙箱与批准服务;独立 profile 挂载 danger-full-access 沙箱策略,不挂载批准服务。两者都不贡献面向模型的策略上下文。
 
-独立的 [`@deepseek-ai/dsh-sdk-minimal` 组合包](../../../../packages/bundle/sdk-minimal/README.zh.md)仍是 `dsh --profile sdk-minimal` 后面的完整 JSON-RPC 进程组合。它挂载 SDK 启动与 JSON-RPC 服务、按平台选择的持久 shell 所需的本地 PTY 和子进程服务、`fs-local`、该 shell 的工具消费方、editor,以及位于 `$DSH_HOME/sessions` 的未压缩 JSONL 持久化。它不挂载 `token-meter`、`compaction-basic`、`fs-sandbox` 或 `fs-observation-policy`。持久 shell 消费该 profile 的 danger-full-access 沙箱策略;编辑器不受该策略限制。[docs/architecture.md](../../../../docs/architecture.zh.md) 负责该组合包的位置及其与 `dsh-base` 的分离。
+独立的 [`@deepseek-ai/dsh-sdk-minimal` 组合包](../../../../packages/bundle/sdk-minimal/README.zh.md)仍是 `dsh --profile sdk-minimal` 后面的完整 JSON-RPC 进程组合。它挂载 SDK 启动与 JSON-RPC 服务、按平台选择的持久 shell 所需的本地 PTY 和子进程服务、该 shell 的工具消费方,以及位于 `$DSH_HOME/sessions` 的未压缩 JSONL 持久化。它不挂载 `token-meter`、`compaction-basic`、`fs-local`、`fs-sandbox`、`fs-observation-policy` 或文件系统工具。持久 shell 消费该 profile 的 danger-full-access 沙箱策略。[docs/architecture.md](../../../../docs/architecture.zh.md) 负责该组合包的位置及其与 `dsh-base` 的分离。
 
 `DSH_SYSTEM_PROMPT` 选择独立组合的 persona,`DSH_CONTEXT_WINDOW` 为没有确切目录元数据的模型提供后备容量。SDK 客户端的 JSON-RPC `initialize` 请求是唯一运行时模型选择。[`minimal.py`](../../../../python/sdk/examples/minimal.py)可以只把 `DSH_MODEL` 读作命令的默认 `model` 参数;显式 `--model` 不需要匹配的子进程环境值。端点与凭据变量继续由 DeepSeek 适配器现有的环境解析路径持有。
 
 ## 验证
 
-Web 回放会启动完整 Web 宿主,通过 preset 服务创建 agent,并断言作用域文件系统为裸后端、不存在作用域压缩服务、没有追加 system-prompt 拥有的 runtime-context 消息,而且组装请求只包含固定提示词与两个工具。随后,它通过真实作用域服务执行持久 Bash 和编辑器。
+Web 回放会启动完整 Web 宿主,通过 preset 服务创建 agent,并断言不存在作用域文件系统或压缩服务、没有追加 system-prompt 拥有的 runtime-context 消息,而且组装请求只包含固定提示词与持久 Bash。随后,它通过真实作用域服务执行持久 Bash。
 
-SDK keyless 源码测试启动真实 `dsh --profile sdk-minimal`,使用环境选择的提示词完成一个回合,并断言生成的单组合包 manifest。Python SDK 打包运行时快照固定组装提示词、精确双工具目录,并固定不存在任何 system-prompt 所拥有的 runtime-context 消息。打包运行时覆盖会通过每种可用载体,使用环境选择的模型、模型容量和提示词值初始化独立 profile,然后执行所选持久 shell 与 editor。Cordis 校验会检查两份配置能否解析声明的插件和配置字段。
+SDK keyless 源码测试启动真实 `dsh --profile sdk-minimal`,使用环境选择的提示词完成一个回合,并断言生成的单组合包 manifest。Python SDK 打包运行时快照固定组装提示词、精确单工具目录,并固定不存在任何 system-prompt 所拥有的 runtime-context 消息。打包运行时覆盖会通过每种可用载体,使用环境选择的模型、模型容量和提示词值初始化独立 profile,然后执行所选持久 shell。Cordis 校验会检查两份配置能否解析声明的插件和配置字段。
 
 ## 考虑过的替代方案
 
 **以较高阈值保留 `compaction-basic`。** 不予采用,因为即便提供方在短测试中未触发,较长会话仍允许替换历史记录,而且 minimal 组合仍会依赖模型容量元数据与 token meter。
 
-**在 danger-full-access 模式下保留 `fs-sandbox`。** 不予采用,因为沙箱提供方仍会使限权与提权成为编辑器能力的一部分。目标运行时要求裸本地提供方,而其不具备 `sandboxMode` 正是组合事实。
+**移除编辑器后保留 `fs-local`。** 不予采用,因为两份 minimal 组合都没有其他文件系统消费方。保留该提供方只会扩大运行时清单,不会新增模型可见能力。
 
 **为 Web 与 Python SDK 启动使用同一个 Cordis leaf。** 不予采用,因为 Web preset 向现有多会话宿主贡献 agent 作用域服务,而 Python SDK 必须启动包含 JSON-RPC 服务器及其进程级依赖的完整进程。
 
@@ -36,4 +36,4 @@ SDK keyless 源码测试启动真实 `dsh --profile sdk-minimal`,使用环境
 
 ## 后果
 
-Minimal 会话不会摘要或替换较早历史,也不会添加 runtime-context 快照;调用方必须让会话轮次保持在所选模型的上下文容量内,且不得依赖模型可见的常驻沙箱或批准策略说明。编辑器可以访问运行时进程可见的任何绝对路径,且不受持久 shell 沙箱策略影响。两条启动路径共享面向模型的工具、无上下文与无压缩保证,同时保留适合各自所有者的不同提示词和模型配置。Python SDK 路径只通过内置 `dsh` stdio JSON-RPC profile 通信。
+Minimal 会话不会摘要或替换较早历史,也不会添加 runtime-context 快照;调用方必须让会话轮次保持在所选模型的上下文容量内,且不得依赖模型可见的常驻沙箱或批准策略说明。两条启动路径共享单工具、无文件系统、无上下文与无压缩保证,同时保留适合各自所有者的不同提示词和模型配置。Python SDK 路径只通过内置 `dsh` stdio JSON-RPC profile 通信。

+ 2 - 2
.agents/notes/implemented/process/2026-08-10-npm-release-sequences.i18n.yaml

@@ -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-10-npm-release-sequences.md
-2026-08-10-npm-release-sequences.md: c403964d8de1c158e5949b5e112a038832709874
-2026-08-10-npm-release-sequences.zh.md: d03da4c4485c4807497dfb28b61ab342bb4c8d12
+2026-08-10-npm-release-sequences.md: c039db2c7463f93f6f8847ae0ae6100df650f2a5
+2026-08-10-npm-release-sequences.zh.md: 1f14c03beb57d3a574305b348379b1542836083c

+ 2 - 0
.agents/notes/implemented/process/2026-08-10-npm-release-sequences.md

@@ -123,6 +123,8 @@ A dsh verification installs the vendored family's pack output too. The harness p
 
 The verification also packs the Landlock entry, which `dsh-sandbox-local` declares as a plain dependency, and omits optional dependencies. The platform packages behind those optional entries need a musl toolchain and one build per architecture, so a job on one runner cannot produce them; a consumer that cannot install them must still start, which is what optional means here. The verification therefore reads a directory by its contents rather than a pack order, because a directory can hold tarballs packed only to satisfy a cross-sequence dependency.
 
+The installed-consumer probe captures npm's HTTP diagnostics and includes them when installation fails. Registry response codes and cache status remain visible even when npm reports a failed peer manifest fetch as `ERESOLVE` with an undefined version.
+
 ### Repository changes this carried
 
 | Item | Content |

+ 2 - 0
.agents/notes/implemented/process/2026-08-10-npm-release-sequences.zh.md

@@ -123,6 +123,8 @@ dsh 的验证会一并安装 vendored 族的 pack 产物。harness 的包把 ven
 
 验证还会打一份 Landlock entry 的 tarball——`dsh-sandbox-local` 把它声明为普通 `dependencies`——同时略去可选依赖。那些可选项背后的平台包需要 musl 工具链且每个架构各构建一次,单台 runner 产不出来;而装不到它们的消费方也必须能起,这正是「可选」在这里的含义。因此验证按目录内容读取 tarball,而不是读发布顺序:一个目录可能只装着为满足跨序列依赖而打出来的包,任何发布顺序都不描述它。
 
+已安装消费方探针会捕获 npm 的 HTTP 诊断信息,并在安装失败时输出。即使 npm 将 peer manifest 获取失败报告为版本未定义的 `ERESOLVE`,日志中仍能看到 registry 响应码和缓存状态。
+
 ### 本次带出的仓库改动
 
 | 项 | 内容 |

+ 6 - 0
.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.i18n.yaml

@@ -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/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md
+2026-09-03-minimal-profiles-persistent-shell-only.md: d58210cb890e4264b02d591f213045b592ca6bfc
+2026-09-03-minimal-profiles-persistent-shell-only.zh.md: 481ef479c2dc01ae503f621df976529aeb6bdbe0

+ 35 - 0
.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md

@@ -0,0 +1,35 @@
+# Agent Note: Minimal profiles expose only a persistent shell
+
+Status: implemented
+
+English | [中文](2026-09-03-minimal-profiles-persistent-shell-only.zh.md)
+
+## Problem
+
+The shipped Web `minimal` preset and standalone `sdk-minimal` profile exposed `str_replace_editor` beside their persistent shell. The editor added a second file-mutation interface and its complete schema to every minimal model request, although the shell already provides file inspection and mutation. It also required a dedicated `fs-local` service that no other row in either minimal composition consumed.
+
+Using one persistent shell gives the model a consistent file-operation interface and keeps the harness composition aligned with that interface. Leaving the editor mounted but hidden through a presentation filter would preserve an inactive capability that could reappear when presentation configuration changes.
+
+## Decision
+
+The shipped minimal compositions expose exactly one platform-selected persistent shell: `bash` on Linux and macOS, or `pwsh` on Windows. Neither composition mounts `@deepseek-ai/dsh-tool-str-replace-editor`, a filesystem tool, or the `fs-local` service that supported the editor. The fixed complete persona, absence of runtime context and compaction, shell timeout, and launch-specific host services remain unchanged.
+
+The standalone editor package remains available for explicit custom compositions. A trusted user-authored preset or higher profile patch must insert the editor into the Cordis tree with a filesystem provider in the same service scope; the shipped `minimal` and `sdk-minimal` defaults never insert it. The [Python SDK guide](../../../../docs/user/guide/python-sdk.md#opt-in-to-str_replace_editor) provides an executable patch example.
+
+The shared [persistent Bash consumer](../../../../packages/shell/tool-bash-persistent/README.md#model-experience) uses the one-shot shell's command-status wording while retaining its persistent state. Settled commands append `[Command finished with exit code N]`, including success; timeout output includes `[Command timed out or OOM]` and the shell-reset notice. Trailing newlines are removed before the status trailer. Both minimal Bash descriptions state that network access depends on the task environment. Explicit compositions using this consumer share its output behavior; the persistent PowerShell description and output remain unchanged.
+
+Exact composition tests assert the single tool and the absence of a preset-local filesystem service. The `sdk-minimal` bundle test and built config dump assert that its row and dependency allowlists contain neither `fs-local` nor `dsh-tool-str-replace-editor`. Web and packaged-Python model-visible snapshots pin the one-tool schema roster. SDK profile smoke tests execute the guide's editor patch and verify file creation and viewing.
+
+This decision partially supersedes the tool selection in [the bare minimal runtime](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md) and the minimal exception in [the base editor decision](2026-09-05-base-default-file-editor.md). Those notes retain authority for prompt ownership, no-compaction behavior, and base-backed file editing. [The application architecture](../../../../docs/architecture.md) owns profile launch and bundle layering.
+
+## Alternatives considered
+
+**Keep the editor row and hide its schema.** Rejected because a presentation or restriction layer would leave the capability in the minimal composition and make its absence depend on another setting.
+
+**Remove the editor package from the distribution.** Rejected because explicit custom compositions remain valid consumers. The requirement concerns the two shipped minimal defaults.
+
+**Keep the editor only in `sdk-minimal`.** Rejected because the two minimal paths would present different tool contracts to the same model class, and the packaged SDK path would retain the schema cost and unused filesystem service.
+
+## Consequences
+
+Minimal agents inspect and modify files through their persistent shell. Their model requests carry one tool schema, and their compositions own no filesystem service. The editor package and explicit editor compositions remain available. Web and SDK replay fixtures pin the persistent Bash status trailers alongside shell state and file effects.

+ 35 - 0
.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md

@@ -0,0 +1,35 @@
+# Agent Note: 极简 profile 只提供持久 shell
+
+Status: implemented
+
+[English](2026-09-03-minimal-profiles-persistent-shell-only.md) | 中文
+
+## 问题
+
+随附 Web `minimal` preset 与独立 `sdk-minimal` profile 在持久 shell 之外还提供 `str_replace_editor`。Shell 已经可以检查和修改文件,editor 仍会为每个极简模型请求增加第二种文件修改接口及其完整 schema。它还要求挂载一个专用 `fs-local` 服务,而两份极简组合中的其他配置项都不使用该服务。
+
+只使用持久 shell 可以为模型提供一致的文件操作接口,并让 harness 组合与这一接口保持一致。如果保留 editor 的挂载,只通过呈现层过滤隐藏它,那么呈现配置变化时,该能力仍可能重新出现。
+
+## 决策
+
+随附的极简组合只提供一个按平台选择的持久 shell:Linux 与 macOS 使用 `bash`,Windows 使用 `pwsh`。两份组合都不挂载 `@deepseek-ai/dsh-tool-str-replace-editor`、文件系统工具或支撑 editor 的 `fs-local` 服务。固定的 complete persona、运行时上下文与 compaction 的缺失、shell 超时和各启动路径的宿主服务保持不变。
+
+独立 editor 包仍可用于显式自定义组合。受信任的用户自定义 preset 或更高优先级的 profile patch 必须将 editor 插入 Cordis tree,并在同一服务作用域内提供文件系统后端;随附的 `minimal` 与 `sdk-minimal` 默认组合不会插入它。[Python SDK 指南](../../../../docs/user/guide/python-sdk.zh.md#opt-in-to-str_replace_editor)提供可执行的 patch 示例。
+
+共享的[持久 Bash 消费方](../../../../packages/shell/tool-bash-persistent/README.zh.md#model-experience)保留跨调用状态,并采用单次 shell 的命令状态文案。完成的命令追加 `[Command finished with exit code N]`,成功时也包含该标记;超时输出包含 `[Command timed out or OOM]` 和 shell 重置说明。追加状态标记前会移除输出末尾的全部换行。两份极简 Bash 描述都说明网络访问取决于任务环境。使用该消费方的显式组合共享相同的输出行为;持久 PowerShell 的描述与输出保持不变。
+
+精确组合测试会断言单工具清单以及 preset 内不存在文件系统服务。`sdk-minimal` bundle 测试与构建后配置转储会断言配置项和依赖 allowlist 都不含 `fs-local` 或 `dsh-tool-str-replace-editor`。Web 与打包 Python 的模型可见快照会固定单工具 schema 清单。SDK profile 冒烟测试会执行指南中的 editor patch,并验证文件创建和查看。
+
+本决策部分取代[极简裸运行时](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md)中的工具选择,以及[base 编辑器决策](2026-09-05-base-default-file-editor.zh.md)中的极简例外。这些 Agent Note 继续负责提示词所有权、无 compaction 行为和基于 base 的文件编辑。[应用架构](../../../../docs/architecture.zh.md)负责 profile 启动与 bundle 分层。
+
+## 考虑过的替代方案
+
+**保留 editor 配置项并隐藏其 schema。** 不予采用,因为呈现层或限制层会让该能力继续留在极简组合中,并使其缺失依赖另一项设置。
+
+**从发行物中删除 editor 包。** 不予采用,因为显式自定义组合仍是有效消费方。本需求只涉及两份随附的极简默认组合。
+
+**只在 `sdk-minimal` 中保留 editor。** 不予采用,因为两条极简路径会向同类模型提供不同的工具约定,而且打包 SDK 路径仍会承担 schema 成本和未被其他配置项使用的文件系统服务。
+
+## 后果
+
+极简 agent 通过持久 shell 检查和修改文件。模型请求只携带一个工具 schema,组合不拥有文件系统服务。editor 包和显式 editor 组合仍然可用。Web 与 SDK 回放快照会同时固定持久 Bash 的状态标记、shell 状态和文件操作结果。

+ 2 - 2
.agents/notes/implemented/simplification/2026-09-05-base-default-file-editor.i18n.yaml

@@ -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-09-05-base-default-file-editor.md
-2026-09-05-base-default-file-editor.md: 68a86769a99d697f9c7c767e9ba59b0cf61669b1
-2026-09-05-base-default-file-editor.zh.md: e8302b609a8647b1a0b90923361da06e859868ab
+2026-09-05-base-default-file-editor.md: b87d7986e7beeab30ff5414908239ba95022d231
+2026-09-05-base-default-file-editor.zh.md: db3c70d0a28527391073ddc34a72e9faabe1c3be

+ 3 - 3
.agents/notes/implemented/simplification/2026-09-05-base-default-file-editor.md

@@ -12,7 +12,7 @@ The shared base selects both `read`/`write`/`edit` and `str_replace_editor`, whi
 
 The [base patch](../../../../packages/bundle/base/cordis.patch.yml) selects `read`, `write`, and `edit` for file editing. It does not insert `tool-str-replace-editor`; SDK and Web application patches therefore need no disabling override. The editor package remains available to compositions that insert it explicitly.
 
-[Web minimal](../../../../packages/preset/agent-presets/presets/minimal/agent.cordis.yml) inserts its own `str-replace-editor` row in the agent scope. The standalone [sdk-minimal bundle](../../../../packages/bundle/sdk-minimal/cordis.patch.yml) inserts its own row without inheriting base. Both minimal compositions retain their editor.
+Web minimal and the standalone `sdk-minimal` bundle own their tool selection independently of base. The [persistent-shell-only decision](2026-09-03-minimal-profiles-persistent-shell-only.md) owns their single-tool defaults.
 
 This refines the shared tool defaults in [one dsh launcher](../architecture/2026-08-22-single-dsh-application-launcher.md). That note remains active for launch ownership, shared services, and patch precedence; no active note is fully superseded.
 
@@ -20,7 +20,7 @@ This refines the shared tool defaults in [one dsh launcher](../architecture/2026
 
 **Disable the editor separately in each application.** This leaves overlapping defaults in base and requires each consumer to opt out. The base owns the shared choice directly.
 
-**Delete the tool package or remove it from minimal.** The dedicated minimal compositions use this interface for file operations. Keeping the package and their explicit rows preserves that behavior.
+**Delete the tool package.** Explicit custom compositions still use this interface. Base default selection does not remove the package or constrain independently owned minimal defaults.
 
 ## Consequences
 
@@ -28,4 +28,4 @@ Base-backed SDK, headless, ACP, and custom profiles omit the editor schema by de
 
 ## Verification
 
-The [SDK process tests](../../../../apps/cli/tests/profiles/sdk/keyless-smoke.e2e.ts) capture actual model requests for default file tools, explicit editor insertion, and the standalone minimal roster. The [headless process test](../../../../apps/cli/tests/profiles/headless/tests/keyless-smoke.e2e.ts) checks the shared default through its application. [Web minimal snapshots](../../../../apps/web/tests/minimal-preset.snapshot.ts) exercise the editor through the minimal preset. The [headless](../../../../snapshots/session/headless.snapshot.ts), [SDK](../../../../snapshots/sdk/sdk.snapshot.ts), and [ACP](../../../../snapshots/acp/acp.snapshot.ts) recorded sessions pin the assembled model-visible outputs, including the SDK fixture that explicitly inserts the editor.
+The [SDK process tests](../../../../apps/cli/tests/profiles/sdk/keyless-smoke.e2e.ts) capture actual model requests for default file tools, explicit editor insertion, and the standalone minimal roster. The [headless process test](../../../../apps/cli/tests/profiles/headless/tests/keyless-smoke.e2e.ts) checks the shared default through its application. The [headless](../../../../snapshots/session/headless.snapshot.ts), [SDK](../../../../snapshots/sdk/sdk.snapshot.ts), and [ACP](../../../../snapshots/acp/acp.snapshot.ts) recorded sessions pin the assembled model-visible outputs, including the SDK fixture that explicitly inserts the editor.

+ 3 - 3
.agents/notes/implemented/simplification/2026-09-05-base-default-file-editor.zh.md

@@ -12,7 +12,7 @@ Status: implemented
 
 [base patch](../../../../packages/bundle/base/cordis.patch.yml) 选择 `read`、`write` 和 `edit` 负责文件编辑。它不插入 `tool-str-replace-editor`;因此 SDK 与 Web 应用 patch 无需禁用覆盖。编辑器包仍可供显式插入它的组合使用。
 
-[Web minimal](../../../../packages/preset/agent-presets/presets/minimal/agent.cordis.yml) 在 agent 作用域插入自己的 `str-replace-editor` 配置项。独立的 [sdk-minimal bundle](../../../../packages/bundle/sdk-minimal/cordis.patch.yml) 不继承 base,自行插入配置项。两种极简组合都保留其编辑器。
+Web minimal 与独立 `sdk-minimal` bundle 各自负责工具选择,不依赖 base。[仅持久 shell 决策](2026-09-03-minimal-profiles-persistent-shell-only.zh.md)负责它们的单工具默认值。
 
 本决策细化了[统一 dsh 启动器](../architecture/2026-08-22-single-dsh-application-launcher.zh.md)中的共享工具默认值。该文档对启动所有权、共享服务和 patch 优先级仍然有效;没有被完全取代的活跃 Agent Note。
 
@@ -20,7 +20,7 @@ Status: implemented
 
 **在每个应用中分别禁用编辑器。** 这会在 base 中保留重叠的默认接口,并要求各消费方主动退出。共享选择由 base 直接负责。
 
-**删除工具包或从 minimal 移除它。** 专用的极简组合通过此接口完成文件操作。保留包及其显式配置项可以保留这一行为。
+**删除工具包。** 显式自定义组合仍使用此接口。base 的默认选择不删除包,也不约束独立负责的极简默认值。
 
 ## Consequences
 
@@ -28,4 +28,4 @@ Status: implemented
 
 ## Verification
 
-[SDK 进程测试](../../../../apps/cli/tests/profiles/sdk/keyless-smoke.e2e.ts) 捕获默认文件工具、显式插入编辑器与独立极简工具清单的实际模型请求。[headless 进程测试](../../../../apps/cli/tests/profiles/headless/tests/keyless-smoke.e2e.ts) 通过所属应用检查共享默认值。[Web minimal 快照](../../../../apps/web/tests/minimal-preset.snapshot.ts) 通过极简 preset 执行编辑器。[headless](../../../../snapshots/session/headless.snapshot.ts)、[SDK](../../../../snapshots/sdk/sdk.snapshot.ts) 与 [ACP](../../../../snapshots/acp/acp.snapshot.ts) 录制会话固定组装后模型可见的输出,包括显式插入编辑器的 SDK fixture。
+[SDK 进程测试](../../../../apps/cli/tests/profiles/sdk/keyless-smoke.e2e.ts) 捕获默认文件工具、显式插入编辑器与独立极简工具清单的实际模型请求。[headless 进程测试](../../../../apps/cli/tests/profiles/headless/tests/keyless-smoke.e2e.ts) 通过所属应用检查共享默认值。[headless](../../../../snapshots/session/headless.snapshot.ts)、[SDK](../../../../snapshots/sdk/sdk.snapshot.ts) 与 [ACP](../../../../snapshots/acp/acp.snapshot.ts) 录制会话固定组装后模型可见的输出,包括显式插入编辑器的 SDK fixture。

+ 2 - 2
.agents/notes/implemented/testing/2026-09-04-session-open-performance-gate.i18n.yaml

@@ -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/testing/2026-09-04-session-open-performance-gate.md
-2026-09-04-session-open-performance-gate.md: d5447f93666a5cb27af2073a4a66f99d0c39f6ee
-2026-09-04-session-open-performance-gate.zh.md: 657f77fb76cf84afc36301d22d9a7798c559075b
+2026-09-04-session-open-performance-gate.md: d69e7424c5b34bc9f50d4a1d355279b65b7dfd8a
+2026-09-04-session-open-performance-gate.zh.md: cce4ae3921d70d43dde823d64dca5a0976344100

+ 2 - 0
.agents/notes/implemented/testing/2026-09-04-session-open-performance-gate.md

@@ -56,6 +56,8 @@ The pre-stack implementation keeps V0 as its current format, so first open does
 
 The [standard two-CPU run](https://github.com/deepseek-harness/deepseek-harness/actions/runs/34023970384/job/101461539961) at `ca3ffe95dac2c55eefeb16ed9b61067bbd19ee90` uses Node 24.20.0 x64 and Ubuntu image `20260831.293.1`. Its five current-generation `open` samples are 49.2, 47.4, 49.1, 48.6, and 48.1 ms: median 48.6 ms, maximum 49.2 ms. The rounded 50 ms CI expectation gives a 63 ms limit without reapplying the 2× machine scale. The log identifies two available CPUs but not their model; it does not isolate hardware from the Node-version change. This is endpoint-specific runner calibration, not evidence of an application optimization or a new reference-machine measurement. Every other benchmark passes its existing budget. Deterministic controls reject the observed median at the historical 30 ms limit, accept it at 63 ms, reject a synthetic 75 ms reopen median, and reject a synthetic 4,000 ms first-open duration at its unchanged 550 ms limit. These controls verify budget enforcement, not a measured new regression.
 
+A cold-verifier packaging change removes runtime workspace-module loading without changing these budgets or the measured endpoint. On macOS arm64, Node 24.18.0, the same 127,400-event fixture at `ac48359b195558806ee5a2286697074fd1a52815` takes 164.2, 162.4, 159.7, 149.3, and 167.7 ms for first writable resume (median 162.4 ms). Bundling the verifier through the workspace build gives 119.8, 120.9, 121.9, 122.3, and 121.8 ms (median 121.8 ms, 25% lower). Retained heap stays at 5.4 MB; median peak RSS changes from 144.9 to 143.7 MB. Reopen medians are 27.5 and 27.1 ms, and all 16 Session cases, including the 128 MB completion checks, pass. A CPU profile attributes part of the old verifier cost to module resolution and compilation. The isolated-package built-worker test fails on the original worker because its workspace imports cannot resolve, and passes with the bundled worker, including rejection of an incorrect event count. These local results do not establish Linux runner timing; the existing 450 ms CI gate remains the acceptance check.
+
 The calibrated source budgets are:
 
 | Measurement | Reference expectation | CI budget |

+ 2 - 0
.agents/notes/implemented/testing/2026-09-04-session-open-performance-gate.zh.md

@@ -56,6 +56,8 @@ Session benchmark 使用固定参数合成 released-v0 输入:200 轮,每轮
 
 `ca3ffe95dac2c55eefeb16ed9b61067bbd19ee90` 上的[标准双 CPU 运行](https://github.com/deepseek-harness/deepseek-harness/actions/runs/34023970384/job/101461539961)使用 Node 24.20.0 x64 和 Ubuntu 镜像 `20260831.293.1`。当前 generation `open` 的五次样本为 49.2、47.4、49.1、48.6 和 48.1 ms:中位数 48.6 ms,最大值 49.2 ms。取整后的 50 ms CI 预期值给出 63 ms 上限,不重复乘以 2 倍机器系数。日志标明两个可用 CPU,但未记录型号;它无法区分硬件变化与 Node 版本变化的影响。这是端点专属的运行器校准,不是应用优化或参考机器新测量的证据。其他每项 benchmark 均通过既有预算。确定性正反例在历史 30 ms 上限下拒绝实测中位数,在 63 ms 下接受它,拒绝合成的 75 ms reopen 中位数,并以未改变的 550 ms 上限拒绝合成的 4,000 ms 首次打开耗时。这些正反例验证预算执行,不代表测得新的退化。
 
+一次冷 verifier 打包调整移除了运行时 workspace 模块加载,未改变这些预算或测量终点。在 macOS arm64、Node 24.18.0 上,`ac48359b195558806ee5a2286697074fd1a52815` 对同一份 127,400-event fixture 的首次 writable resume 耗时为 164.2、162.4、159.7、149.3、167.7 ms(中位数 162.4 ms)。通过 workspace build 打包 verifier 后为 119.8、120.9、121.9、122.3、121.8 ms(中位数 121.8 ms,降低 25%)。Retained heap 保持 5.4 MB;peak RSS 中位数从 144.9 变为 143.7 MB。Reopen 中位数为 27.5 和 27.1 ms,包含 128 MB completion check 的全部 16 项 Session 用例通过。CPU profile 将旧 verifier 的部分成本归因于模块解析和编译。隔离 package 的 built-worker 测试在旧 worker 上因无法解析 workspace import 而失败,在打包后的 worker 上通过,同时验证错误的 event count 会被拒绝。这些本地结果不能证明 Linux runner 耗时;现有 450 ms CI gate 仍是验收检查。
+
 校准后的源码预算如下:
 
 | 测量项 | 参考机预期 | CI 预算 |

+ 2 - 2
.agents/notes/implemented/testing/2026-09-06-backend-continuation-performance.i18n.yaml

@@ -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/testing/2026-09-06-backend-continuation-performance.md
-2026-09-06-backend-continuation-performance.md: f4316e790cf62f5027a0f7bfb2d3148cc79e7536
-2026-09-06-backend-continuation-performance.zh.md: 0fb36ba5375c61e907791b31d96f09062b7f62a3
+2026-09-06-backend-continuation-performance.md: 62d8af10015cc2da7b399aae3c9d197f75931753
+2026-09-06-backend-continuation-performance.zh.md: 7c8904fa019b27362e2cdb0e50697921913e5d09

+ 2 - 0
.agents/notes/implemented/testing/2026-09-06-backend-continuation-performance.md

@@ -23,6 +23,8 @@ The shared history has 800 completed two-step turns, four tool calls per turn, a
 
 The tool execution pipeline, request preparation, Session projections required by those services, persistence, and catalog observations remain production code. Only the model adapter and bounded tool body are synthetic. The adapter retains a request counter, not request objects, so the fixture cannot manufacture a growing retention cost. Sequential input means each idle interval belongs to the one request delivered by this worker; it does not generalize idle to a per-message completion API under concurrent input.
 
+The SDK fixture explicitly inserts `fs-local` and `str_replace_editor` through its profile patch. This preserves the calibrated file-view workload independently of the [minimal profile's shell-only defaults](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.md). File reads, timing endpoints, and budgets remain the same.
+
 Five samples report raw wall time, CPU user/system time, peak RSS, endpoint counts, and the minimum, median, and maximum total wall time. Budgets enforce the unrounded median. Continuation additionally measures retained heap against an initialized Host: two explicit GCs separated by an event-loop yield precede and follow the timed operation, while the idle Agent remains reachable. The measured delta therefore includes the resident historical Session and live additions, not just newly appended turns. GC and teardown are outside timing; flush is inside. Request-history retention starts after resume and is diagnostic only. Catalog peak RSS is diagnostic; no retained-heap budget claims to measure already-released child observations.
 
 The parent bounds every child to 60 seconds, checks timeout, signal, exit, and report independently, awaits process close, and removes private roots after failures. Context and Agent teardown run in finally blocks. Seed processes cannot warm the measured process's caches. Filesystem caches are not forcibly evicted: cold means a fresh process, not cold physical storage.

+ 2 - 0
.agents/notes/implemented/testing/2026-09-06-backend-continuation-performance.zh.md

@@ -23,6 +23,8 @@ Status: implemented
 
 工具执行管线、请求准备、这些服务所需的 Session 投影、持久化和目录观察均保留生产代码。只有模型适配器和有界工具体是合成的。适配器只保留请求计数,不保留请求对象,因此 fixture(测试前置数据)不会制造不断增长的保留成本。顺序输入使每个空闲区间对应此 worker 提交的唯一请求;这不代表并发输入时可以把空闲状态推广为逐消息完成 API。
 
+SDK fixture 通过 profile patch 显式插入 `fs-local` 和 `str_replace_editor`。这使经校准的文件查看负载不依赖[极简 profile 只提供 shell 的默认组合](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md)。文件读取、计时终点和预算保持不变。
+
 五个样本报告原始壁钟时间、CPU 用户态/内核态时间、峰值 RSS、终点计数及总壁钟时间的最小值、中位数和最大值。预算约束未经舍入的中位数。续聊还相对已初始化 Host 测量保留堆内存:计时操作前后各执行两次显式 GC,中间让出一次事件循环,空闲 Agent 始终可达。因此该增量包含常驻历史 Session 和实时追加,而不只是新轮次。GC 与资源释放不计时;flush 计时。请求历史的内存基线从恢复后开始,只作诊断。目录峰值 RSS 仅作诊断;没有保留堆预算声称衡量已经释放的子会话观察。
 
 父进程为每个子进程设置 60 秒上限,独立检查超时、信号、退出状态和报告,等待进程关闭,并在失败后删除私有根目录。Context 和 Agent 在 finally 中释放。播种进程无法预热被测进程的缓存。不强制清除文件系统缓存:冷指新进程,不指冷物理存储。

+ 2 - 2
apps/cli/reference/README.i18n.yaml

@@ -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 apps/cli/reference/README.md
-README.md: 42fb2855e97465417ca284944102d3c6a610416d
-README.zh.md: 786774dae50a2dfcadbf3fd260b4680277c0d39e
+README.md: 99ef8310cd1316e9ee4ade55fcac00766ae1e821
+README.zh.md: aa446d539a5b666e7745f459b5a014b22e72f90e

+ 2 - 2
apps/cli/reference/README.md

@@ -90,11 +90,11 @@ The production Web runner needs built package and frontend artifacts (`pnpm run
 
 Process shutdown gives the plugin tree up to five seconds to dispose. The first `SIGINT`/`SIGTERM` starts that graceful drain — `SIGTERM` is a supervisor's ordinary stop request and exits 0 on every surface, `SIGINT` reports 130; a second signal forces immediate exit. If one-shot normal completion is already stuck in disposal, the first `Ctrl+C` is the escalation and exits immediately instead of being swallowed.
 
-The base-backed modes treat the invoking directory as the default workspace root, load applicable `AGENTS.md` or `CLAUDE.md` instructions with a 65,536-byte render budget, and use an in-memory SQLite session content index. The standalone `sdk-minimal` profile uses the invoking directory as its local filesystem and sandbox-policy root but intentionally omits instruction discovery and SQLite. A `patchReload: live` profile watches valid edits of both `cordis.patch.yml` layers (profile and home) and reapplies them transactionally; a `startup` profile applies them once. A one-shot surface exits through its bounded shutdown, which disposes any live watchers.
+The base-backed modes treat the invoking directory as the default workspace root, load applicable `AGENTS.md` or `CLAUDE.md` instructions with a 65,536-byte render budget, and use an in-memory SQLite session content index. The standalone `sdk-minimal` profile uses the invoking directory as its sandbox-policy root but intentionally omits filesystem tools, instruction discovery, and SQLite. A `patchReload: live` profile watches valid edits of both `cordis.patch.yml` layers (profile and home) and reapplies them transactionally; a `startup` profile applies them once. A one-shot surface exits through its bounded shutdown, which disposes any live watchers.
 
 New sessions in base-backed profiles default to the `workspace-write` permission preset. Bash and filesystem mutations are restricted to the session workspace and platform temporary roots; reads and network access are not confined, while process visibility depends on the selected sandbox backend — bwrap runs commands in a private PID namespace that hides host processes, and Landlock and Seatbelt leave host process visibility unchanged. `DSH_PERMISSION_MODE` changes the process fallback. Stored General-settings permissions affect later Web sessions, not an already-open one. The standalone `sdk-minimal` tree instead pins `danger-full-access` and mounts no approval or permission-settings service.
 
-`DSH_TOOLS_MODE` selects `native`, `ptc`, or `both` for the process; another value fails at boot. The shipped `minimal` agent preset keeps that deployment presentation, fixes the complete system prompt to `You are a helpful software engineer assistant.`, and composes only persistent `bash` plus `str_replace_editor`. Select 极简模式 when creating a Web session; every other prompt section and model-facing plugin remains absent from that agent while the shared browser, workspace, persistence, sandbox, and permission host stays in place.
+`DSH_TOOLS_MODE` selects `native`, `ptc`, or `both` for the process; another value fails at boot. The shipped `minimal` agent preset keeps that deployment presentation, fixes the complete system prompt to `You are a helpful software engineer assistant.`, and composes only the platform-selected persistent shell. Select 极简模式 when creating a Web session; every other prompt section and model-facing plugin remains absent from that agent while the shared browser, workspace, persistence, sandbox, and permission host stays in place.
 
 ## Shared deployment behavior
 

+ 2 - 2
apps/cli/reference/README.zh.md

@@ -92,11 +92,11 @@ dsh web --help
 
 进程关闭时,插件树最多有 5 秒完成 dispose。首次收到 `SIGINT` 或 `SIGTERM` 时会开始优雅排空:`SIGTERM` 是监督进程发出的常规停止请求,在所有运行模式下都以 0 退出;`SIGINT` 则报告 130。第二次收到信号时会立即强制退出。如果一次性运行在正常结束时已经卡在 dispose 阶段,第一次按下 `Ctrl+C` 就会直接升级为强制退出,而不会被忽略。
 
-基于 base 的模式都将运行命令时所在的目录作为默认 workspace 根目录,以 65,536 字节渲染预算加载适用的 `AGENTS.md` 或 `CLAUDE.md` 指令,并使用内存 SQLite 会话内容索引。独立的 `sdk-minimal` profile 把运行命令时所在的目录作为本地文件系统与沙箱策略根目录,但刻意省略指令发现与 SQLite。`patchReload: live` profile 会监视 profile 与 home 两个 `cordis.patch.yml` 配置层的有效变更,并以事务方式重新应用;`startup` profile 则只应用一次。一次性运行模式通过有界关闭流程退出,该流程会 dispose(资源释放)所有实时监视器。
+基于 base 的模式都将运行命令时所在的目录作为默认 workspace 根目录,以 65,536 字节渲染预算加载适用的 `AGENTS.md` 或 `CLAUDE.md` 指令,并使用内存 SQLite 会话内容索引。独立的 `sdk-minimal` profile 把运行命令时所在的目录作为沙箱策略根目录,但刻意省略文件系统工具、指令发现与 SQLite。`patchReload: live` profile 会监视 profile 与 home 两个 `cordis.patch.yml` 配置层的有效变更,并以事务方式重新应用;`startup` profile 则只应用一次。一次性运行模式通过有界关闭流程退出,该流程会 dispose(资源释放)所有实时监视器。
 
 基于 base 的 profile 中,新会话默认使用 `workspace-write` 权限预设。Bash 和文件系统修改仅限于会话 workspace 与平台临时根目录;读取和网络访问不受限制,进程可见性则取决于所选沙箱后端——bwrap 在私有 PID 命名空间中运行命令并隐藏宿主进程,Landlock 与 Seatbelt 保持宿主进程可见性不变。`DSH_PERMISSION_MODE` 更改进程后备值。General settings 中存储的权限影响后续 Web 会话,不改变已打开的会话。独立的 `sdk-minimal` 配置树则固定为 `danger-full-access`,且不挂载 approval 或权限 settings 服务。
 
-`DSH_TOOLS_MODE` 为进程选择 `native`、`ptc` 或 `both`;其他值会导致启动失败。随附的 `minimal` agent preset 会保留该部署的呈现方式,将完整系统提示词固定为 `You are a helpful software engineer assistant.`,并且仅组合持久 `bash` 和 `str_replace_editor`。创建 Web 会话时请选择极简模式;该 agent 不包含任何其他提示词段落或面向模型的插件,而共享的浏览器、workspace、持久化、沙箱与权限宿主保持不变。
+`DSH_TOOLS_MODE` 为进程选择 `native`、`ptc` 或 `both`;其他值会导致启动失败。随附的 `minimal` agent preset 会保留该部署的呈现方式,将完整系统提示词固定为 `You are a helpful software engineer assistant.`,并且仅组合按平台选择的持久 shell。创建 Web 会话时请选择极简模式;该 agent 不包含任何其他提示词段落或面向模型的插件,而共享的浏览器、workspace、持久化、沙箱与权限宿主保持不变。
 
 ## 共享部署行为
 

+ 0 - 2
apps/cli/tests/built-bin.e2e.ts

@@ -1081,7 +1081,6 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
         ['pty', '@deepseek-ai/dsh-terminal'],
         ['terminal-bash', '@deepseek-ai/dsh-terminal-bash'],
         ['terminal-pwsh', '@deepseek-ai/dsh-terminal-bash'],
-        ['fs-local', '@deepseek-ai/dsh-fs-local'],
         ['timer', '@deepseek-ai/cordis-plugin-timer'],
         ['llm', '@deepseek-ai/dsh-llm'],
         ['session', '@deepseek-ai/dsh-session'],
@@ -1099,7 +1098,6 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
         ['agent-loop', '@deepseek-ai/dsh-agent-loop'],
         ['persistent-bash', '@deepseek-ai/dsh-tool-bash-persistent'],
         ['persistent-pwsh', '@deepseek-ai/dsh-tool-pwsh-persistent'],
-        ['str-replace-editor', '@deepseek-ai/dsh-tool-str-replace-editor'],
         ['sessions', '@deepseek-ai/dsh-session-persistence-jsonl'],
       ])
       expect(stdout).toContain('# == @deepseek-ai/dsh-sdk-minimal')

+ 55 - 7
apps/cli/tests/profiles/sdk/keyless-smoke.e2e.ts

@@ -189,8 +189,25 @@ describe('Python SDK dsh profile keyless smoke', () => {
     }
   }, 40_000)
 
-  it('boots the standalone minimal profile through its generated manifest', async () => {
+  it.each([
+    { label: 'boots the standalone minimal profile through its generated manifest', editorEnabled: false },
+    { label: 'executes the documented editor opt-in patch with sdk-minimal', editorEnabled: true },
+  ])('$label', async ({ editorEnabled }) => {
     const root = await mkdtemp(join(tmpdir(), 'dsh-python-sdk-minimal-'))
+    const editorPatch = join(root, 'editor.patch.yml')
+    if (editorEnabled) {
+      const guide = await readFile(join(repoRoot, 'docs/user/guide/python-sdk.md'), 'utf8')
+      const yaml = guide.split('<a id="opt-in-to-str_replace_editor"></a>')[1]
+        ?.match(/```yaml\n([\s\S]*?)```/)?.[1]
+      expect(yaml).toBeDefined()
+      await writeFile(editorPatch, yaml!)
+    }
+    const editorFile = join(root, 'editor.txt')
+    const editorContent = 'sdk-minimal editor opt-in\n'
+    const editorCalls = editorEnabled ? [
+      { command: 'create', path: editorFile, file_text: editorContent },
+      { command: 'view', path: editorFile },
+    ] : []
     const modelRequests: Record<string, unknown>[] = []
     const modelServer = createServer((request, response) => {
       let body = ''
@@ -200,8 +217,21 @@ describe('Python SDK dsh profile keyless smoke', () => {
         modelRequests.push(JSON.parse(body) as Record<string, unknown>)
         response.writeHead(200, { 'content-type': 'text/event-stream' })
         response.write('data: {"choices":[{"delta":{"role":"assistant","content":null}}]}\n\n')
-        response.write('data: {"choices":[{"delta":{"content":"done"}}]}\n\n')
-        response.write('data: {"choices":[{"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":3,"completion_tokens":1}}\n\n')
+        const toolCall = editorCalls[modelRequests.length - 1]
+        if (toolCall) {
+          response.write(`data: ${JSON.stringify({ choices: [{ delta: { tool_calls: [{
+            index: 0,
+            id: `editor-${toolCall.command}`,
+            type: 'function',
+            function: { name: 'str_replace_editor', arguments: JSON.stringify(toolCall) },
+          }] } }] })}\n\n`)
+        } else {
+          response.write('data: {"choices":[{"delta":{"content":"done"}}]}\n\n')
+        }
+        response.write(`data: ${JSON.stringify({
+          choices: [{ delta: {}, finish_reason: toolCall ? 'tool_calls' : 'stop' }],
+          usage: { prompt_tokens: 3, completion_tokens: 1 },
+        })}\n\n`)
         response.end('data: [DONE]\n\n')
       })
     })
@@ -214,6 +244,7 @@ describe('Python SDK dsh profile keyless smoke', () => {
       binScript,
       '--profile',
       'sdk-minimal',
+      ...(editorEnabled ? ['--patch', editorPatch] : []),
     ], {
       cwd: repoRoot,
       env: {
@@ -251,11 +282,14 @@ describe('Python SDK dsh profile keyless smoke', () => {
         method: 'session/prompt',
         params: { sessionId: 'minimal', contentBlocks: [{ type: 'text', text: 'inspect tools' }] },
       })}\n`)
-      await waitForLine(lines, (value) => {
+      const turnEnd = await waitForLine(lines, (value) => {
         const params = value.params as Record<string, unknown> | undefined
         const event = params?.event as Record<string, unknown> | undefined
         return params?.sessionId === 'minimal' && event?.type === 'turn/end'
       }, () => stderr)
+      expect(turnEnd).toMatchObject({
+        params: { event: { data: { reason: { kind: 'completed' } } } },
+      })
 
       const profile = JSON.parse(
         await readFile(join(root, '.dsh', 'profiles', 'sdk-minimal', 'package.json'), 'utf8'),
@@ -266,13 +300,27 @@ describe('Python SDK dsh profile keyless smoke', () => {
       })
       expect(modelRequests[0]?.tools).toEqual(expect.any(Array))
       const tools = modelRequests[0]?.tools as { function?: { name?: string } }[]
-      expect(tools.map(tool => tool.function?.name).sort()).toEqual(
-        [process.platform === 'win32' ? 'pwsh' : 'bash', 'str_replace_editor'].sort(),
-      )
+      expect(tools.map(tool => tool.function?.name)).toEqual([
+        process.platform === 'win32' ? 'pwsh' : 'bash',
+        ...(editorEnabled ? ['str_replace_editor'] : []),
+      ])
+      expect(modelRequests).toHaveLength(editorEnabled ? 3 : 1)
+      if (editorEnabled) {
+        expect(await readFile(editorFile, 'utf8')).toBe(editorContent)
+        expect(modelRequests[2]?.messages).toEqual(expect.arrayContaining([
+          expect.objectContaining({
+            role: 'tool',
+            tool_call_id: 'editor-view',
+            content: expect.stringContaining(editorContent.trim()) as unknown,
+          }),
+        ]))
+      }
 
       child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'shutdown' })}\n`)
       await waitForLine(lines, value => value.id === 3, () => stderr)
       const exit = await child
+      expect(exit.timedOut, stderr).toBe(false)
+      expect(exit.signal, stderr).toBeUndefined()
       expect(exit.exitCode, `signal=${String(exit.signal)}; stderr=${stderr}`).toBe(0)
     } finally {
       child.kill('SIGKILL')

+ 15 - 16
apps/cli/tests/web-agent-presets.e2e.ts

@@ -32,8 +32,7 @@ const INSTALL_ANCHOR = join(REPO_ROOT, 'apps/cli/package.json')
 const MINIMAL_PROMPT = 'You are a helpful software engineer assistant.'
 const MINIMAL_BASH_DESCRIPTION = `Run commands in a bash shell
 * When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped.
-* You don't have access to the internet via this tool.
-* You do have access to a mirror of common linux and python packages via apt and pip.
+* Network access depends on the task environment. Prefer configured mirrors/proxies when they are available.
 * State is persistent across command calls and discussions with the user.
 * To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.
 * Please avoid commands that may produce a very large amount of output.
@@ -191,9 +190,7 @@ describe('the shipped Web composition', () => {
   it('leaves the global tool layer empty', () => {
     // Every model-facing tool belongs to a preset, `ask_user_question`
     // included: a tool in the global layer reaches EVERY agent regardless of
-    // which preset composed it, so a two-tool benchmark surface would really
-    // present three. A regression here means an agent-plane row came back to
-    // the host composition.
+    // which preset composed it, expanding that preset's tool list.
     expect(toolNames(ctx)).toEqual([])
   })
 
@@ -289,7 +286,7 @@ describe('the shipped Web composition', () => {
     }
   })
 
-  it('composes the exact RL prompt and two tools from `minimal`', async () => {
+  it('composes the exact RL prompt and persistent shell from `minimal`', async () => {
     const handle = await ctx.agents.create({
       sessionId: SessionId('preset-minimal'),
       setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'minimal').then(() => undefined),
@@ -299,11 +296,13 @@ describe('the shipped Web composition', () => {
       expect(assembly.sections).toEqual([
         { name: 'deployment:persona-prefix', text: MINIMAL_PROMPT },
       ])
-      expect(assembly.tools.map(tool => tool.name)).toEqual(['bash', 'str_replace_editor'])
+      expect(assembly.tools.map(tool => tool.name)).toEqual(['bash'])
       expect(assembly.tools.find(tool => tool.name === 'bash')?.description).toBe(MINIMAL_BASH_DESCRIPTION)
-      expect(JSON.stringify(assembly.tools.find(tool => tool.name === 'str_replace_editor')?.parameters))
-        .toContain('Absolute path')
       expect(ctx.commands.find(handle.agent, 'goal')).toBeUndefined()
+      // serviceFor reports preset-owned providers; unisolated consumers inherit the host fs.
+      expect(ctx.agentPresets.serviceFor(handle.agent, 'fs')).toBeUndefined()
+      expect(ctx.get('fs')?.sandboxMode).toBeDefined()
+      expect(handle.agent.ctx.get('fs')?.sandboxMode).toBe(ctx.get('fs')?.sandboxMode)
       expect(ctx.agentPresets.serviceFor(handle.agent, 'compaction')).toBeUndefined()
       expect(handle.agent.ctx.get('compaction')).toBeUndefined()
     } finally {
@@ -321,7 +320,7 @@ describe('the shipped Web composition', () => {
       setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'minimal').then(() => undefined),
     })
     try {
-      expect(toolNames(ctx, minimal.agent)).toEqual(['bash', 'str_replace_editor'])
+      expect(toolNames(ctx, minimal.agent)).toEqual(['bash'])
       expect(toolNames(ctx, full.agent).length).toBeGreaterThan(10)
 
       await minimal.dispose()
@@ -472,7 +471,7 @@ describe('the shipped Web composition', () => {
       // stays the preset's choice — minimal mounts no `tool-skill`, so its
       // tool table has no loader even though the global layer is readable.
       expect((await ctx.skills.list({ scope: handle.agent })).map(skill => skill.name)).toContain('dsh-badge')
-      expect(toolNames(ctx, handle.agent)).toEqual(['bash', 'str_replace_editor'])
+      expect(toolNames(ctx, handle.agent)).toEqual(['bash'])
     } finally {
       await handle.dispose()
     }
@@ -851,7 +850,7 @@ describe('authoring a preset on the shipped composition', () => {
     try {
       // The same tools the shipped `minimal` composes, from a directory copied
       // through the service into a root outside the installed harness.
-      expect(toolNames(authorCtx, handle.agent)).toEqual(['bash', 'str_replace_editor'])
+      expect(toolNames(authorCtx, handle.agent)).toEqual(['bash'])
     } finally {
       await handle.dispose()
     }
@@ -886,9 +885,9 @@ describe('the default preset as a user setting', () => {
         setup: agentCtx => ctx.agentPresets.mount(agentCtx).then(() => undefined),
       })
       try {
-        // `mount()` with no id resolves the effective default. Two tools, not
+        // `mount()` with no id resolves the effective default. One tool, not
         // `standard`'s catalog: the setting decided the composition.
-        expect(toolNames(ctx, handle.agent)).toEqual(['bash', 'str_replace_editor'])
+        expect(toolNames(ctx, handle.agent)).toEqual(['bash'])
       } finally {
         await handle.dispose()
       }
@@ -913,7 +912,7 @@ describe('a session keeps the preset it was created with', () => {
     try {
       // The api-proxy guard reads exactly this: the header records what the
       // session runs, so naming anything else is a caller error rather than a
-      // switch. Its history was produced under `minimal`'s two tools.
+      // switch. Its history was produced under `minimal`'s single tool.
       expect(handle.agent.session.header.agentPreset).toBe('minimal')
     } finally {
       await handle.dispose()
@@ -975,7 +974,7 @@ describe('a composition that configures its own preset roots', () => {
       setup: agentCtx => rootsCtx.agentPresets.mount(agentCtx, 'team-spec').then(() => undefined),
     })
     try {
-      expect(toolNames(rootsCtx, handle.agent)).toEqual(['bash', 'str_replace_editor'])
+      expect(toolNames(rootsCtx, handle.agent)).toEqual(['bash'])
     } finally {
       await handle.dispose()
     }

+ 1 - 1
apps/web/tests/agent-preset-authoring.e2e.ts

@@ -150,7 +150,7 @@ describe('web e2e: agent-preset authoring is a host-side copy', () => {
     expect(composition).toBe(await readFile(join(SHIPPED_PRESETS, 'minimal', 'agent.cordis.yml'), 'utf8'))
     const metadata = await readFile(join(userRoot, 'my-agent', 'preset.yml'), 'utf8')
     expect(metadata).toContain('name: 我的模式')
-    expect(metadata).toContain('description: 仅提供持久 bash 与 str_replace_editor 的双工具编码 Agent。')
+    expect(metadata).toContain('description: 仅提供持久 shell 的单工具编码 Agent。')
     expect(metadata).not.toContain('order:')
   }, 60_000)
 

+ 2 - 2
apps/web/tests/expected/agent-preset-authoring/created.expected.md

@@ -43,7 +43,7 @@
         - text: 复制
     - listitem:
       - 'button "设为默认: 极简模式"':
-        - text: 极简模式 内置 仅提供持久 bash 与 str_replace_editor 的双工具编码 Agent。
+        - text: 极简模式 内置 仅提供持久 shell 的单工具编码 Agent。
         - code: minimal
       - 'button "查看: 极简模式"':
         - img
@@ -65,7 +65,7 @@
   - list:
     - listitem:
       - 'button "设为默认: 我的模式"':
-        - text: 我的模式 自定义 仅提供持久 bash 与 str_replace_editor 的双工具编码 Agent。
+        - text: 我的模式 自定义 仅提供持久 shell 的单工具编码 Agent。
         - code: my-agent
       - 'button "查看路径: 我的模式"':
         - img

+ 1 - 1
apps/web/tests/expected/agent-preset-authoring/damaged.expected.md

@@ -43,7 +43,7 @@
         - text: 复制
     - listitem:
       - 'button "设为默认: 极简模式"':
-        - text: 极简模式 内置 仅提供持久 bash 与 str_replace_editor 的双工具编码 Agent。
+        - text: 极简模式 内置 仅提供持久 shell 的单工具编码 Agent。
         - code: minimal
       - 'button "查看: 极简模式"':
         - img

+ 1 - 1
apps/web/tests/expected/agent-preset-authoring/section.expected.md

@@ -43,7 +43,7 @@
         - text: 复制
     - listitem:
       - 'button "设为默认: 极简模式"':
-        - text: 极简模式 内置 仅提供持久 bash 与 str_replace_editor 的双工具编码 Agent。
+        - text: 极简模式 内置 仅提供持久 shell 的单工具编码 Agent。
         - code: minimal
       - 'button "查看: 极简模式"':
         - img

+ 1 - 1
apps/web/tests/expected/agent-preset-selection/menu.expected.md

@@ -3,6 +3,6 @@
     - text: Standard mode Full coding agent with file editing, shell, file and web search, skills, planning, goals, subagents, and workflows.
     - img
   - menuitem "PTC mode Full coding agent without the workflow tool; other tools are exposed through the PTC mode SDK so the model can combine multi-step operations in one TypeScript program."
-  - menuitem "Minimal mode Two-tool coding agent with persistent bash and str_replace_editor."
+  - menuitem "Minimal mode Single-tool coding agent with a persistent shell."
   - menuitem "Creator mode Built for creating custom agent presets, with all Standard mode capabilities plus runtime inspection, plugin experiments, and preset-authoring guidance."
   - menuitem "Refusing mode Resolves, then refuses to start."

+ 6 - 22
apps/web/tests/minimal-preset.snapshot.ts

@@ -1,4 +1,4 @@
-import { mkdir, writeFile } from 'node:fs/promises'
+import { mkdir } from 'node:fs/promises'
 import { join } from 'node:path'
 import { fileURLToPath } from 'node:url'
 import type { Browser, Page } from 'playwright'
@@ -76,7 +76,7 @@ describe('minimal agent preset', () => {
     if (failures.length > 1) throw new AggregateError(failures, 'minimal preset smoke teardown failed')
   })
 
-  it('sends the exact RL prompt and schemas, then executes the persistent shell and editor', async () => {
+  it('sends the exact RL prompt and shell schema, then executes the persistent shell', async () => {
     const requestHeader = agentHandle.agent.session.requestHeader()
     if (requestHeader === undefined) throw new Error('the minimal agent issued no model request')
     const systemPrompt = systemPromptText(agentHandle.agent.session)
@@ -84,9 +84,7 @@ describe('minimal agent preset', () => {
     expect(agentHandle.agent.session.snapshotEvents().some(event => event.type === 'user/message'
       && event.data.source.kind === 'plugin'
       && event.data.source.plugin === '@deepseek-ai/dsh-system-prompt')).toBe(false)
-    const presetFileSystem = scaffold.ctx.agentPresets.serviceFor(agentHandle.agent, 'fs')
-    expect(presetFileSystem).toBeDefined()
-    expect(presetFileSystem?.sandboxMode).toBeUndefined()
+    expect(scaffold.ctx.agentPresets.serviceFor(agentHandle.agent, 'fs')).toBeUndefined()
     expect(scaffold.ctx.agentPresets.serviceFor(agentHandle.agent, 'compaction')).toBeUndefined()
 
     const stateDir = join(scaffold.workspaceCwd, 'persistent-state')
@@ -106,16 +104,6 @@ describe('minimal agent preset', () => {
       arguments: { command: 'printf \'%s:%s\n\' "$DSH_MINIMAL_STATE" "$PWD"' },
       agent: agentHandle.agent,
     })
-    const seedPath = join(scaffold.workspaceCwd, 'preset-smoke.txt')
-    await writeFile(seedPath, 'MINIMAL_EDITOR_OK\n')
-    const editor = await scaffold.ctx.tools.execute({
-      signal,
-      callId: ToolCallId('minimal-editor-smoke'),
-      name: 'str_replace_editor',
-      arguments: { command: 'view', path: seedPath },
-      agent: agentHandle.agent,
-    })
-
     const text = (result: typeof bash): string => result.content
       .filter(block => block.type === 'text')
       .map(block => block.text)
@@ -128,18 +116,14 @@ describe('minimal agent preset', () => {
       tools: requestHeader.tools?.map(tool => tool.name),
       goalCommand: scaffold.ctx.commands.find(agentHandle.agent, 'goal') !== undefined,
       bash: text(bash),
-      editor: text(editor),
     }).toMatchInlineSnapshot(`
       {
-        "bash": "PERSISTED:{{cwd}}/persistent-state",
-        "editor": "Here's the content of {{cwd}}/preset-smoke.txt with line numbers (which has a total of 2 lines):
-           1  MINIMAL_EDITOR_OK
-           2",
+        "bash": "PERSISTED:{{cwd}}/persistent-state
+      [Command finished with exit code 0]",
         "goalCommand": false,
         "prompt": "You are a helpful software engineer assistant.",
         "tools": [
           "bash",
-          "str_replace_editor",
         ],
       }
     `)
@@ -178,7 +162,7 @@ describe('minimal agent preset', () => {
     const call = row.locator('xpath=..')
     await call.getByText('IN', { exact: true }).waitFor()
     await call.getByText('OUT', { exact: true }).waitFor()
-    await call.getByText('MINIMAL_BASH_CARD_OK', { exact: true }).waitFor()
+    await call.getByText('MINIMAL_BASH_CARD_OK\n[Command finished with exit code 0]', { exact: true }).waitFor()
     await call.getByText(/"command": "printf 'MINIMAL_BASH_CARD_OK/).waitFor()
 
     const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)

+ 2 - 2
benchmarks/agent-continuation/README.i18n.yaml

@@ -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 benchmarks/agent-continuation/README.md
-README.md: 75f804fc18deb90f1db40093cd52e5df10886b46
-README.zh.md: 047c4988beaebe96e0bd561674e5314139608c41
+README.md: cb3d99d749922e08d493f0b58dab04c63356024a
+README.zh.md: 30331aa886d75f8582c6e386cbb022bb487dbe9f

+ 1 - 1
benchmarks/agent-continuation/README.md

@@ -4,7 +4,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-Measure long-history request processing, cold tool-heavy continuation, and repeated discovery of inactive fork children without network services or recorded user data. The SDK variant drives 100 turns and 800 real file reads through the shipped sdk-minimal profile; other cases isolate backend service costs. No case renders a browser.
+Measure long-history request processing, cold tool-heavy continuation, and repeated discovery of inactive fork children without network services or recorded user data. The SDK variant drives 100 turns and 800 real file reads through the shipped sdk-minimal profile with an explicit editor patch; other cases isolate backend service costs. No case renders a browser.
 
 ## Table of Contents
 

+ 1 - 1
benchmarks/agent-continuation/README.zh.md

@@ -4,7 +4,7 @@
 
 ## Summary
 
-在不使用网络服务或录制用户数据的情况下,测量长历史请求处理、冷工具密集续聊和重复发现非活动 fork 子会话。SDK 变体通过已发布 sdk-minimal profile 执行 100 个轮次和 800 次真实文件读取;其他用例隔离后端服务成本。所有用例均不渲染浏览器。
+在不使用网络服务或录制用户数据的情况下,测量长历史请求处理、冷工具密集续聊和重复发现非活动 fork 子会话。SDK 变体通过已发布 sdk-minimal profile 和显式 editor patch 执行 100 个轮次和 800 次真实文件读取;其他用例隔离后端服务成本。所有用例均不渲染浏览器。
 
 ## Table of Contents
 

+ 5 - 2
benchmarks/agent-continuation/profile-continuation.worker.ts

@@ -1,4 +1,4 @@
-/** End-to-end SDK continuation through the built dsh sdk-minimal profile and real file tools. */
+/** End-to-end SDK continuation through built dsh sdk-minimal with an explicitly mounted file editor. */
 
 import { mkdir, writeFile } from 'node:fs/promises'
 import { join } from 'node:path'
@@ -27,7 +27,10 @@ async function run(root: string): Promise<ProfileReport> {
   await writeFile(patch, [
     '- id: llm-deepseek', '  disabled: true',
     '- id: sessions', '  config:', '    root: ' + JSON.stringify(join(root, 'profile-sessions')), '    compression: zstd',
-    '- insert:', '    - id: benchmark-model', '      name: ' + JSON.stringify(join(import.meta.dirname, 'profile-adapter.js')),
+    '- insert:',
+    '    - id: fs-local', "      name: '@deepseek-ai/dsh-fs-local'",
+    '    - id: str-replace-editor', "      name: '@deepseek-ai/dsh-tool-str-replace-editor'",
+    '    - id: benchmark-model', '      name: ' + JSON.stringify(join(import.meta.dirname, 'profile-adapter.js')),
     '',
   ].join('\n'))
   const env: NodeJS.ProcessEnv = {

+ 2 - 2
docs/config-catalog.i18n.yaml

@@ -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/config-catalog.md
-config-catalog.md: 0a06bdf478ccc25b3a3cedb45df18ee0dca285a9
-config-catalog.zh.md: 580c11bd2b3cf6c598206eaa6b1bb0398e85713d
+config-catalog.md: 81281af98001149f88896b490bc261f1c1fc0d26
+config-catalog.zh.md: e0b5156b52f67a0cf99688fe1f61feff5419b2f0

+ 1 - 1
docs/config-catalog.md

@@ -2740,7 +2740,7 @@ export interface Config {
 }
 ```
 
-Source: [`packages/shell/tool-bash-persistent/src/index.ts:432`](../packages/shell/tool-bash-persistent/src/index.ts)
+Source: [`packages/shell/tool-bash-persistent/src/index.ts:435`](../packages/shell/tool-bash-persistent/src/index.ts)
 
 <a id="deepseek-aidsh-tool-fs"></a>
 

+ 1 - 1
docs/config-catalog.zh.md

@@ -2742,7 +2742,7 @@ export interface Config {
 }
 ```
 
-来源:[`packages/shell/tool-bash-persistent/src/index.ts:432`](../packages/shell/tool-bash-persistent/src/index.ts)
+来源:[`packages/shell/tool-bash-persistent/src/index.ts:435`](../packages/shell/tool-bash-persistent/src/index.ts)
 
 <a id="deepseek-aidsh-tool-fs"></a>
 

+ 2 - 2
docs/user/guide/python-sdk.i18n.yaml

@@ -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/user/guide/python-sdk.md
-python-sdk.md: 88cf32f60c285a4ce7c09e424532a6d9b7b00890
-python-sdk.zh.md: e443ec811a3b42034c75162b759408f1acd3486f
+python-sdk.md: 6fde258d9f4f3e8ba1fd41537129ba5b8f02fd58
+python-sdk.zh.md: f50fd6d2bd37e3dc203b519e07d0df4098cab0c7

+ 19 - 3
docs/user/guide/python-sdk.md

@@ -129,19 +129,35 @@ The first command initializes the shipped standalone profile. The second forward
 
 Another `profile` is valid when it includes `@deepseek-ai/dsh-sdk-app` or another JSON-RPC server row. Missing server rows, unresolved plugins, and invalid patches fail during startup instead of falling back to another composition.
 
+<a id="opt-in-to-str_replace_editor"></a>
+### Opt in to `str_replace_editor`
+
+The bundled runtime includes `str_replace_editor`, but `sdk-minimal` omits it from the default Cordis tree. To use it, save this configuration as `editor.patch.yml`; `insert` adds both the editor and the filesystem provider that the minimal profile lacks:
+
+```yaml
+- insert:
+    - id: fs-local
+      name: '@deepseek-ai/dsh-fs-local'
+      config:
+        cwd: !!js process.cwd()
+    - id: tool-str-replace-editor
+      name: '@deepseek-ai/dsh-tool-str-replace-editor'
+```
+
+Pass `patches=("/absolute/path/to/editor.patch.yml",)` when constructing `DeepSeekHarness(profile="sdk-minimal", ...)`, or put the patch in `$DSH_HOME/profiles/sdk-minimal/cordis.patch.yml` for persistent configuration. On the next runtime launch, model requests include `str_replace_editor` beside the persistent shell. The local filesystem provider uses the runtime working directory for relative paths; like the minimal shell, it does not confine access to that directory. For the standard `sdk` profile, insert only the editor row so it uses the existing filesystem provider and policies.
+
 ## Understand the minimal profile
 
 | Property | Value |
 |---|---|
 | System prompt | `DSH_SYSTEM_PROMPT`, falling back to `You are a helpful software engineer assistant.` |
 | Model in `minimal.py` | `--model`, then `DSH_MODEL`, then `deepseek-v4-flash` |
-| Model-facing tools | Persistent `bash` on Linux/macOS or `pwsh` on Windows, plus `str_replace_editor` |
+| Model-facing tool | Persistent `bash` on Linux/macOS or `pwsh` on Windows |
 | Shell timeout | 300 seconds |
-| Editor output limit | 16,000 characters |
 | Runtime context and compaction | Absent |
 | Session persistence | Uncompressed JSONL under `<dsh_home>/sessions` |
 
-The profile's sole bundle inserts the complete tree over an empty root and does not include `dsh-base`; later base-profile tools therefore cannot appear implicitly. It contains the SDK protocol, one environment-configured DeepSeek adapter, local execution, and persistence, while settings, managed credentials, telemetry, Web tools, subagents, local instruction discovery, and compaction are absent. It pins `danger-full-access`, so the platform-selected persistent shell and editor can modify any path visible to the runtime; use a disposable checkout or container.
+The profile's sole bundle inserts the complete tree over an empty root and does not include `dsh-base`; later base-profile tools therefore cannot appear implicitly. It contains the SDK protocol, one environment-configured DeepSeek adapter, local execution, and persistence, while filesystem tools, settings, managed credentials, telemetry, Web tools, subagents, local instruction discovery, and compaction are absent. It pins `danger-full-access`, so the platform-selected persistent shell can modify any path visible to the runtime; use a disposable checkout or container.
 
 The installed wheel still packages the full `web` profile and frontend assets. Run `dsh web` against an explicit `DSH_HOME` when a Python SDK deployment also needs the browser application; `web` is a separate CLI application and cannot serve a Python SDK client.
 

+ 19 - 3
docs/user/guide/python-sdk.zh.md

@@ -129,19 +129,35 @@ dsh plugin --profile sdk-minimal add file:C:/work/my-plugin-bundle
 
 另一个 `profile` 只有包含 `@deepseek-ai/dsh-sdk-app` 或另一个 JSON-RPC server 配置项时才有效。缺失 server 配置项、无法解析的插件和非法 patch 会在启动时失败,不会回退到其他组合。
 
+<a id="opt-in-to-str_replace_editor"></a>
+### 显式启用 `str_replace_editor`
+
+随附运行时包含 `str_replace_editor`,但 `sdk-minimal` 的默认 Cordis tree 不挂载它。要使用该工具,请将以下配置保存为 `editor.patch.yml`;`insert` 会添加 editor,以及极简 profile 缺少的文件系统后端:
+
+```yaml
+- insert:
+    - id: fs-local
+      name: '@deepseek-ai/dsh-fs-local'
+      config:
+        cwd: !!js process.cwd()
+    - id: tool-str-replace-editor
+      name: '@deepseek-ai/dsh-tool-str-replace-editor'
+```
+
+构造 `DeepSeekHarness(profile="sdk-minimal", ...)` 时传入 `patches=("/absolute/path/to/editor.patch.yml",)`,或将 patch 写入 `$DSH_HOME/profiles/sdk-minimal/cordis.patch.yml` 以持久保存配置。下次运行时启动后,模型请求会在持久 shell 之外包含 `str_replace_editor`。本地文件系统后端以运行时工作目录解析相对路径;与极简 shell 一样,它不会将访问限制在该目录内。对于标准 `sdk` profile,只插入 editor 配置项,让它使用已有的文件系统后端与策略。
+
 ## 理解极简 profile
 
 | 属性 | 值 |
 |---|---|
 | 系统提示词 | `DSH_SYSTEM_PROMPT`,未设置时为 `You are a helpful software engineer assistant.` |
 | `minimal.py` 的模型 | `--model`,然后是 `DSH_MODEL`,最后是 `deepseek-v4-flash` |
-| 面向模型的工具 | Linux/macOS 上的持久 `bash` 或 Windows 上的 `pwsh`,以及 `str_replace_editor` |
+| 面向模型的工具 | Linux/macOS 上的持久 `bash` 或 Windows 上的 `pwsh` |
 | Shell 超时 | 300 秒 |
-| Editor 输出上限 | 16,000 字符 |
 | 运行时上下文与 compaction | 不存在 |
 | 会话持久化 | `<dsh_home>/sessions` 下的未压缩 JSONL |
 
-该 profile 的唯一组合包会在空根之上插入完整配置树,且不包含 `dsh-base`,因此基础 profile 以后新增的工具不会隐式出现。它包含 SDK 协议、一个由环境配置的 DeepSeek 适配器、本地执行与持久化;settings、托管凭据、遥测、Web 工具、subagent、本地指令发现和 compaction 均不存在。它固定使用 `danger-full-access`,因此按平台选择的持久 shell 与 editor 可以修改运行时可见的任何路径;应使用一次性 checkout 或容器。
+该 profile 的唯一组合包会在空根之上插入完整配置树,且不包含 `dsh-base`,因此基础 profile 以后新增的工具不会隐式出现。它包含 SDK 协议、一个由环境配置的 DeepSeek 适配器、本地执行与持久化;文件系统工具、settings、托管凭据、遥测、Web 工具、subagent、本地指令发现和 compaction 均不存在。它固定使用 `danger-full-access`,因此按平台选择的持久 shell 可以修改运行时可见的任何路径;应使用一次性 checkout 或容器。
 
 已安装 wheel 仍会打包完整 `web` profile 与前端产物。如果 Python SDK 部署还需要浏览器应用,请针对显式 `DSH_HOME` 运行 `dsh web`;`web` 是独立 CLI 应用,不能为 Python SDK client 提供服务。
 

+ 2 - 2
packages/bundle/base/README.i18n.yaml

@@ -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 packages/bundle/base/README.md
-README.md: 7b78e910b0cb54f0c64efb7816b428fe9f0d131c
-README.zh.md: b25bb58ac031792b0f6ee0b81dfff2713668c5c4
+README.md: 186d3184b344789187e0096b49dd93c191c2fde4
+README.zh.md: fcc42c39c4489afa484844a036a5f85ef72d1e7e

+ 1 - 1
packages/bundle/base/README.md

@@ -49,7 +49,7 @@ Run `dsh --profile my-profile "your task"` and you get a working agent with mode
 
 Out of the box, every profile built on this core provides: a DeepSeek model connection (the provider and model are configurable, and you can enable extra providers from your settings), the full tool set — file editing, shell commands, web search, public HTTP(S) fetch, subagents, task and goal tracking — durable sessions that survive restarts, and the default permission policy that confines file writes to your workspace and asks before risky actions. Web fetch runs without per-call approval; its provider rejects non-public destinations. Feedback stays in the Session log. [OTel session upload](../../session/session-telemetry-otel/README.md) defaults to `FEEDBACK_ONLY` for all users, including `deepseek-official`: new text feedback, message ratings, edits, and withdrawals release the complete canonical prefix through that event, including context. Later records wait for the next explicit feedback; sending an authorized batch needs no further interaction or model call. `DISABLED` prevents OTel capture. The opt-in [DeepSeek session-log contributor](../../session/session-log-deepseek/README.md) remains a separate request path.
 
-Default file editing uses `read`, `write`, and `edit`. The optional `str_replace_editor` tool is selected independently by Web minimal and `sdk-minimal`. To add it to a base-backed profile, put this entry in the profile, home, or invocation patch:
+Default file editing uses `read`, `write`, and `edit`. The `str_replace_editor` tool remains available as an explicit opt-in. To add it to a base-backed profile, put this entry in the profile, home, or invocation patch:
 
 ```yaml
 - insert:

+ 1 - 1
packages/bundle/base/README.zh.md

@@ -49,7 +49,7 @@ kind: "package-bundle"
 
 开箱即用,基于本核心构建的每个 profile 都提供:DeepSeek 模型连接(provider 与模型可配置,你还可以在设置中启用额外 provider)、完整工具集——文件编辑、shell 命令、web 搜索、公开 HTTP(S) 抓取、subagent、任务与目标跟踪——可跨重启存活的持久会话,以及默认权限策略:把文件写入限制在工作区内,危险操作前征询许可。Web 抓取无需逐次审批,其提供方会拒绝非公开目的地址。反馈保存在会话日志中。[OTel 会话上传](../../session/session-telemetry-otel/README.zh.md)对所有用户默认使用 `FEEDBACK_ONLY`,包括 `deepseek-official`:新的文本反馈、消息评分、编辑与撤回会释放截至该事件的完整权威日志前缀,包含上下文。后续记录等待下一次显式反馈;发送已授权批次无需进一步交互或模型调用。`DISABLED` 阻止 OTel 捕获。需主动开启的 [DeepSeek 会话日志贡献器](../../session/session-log-deepseek/README.zh.md)仍是独立的请求路径。
 
-默认文件编辑使用 `read`、`write` 和 `edit`。Web minimal 与 `sdk-minimal` 各自选择可选的 `str_replace_editor` 工具。要将它加入基于 base 的 profile,请在 profile、home 或逐次调用 patch 中添加以下条目:
+默认文件编辑使用 `read`、`write` 和 `edit`。`str_replace_editor` 工具仍可显式启用。要将它加入基于 base 的 profile,请在 profile、home 或逐次调用 patch 中添加以下条目:
 
 ```yaml
 - insert:

+ 2 - 2
packages/bundle/sdk-minimal/README.i18n.yaml

@@ -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 packages/bundle/sdk-minimal/README.md
-README.md: 3be71c7e319f70bddda716c92ade14336db0a065
-README.zh.md: dff652f53d0a4fd6d50c216e564c46f81f4a188b
+README.md: e67b1eb0637f870f47bd97ecff7133242e2f2210
+README.zh.md: 077d3ac0f603ce6781476b4850306d8f8486f82a

+ 5 - 5
packages/bundle/sdk-minimal/README.md

@@ -1,5 +1,5 @@
 ---
-description: "Standalone two-tool SDK profile for users who need a minimal cross-platform coding agent without the shared base bundle."
+description: "Standalone single-tool SDK profile for users who need a minimal cross-platform coding agent without the shared base bundle."
 kind: "package-bundle"
 ---
 
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-Use `dsh --profile sdk-minimal` when an SDK client needs a small, explicit coding-agent runtime. The profile advertises a platform-selected persistent shell and `str_replace_editor`, persists sessions as uncompressed JSONL, and selects the model from the SDK initialization request. It supplies a complete Cordis tree and deliberately excludes `dsh-base`, Web, settings, managed credentials, telemetry, compaction, workspace instructions, skills, jobs, and subagents. Its danger-full-access policy lets the shell and editor modify any path available to the process, so use it only with an isolated workspace.
+Use `dsh --profile sdk-minimal` when an SDK client needs a small, explicit coding-agent runtime. The profile advertises only a platform-selected persistent shell, persists sessions as uncompressed JSONL, and selects the model from the SDK initialization request. It supplies a complete Cordis tree and deliberately excludes `dsh-base`, Web, settings, managed credentials, telemetry, compaction, filesystem tools, workspace instructions, skills, jobs, and subagents. Its danger-full-access policy lets the shell modify any path available to the process, so use it only with an isolated workspace.
 
 ## Table of Contents
 
@@ -46,7 +46,7 @@ The profile mounts exactly one persistent shell stack: Bash on Linux and macOS,
 <details>
 <summary>Implementation internals — click to expand</summary>
 
-The bundle's single insert is the complete application tree: SDK stdio startup and JSON-RPC serving, one environment-configured DeepSeek adapter, the explicit agent core, local subprocess and unrestricted filesystem providers, a platform-selected persistent shell PTY, the string-replace editor, and uncompressed JSONL persistence under `$DSH_HOME/sessions`. It does not inherit another bundle, so every extra row is an explicit profile change.
+The bundle's single insert is the complete application tree: SDK stdio startup and JSON-RPC serving, one environment-configured DeepSeek adapter, the explicit agent core, local subprocess execution, a platform-selected persistent shell PTY, and uncompressed JSONL persistence under `$DSH_HOME/sessions`. It does not inherit another bundle, so every extra row is an explicit profile change.
 
 ### Source map
 
@@ -77,11 +77,11 @@ The bundle's single insert is the complete application tree: SDK stdio startup a
 
 #### What the model sees
 
-The system prompt is `DSH_SYSTEM_PROMPT` or `You are a helpful software engineer assistant.`. The only advertised tools are owner-scoped persistent `bash` on Linux/macOS or `pwsh` on Windows, plus `str_replace_editor`; runtime context, workspace instructions, skills, jobs controls, compaction, and Harness identity are absent.
+The system prompt is `DSH_SYSTEM_PROMPT` or `You are a helpful software engineer assistant.`. The only advertised tool is owner-scoped persistent `bash` on Linux/macOS or `pwsh` on Windows; runtime context, filesystem tools, workspace instructions, skills, jobs controls, compaction, and Harness identity are absent.
 
 #### Token effect
 
-One stable persona plus the two tool schemas. Tool results and ordinary conversation history grow with the session.
+One stable persona plus one tool schema. Tool results and ordinary conversation history grow with the session.
 
 #### KV Cache effect
 

+ 5 - 5
packages/bundle/sdk-minimal/README.zh.md

@@ -1,5 +1,5 @@
 ---
-description: "供需要不含共享 base bundle 的极简跨平台 coding agent 的用户使用的独立双工具 SDK profile。"
+description: "供需要不含共享 base bundle 的极简跨平台 coding agent 的用户使用的独立单工具 SDK profile。"
 kind: "package-bundle"
 ---
 
@@ -9,7 +9,7 @@ kind: "package-bundle"
 
 ## 概述
 
-当 SDK 客户端需要小型、显式的 coding agent 运行时时,请使用 `dsh --profile sdk-minimal`。该 profile 只公布按平台选择的持久 shell 与 `str_replace_editor`,把会话持久化为未压缩 JSONL,并从 SDK 初始化请求选择模型。它提供完整 Cordis 配置树,并刻意排除 `dsh-base`、Web、settings、托管凭据、遥测、compaction、workspace 指令、skills、jobs 与 subagent。其 danger-full-access 策略允许 shell 与编辑器修改进程可访问的任何路径,因此只能配合隔离 workspace 使用。
+当 SDK 客户端需要小型、显式的 coding agent 运行时时,请使用 `dsh --profile sdk-minimal`。该 profile 只公布按平台选择的持久 shell,把会话持久化为未压缩 JSONL,并从 SDK 初始化请求选择模型。它提供完整 Cordis 配置树,并刻意排除 `dsh-base`、Web、settings、托管凭据、遥测、compaction、文件系统工具、workspace 指令、skills、jobs 与 subagent。其 danger-full-access 策略允许 shell 修改进程可访问的任何路径,因此只能配合隔离 workspace 使用。
 
 ## 目录
 
@@ -46,7 +46,7 @@ dsh --profile sdk-minimal
 <details>
 <summary>实现细节——点击展开</summary>
 
-该 bundle 的单个 insert 就是完整应用配置树:SDK stdio 启动与 JSON-RPC 服务、一个由环境配置的 DeepSeek 适配器、显式 agent 核心、本地子进程与不受限文件系统提供方、按平台选择的持久 shell PTY、字符串替换编辑器,以及位于 `$DSH_HOME/sessions` 的未压缩 JSONL 持久化。它不继承其他 bundle,因此每个额外配置项都是显式 profile 变更。
+该 bundle 的单个 insert 就是完整应用配置树:SDK stdio 启动与 JSON-RPC 服务、一个由环境配置的 DeepSeek 适配器、显式 agent 核心、本地子进程执行、按平台选择的持久 shell PTY,以及位于 `$DSH_HOME/sessions` 的未压缩 JSONL 持久化。它不继承其他 bundle,因此每个额外配置项都是显式 profile 变更。
 
 ### 源码地图
 
@@ -77,11 +77,11 @@ dsh --profile sdk-minimal
 
 #### 模型看到的内容
 
-系统提示词取 `DSH_SYSTEM_PROMPT`,未设置时使用 `You are a helpful software engineer assistant.`。对外公布的工具只有 Linux/macOS 上 agent 所有的持久 `bash` 或 Windows 上的 `pwsh`,外加 `str_replace_editor`;运行时上下文、workspace 指令、skills、jobs 控制、compaction 与 Harness 身份均不存在。
+系统提示词取 `DSH_SYSTEM_PROMPT`,未设置时使用 `You are a helpful software engineer assistant.`。对外公布的唯一工具是 Linux/macOS 上 agent 所有的持久 `bash` 或 Windows 上的 `pwsh`;运行时上下文、文件系统工具、workspace 指令、skills、jobs 控制、compaction 与 Harness 身份均不存在。
 
 #### Token 影响
 
-一个稳定 persona 加两个工具 schema。工具结果与普通对话历史随会话增长。
+一个稳定 persona 加一个工具 schema。工具结果与普通对话历史随会话增长。
 
 #### KV Cache 影响
 

+ 1 - 14
packages/bundle/sdk-minimal/cordis.patch.yml

@@ -63,13 +63,6 @@
         shellDialect: pwsh
         timeoutMs: 300000
 
-    # The editor uses the bare local filesystem; persistent Bash still consumes
-    # the shared danger-full-access sandbox policy above.
-    - id: fs-local
-      name: '@deepseek-ai/dsh-fs-local'
-      config:
-        cwd: !!js process.cwd()
-
     # Keep the minimal agent kernel explicit: unlike dsh-base, this profile
     # owns each service row and omits every optional producer it does not use.
     - id: timer
@@ -135,8 +128,7 @@
         description: |-
           Run commands in a bash shell
           * When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped.
-          * You don't have access to the internet via this tool.
-          * You do have access to a mirror of common linux and python packages via apt and pip.
+          * Network access depends on the task environment. Prefer configured mirrors/proxies when they are available.
           * State is persistent across command calls and discussions with the user.
           * To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.
           * Please avoid commands that may produce a very large amount of output.
@@ -156,11 +148,6 @@
           * Please avoid commands that may produce a very large amount of output.
           * Please run long lived commands in the background, e.g. 'Start-Job' or start a server with Start-Process.
 
-    - id: str-replace-editor
-      name: '@deepseek-ai/dsh-tool-str-replace-editor'
-      config:
-        maxOutputChars: 16000
-
     - id: sessions
       name: '@deepseek-ai/dsh-session-persistence-jsonl'
       config:

+ 1 - 3
packages/bundle/sdk-minimal/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@deepseek-ai/dsh-sdk-minimal",
-  "description": "The standalone minimal SDK profile bundle: JSON-RPC, one DeepSeek adapter, persistent shell, editor, and JSONL sessions",
+  "description": "The standalone minimal SDK profile bundle: JSON-RPC, one DeepSeek adapter, persistent shell, and JSONL sessions",
   "version": "0.1.5-alpha.1",
   "publishConfig": {
     "access": "public"
@@ -38,7 +38,6 @@
     "@deepseek-ai/dsh-agent": "workspace:^",
     "@deepseek-ai/dsh-agent-loop": "workspace:^",
     "@deepseek-ai/dsh-deepseek-llm-api-extensions": "workspace:^",
-    "@deepseek-ai/dsh-fs-local": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
     "@deepseek-ai/dsh-jobs-local": "workspace:^",
     "@deepseek-ai/dsh-llm": "workspace:^",
@@ -61,7 +60,6 @@
     "@deepseek-ai/dsh-terminal-bash": "workspace:^",
     "@deepseek-ai/dsh-tool-bash-persistent": "workspace:^",
     "@deepseek-ai/dsh-tool-pwsh-persistent": "workspace:^",
-    "@deepseek-ai/dsh-tool-str-replace-editor": "workspace:^",
     "@deepseek-ai/dsh-tools": "workspace:^"
   },
   "peerDependencies": {

+ 0 - 2
packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts

@@ -39,7 +39,6 @@ describe('dsh-sdk-minimal bundle', () => {
       ['pty', '@deepseek-ai/dsh-terminal'],
       ['terminal-bash', '@deepseek-ai/dsh-terminal-bash'],
       ['terminal-pwsh', '@deepseek-ai/dsh-terminal-bash'],
-      ['fs-local', '@deepseek-ai/dsh-fs-local'],
       ['timer', '@deepseek-ai/cordis-plugin-timer'],
       ['llm', '@deepseek-ai/dsh-llm'],
       ['session', '@deepseek-ai/dsh-session'],
@@ -57,7 +56,6 @@ describe('dsh-sdk-minimal bundle', () => {
       ['agent-loop', '@deepseek-ai/dsh-agent-loop'],
       ['persistent-bash', '@deepseek-ai/dsh-tool-bash-persistent'],
       ['persistent-pwsh', '@deepseek-ai/dsh-tool-pwsh-persistent'],
-      ['str-replace-editor', '@deepseek-ai/dsh-tool-str-replace-editor'],
       ['sessions', '@deepseek-ai/dsh-session-persistence-jsonl'],
     ])
     expect(rows.find(row => row.id === 'sdk-app-startup')?.config).toEqual({ profile: 'sdk-minimal' })

+ 2 - 2
packages/client/ui-agent-preset/src/client/locales.ts

@@ -39,7 +39,7 @@ export const en: Record<AgentPresetSettingsKey, string> = {
     'Full coding agent without the workflow tool; other tools are exposed through the PTC mode SDK so the model can combine multi-step operations in one TypeScript program.',
   presetMinimalName: 'Minimal mode',
   presetMinimalDescription:
-    'Two-tool coding agent with persistent bash and str_replace_editor.',
+    'Single-tool coding agent with a persistent shell.',
   presetCordisName: 'Creator mode',
   presetCordisDescription:
     'Built for creating custom agent presets, with all Standard mode capabilities plus runtime inspection, plugin experiments, and preset-authoring guidance.',
@@ -98,7 +98,7 @@ export const zh: Record<AgentPresetSettingsKey, string> = {
   presetPtcName: 'PTC 模式',
   presetPtcDescription: '功能完整的编码 Agent,但默认不提供 workflow 工具;其他工具通过 PTC 模式 SDK 呈现,让模型用一个 TypeScript 程序组合多步操作。',
   presetMinimalName: '极简模式',
-  presetMinimalDescription: '仅提供持久 bash 与 str_replace_editor 的双工具编码 Agent。',
+  presetMinimalDescription: '仅提供持久 shell 的单工具编码 Agent。',
   presetCordisName: '创造模式',
   presetCordisDescription: '用于创建自定义 Agent preset:具备标准模式的全部能力,并提供运行时检查、插件实验和 preset 创作指导。',
   duplicate: '复制',

+ 4 - 7
packages/client/ui-user-questions/src/index.ts

@@ -1,13 +1,10 @@
 /**
  * Web question plugin, node half.
  *
- * Deliberately empty. Mounting `ask_user_question` here put it in the tools
- * registry's GLOBAL layer, so every agent saw it no matter which preset
- * composed it — a two-tool benchmark preset actually presented three, and a
- * locally authored `bash-only` preset presented two. Rendering a question is
- * a host UI capability; having the tool is an agent capability, and only a
- * preset decides that. The `tool-ask-user` row belongs in the presets that
- * want it (and in the TUI composition, which has no presets).
+ * Mounting `ask_user_question` in the tools registry's global layer expands
+ * every agent's tool list regardless of its preset. Rendering a question is
+ * a host UI capability; the model-facing tool belongs to the presets that
+ * include it and to the TUI composition, which has no presets.
  */
 
 /** Host plugin body — the model-facing tool is composed per preset, not here. */

+ 1 - 5
packages/client/ui-user-questions/tests/node-plugin.client.spec.ts

@@ -21,11 +21,7 @@ describe('ui-user-questions node plugin', () => {
 
     await ctx.plugin({ apply }).await()
 
-    // Selecting the Web question FEATURE must not hand every agent the tool.
-    // `ctx.tools.register` on an unscoped host context files into the global
-    // layer, which merges into every agent's view regardless of the preset
-    // that composed it — so a two-tool benchmark preset would really present
-    // three. The `tool-ask-user` row belongs to the presets that want it.
+    // Host UI registration must leave each preset's model-facing tool list intact.
     expect(ctx.tools.get('ask_user_question')).toBeUndefined()
   })
 })

+ 6 - 2
packages/lsp/lsp-stdio/tests/lifecycle.spec.ts

@@ -304,6 +304,7 @@ describe('lsp-stdio end to end over a fake server', () => {
       LSP_FAKE_DEF: 'null',
       LSP_FAKE_OPEN_MARKER: marker,
     }, {}, (registered) => { provider = registered })
+    const firstReply = Promise.withResolvers<undefined>()
     const release = Promise.withResolvers<undefined>()
     const request = Object.getOwnPropertyDescriptor(LspConnection.prototype, 'request')?.value as LspConnection['request']
     let holdFirst = true
@@ -311,14 +312,17 @@ describe('lsp-stdio end to end over a fake server', () => {
       const hold = method === 'textDocument/definition' && holdFirst
       if (hold) holdFirst = false
       const result = await request.call(this, method, params)
-      if (hold) await release.promise
+      if (hold) {
+        firstReply.resolve(undefined)
+        await release.promise
+      }
       return result
     })
     const pending: Promise<unknown>[] = []
     try {
       const first = ctx.lsp.query(query('goToDefinition'))
       pending.push(Promise.allSettled([first]))
-      await waitFor(async () => (await markerLines(marker)).length === 1)
+      await Promise.race([firstReply.promise, first])
       // The changed tail proves the second query entered the provider queue
       // while the first response is held, before the source rewrite starts.
       const queues = (provider as unknown as { queues: ReadonlyMap<unknown, Promise<void>> }).queues

+ 5 - 24
packages/preset/agent-presets/presets/minimal/agent.cordis.yml

@@ -1,10 +1,10 @@
-# The `minimal` agent preset: a fixed-prompt, two-tool coding-agent composition.
+# The `minimal` agent preset: a fixed-prompt, single-tool coding-agent composition.
 #
 # The persona is the complete system prompt, so global identity, Web orientation,
 # tool guidance, and later assembly listeners cannot add prompt text. Runtime
-# context snapshots are suppressed for this preset, and the model composes only
-# the persistent shell (`bash` on POSIX, `pwsh` on win32) and
-# `str_replace_editor`. Context compaction is absent.
+# context snapshots are suppressed for this preset, and the model receives only
+# the persistent shell (`bash` on POSIX, `pwsh` on win32). Context compaction is
+# absent.
 
 - id: persona
   name: '@deepseek-ai/dsh-persona'
@@ -41,8 +41,7 @@
         description: |-
           Run commands in a bash shell
           * When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped.
-          * You don't have access to the internet via this tool.
-          * You do have access to a mirror of common linux and python packages via apt and pip.
+          * Network access depends on the task environment. Prefer configured mirrors/proxies when they are available.
           * State is persistent across command calls and discussions with the user.
           * To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.
           * Please avoid commands that may produce a very large amount of output.
@@ -68,21 +67,3 @@
           * Use native Windows paths (C:\...) and $env:NAME variables; this is PowerShell, not bash.
           * Please avoid commands that may produce a very large amount of output.
           * Please run long lived commands in the background, e.g. 'Start-Job' or start a server with Start-Process.
-
-# The bare local filesystem shadows the host's sandboxed provider only for this
-# preset. The editor shares that realm and requires absolute paths.
-- id: filesystem
-  name: cordis:group
-  group: true
-  isolate:
-    fs: true
-  config:
-    - id: fs-local
-      name: '@deepseek-ai/dsh-fs-local'
-      config:
-        cwd: !!js process.env.DSH_CWD ?? process.cwd()
-
-    - id: str-replace-editor
-      name: '@deepseek-ai/dsh-tool-str-replace-editor'
-      config:
-        maxOutputChars: 16000

+ 1 - 1
packages/preset/agent-presets/presets/minimal/preset.yml

@@ -1,3 +1,3 @@
 name: 极简模式
-description: 仅提供持久 bash 与 str_replace_editor 的双工具编码 Agent。
+description: 仅提供持久 shell 的单工具编码 Agent。
 order: 3

+ 35 - 6
packages/session/session-persistence-jsonl/tests/built-migration-worker.e2e.ts

@@ -9,11 +9,12 @@ const built = ['lib/index.js', 'lib/worker.cjs']
   .every(path => existsSync(join(packageRoot, path)))
 
 describe.skipIf(!built)('built migration verifier (plain node)', () => {
-  it('publishes a historical generation through the bundled worker', async () => {
+  it('publishes history and verifies it without runtime workspace imports', async () => {
     const script = `
-      import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
+      import { copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
       import { tmpdir } from 'node:os'
       import { join } from 'node:path'
+      import { Worker } from 'node:worker_threads'
       import { Context } from '@deepseek-ai/cordis'
       import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
 
@@ -30,8 +31,30 @@ describe.skipIf(!built)('built migration verifier (plain node)', () => {
         const handle = await ctx.sessionPersistence.open(id, 'write')
         await handle.close()
         await ctx.sessionPersistence.flush()
-        const header = JSON.parse((await readFile(join(directory, 'session.v3.jsonl'), 'utf8')).trim())
-        console.log(JSON.stringify({ id: header.id, version: header.version }))
+        const currentPath = join(directory, 'session.v3.jsonl')
+        const header = JSON.parse((await readFile(currentPath, 'utf8')).trim())
+        await mkdir(join(root, 'lib'))
+        await copyFile('package.json', join(root, 'package.json'))
+        const workerPath = join(root, 'lib', 'worker.cjs')
+        await copyFile('lib/worker.cjs', workerPath)
+        async function verify(expectedEventCount) {
+          const worker = new Worker(workerPath, { workerData: {
+            path: currentPath, compression: 'none', expectedId: id, expectedEventCount,
+          } })
+          try {
+            return await new Promise((resolve, reject) => {
+              worker.once('message', resolve)
+              worker.once('error', reject)
+              worker.once('exit', code => reject(new Error('verifier exited before a result: ' + code)))
+            })
+          } finally {
+            await worker.terminate()
+          }
+        }
+        const verified = await verify(0)
+        const refused = await verify(1)
+        console.log(JSON.stringify({ id: header.id, version: header.version,
+          verified: verified.ok, refused: refused.ok, refusal: refused.message }))
       } finally {
         await ctx.fiber.dispose()
         await rm(root, { recursive: true, force: true })
@@ -40,10 +63,16 @@ describe.skipIf(!built)('built migration verifier (plain node)', () => {
     const { exitCode, stdout, stderr } = await execa(
       process.execPath,
       ['--input-type=module', '-e', script],
-      { cwd: packageRoot, stdin: 'ignore', timeout: 30_000, killSignal: 'SIGKILL', reject: false },
+      {
+        cwd: packageRoot, env: { NODE_PATH: undefined, NODE_OPTIONS: undefined },
+        stdin: 'ignore', timeout: 30_000, killSignal: 'SIGKILL', reject: false,
+      },
     )
 
     expect(exitCode, `stderr:\n${stderr}`).toBe(0)
-    expect(JSON.parse(stdout.trim())).toEqual({ id: 'built-migration-worker', version: 3 })
+    expect(JSON.parse(stdout.trim())).toEqual({
+      id: 'built-migration-worker', version: 3, verified: true, refused: false,
+      refusal: 'current session generation contains 0 events, expected 1',
+    })
   })
 })

+ 4 - 1
packages/session/session-persistence-jsonl/tsdown.config.ts

@@ -1,7 +1,7 @@
 import { defineConfig } from 'tsdown'
 
 /** Build the backend and its path-loaded verifier as separate bundles. */
-export default defineConfig([
+export default defineConfig(({ env }) => env?.DSH_BUILD_FACE === 'client' ? [] : [
   {
     entry: ['lib/types/index.js'],
     outDir: 'lib',
@@ -14,6 +14,9 @@ export default defineConfig([
   },
   {
     entry: ['lib/types/worker.js'],
+    // Fresh verifiers need no host singleton identity. Inline their JavaScript
+    // closure to avoid resolving and compiling workspace packages on every open.
+    deps: { alwaysBundle: [/^@deepseek-ai\/(?!node-addon-system)/] },
     outDir: 'lib',
     format: ['cjs'],
     platform: 'node',

+ 2 - 2
packages/shell/tool-bash-persistent/README.i18n.yaml

@@ -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 packages/shell/tool-bash-persistent/README.md
-README.md: f03e51c4bff94ae288d20827615f32782568014c
-README.zh.md: cd9f859b38009dd7aaa7842b78b103ab67c545aa
+README.md: e2b1e16cb38a9e11d50033de73e10ecfff7b0ae2
+README.zh.md: d478831b4e432df916492d8e60a6c9fa8d788b42

+ 2 - 2
packages/shell/tool-bash-persistent/README.md

@@ -52,7 +52,7 @@ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-a
 
 ### What the agent can rely on
 
-Commands share one shell per agent, so state persists until an `exit`, a timeout, or a reset — each of which closes the shell and tells the agent the next call starts from the workspace with a fresh directory and environment. Results exclude the private completion markers; a non-zero wrapped command appends `[exit code: N]`, and a shell that exits before reporting that status instead appends `[shell exited: code N]`, `[shell killed by signal: SIG]`, or `[shell exited]`, then resets. Long output keeps the earliest retained prefix plus a clipping notice; if the terminal has already dropped that prefix, the result says so explicitly rather than presenting a tail as complete output.
+Commands share one shell per agent, so state persists until an `exit`, a timeout, or a reset — each of which closes the shell and tells the agent the next call starts from the workspace with a fresh directory and environment. Results exclude the private completion markers; every settled command appends `[Command finished with exit code N]`, and a shell that exits before reporting that status instead appends `[shell exited: code N]`, `[shell killed by signal: SIG]`, or `[shell exited]`, then resets. Long output keeps the earliest retained prefix plus a clipping notice; if the terminal has already dropped that prefix, the result says so explicitly rather than presenting a tail as complete output.
 
 ### What can go wrong
 
@@ -126,7 +126,7 @@ Prefix-stable while the configured description and schema remain unchanged.
 
 #### What the model sees
 
-Commands share one shell per Agent, so cwd, exported variables, activated environments, functions, and background jobs persist across calls. Results exclude private completion markers. When the shell reads stdin again without having printed the completion marker — after `exec`, an interrupt, or an interactive foreground child whose stdin wait the provider proves — the call returns the captured partial output, which can end with the backend's own prompt text. A nonzero wrapped command appends `[exit code: N]`; a shell that exits before reporting that status instead appends `[shell exited: code N]`, `[shell killed by signal: SIG]`, or `[shell exited]` when the backend supplies neither, then resets and tells the model that the next call starts fresh. Long output keeps the earliest retained prefix plus a clipping notice. If the PTY has already dropped that prefix, the result says so explicitly instead of presenting a tail as complete output. Timeout returns bounded partial output, closes the uncertain shell, and reports the reset.
+Commands share one shell per Agent, so cwd, exported variables, activated environments, functions, and background jobs persist across calls. Results exclude private completion markers. When the shell reads stdin again without having printed the completion marker — after `exec`, an interrupt, or an interactive foreground child whose stdin wait the provider proves — the call returns the captured partial output, which can end with the backend's own prompt text. Every settled command appends `[Command finished with exit code N]`; a shell that exits before reporting that status instead appends `[shell exited: code N]`, `[shell killed by signal: SIG]`, or `[shell exited]` when the backend supplies neither, then resets and tells the model that the next call starts fresh. Long output keeps the earliest retained prefix plus a clipping notice. If the PTY has already dropped that prefix, the result says so explicitly instead of presenting a tail as complete output. Timeout returns bounded partial output followed by `[Command timed out or OOM]`, closes the uncertain shell, and reports the reset.
 
 #### Token effect
 

+ 2 - 2
packages/shell/tool-bash-persistent/README.zh.md

@@ -52,7 +52,7 @@ kind: "package-reference"
 
 ### agent 可以依赖什么
 
-命令共享每个 agent 一个 shell,因此状态一直保留到 `exit`、超时或重置——每一种都会关闭 shell 并告诉 agent 下一次调用从工作区的新目录与环境开始。结果排除私有完成标记;非零的包装命令追加 `[exit code: N]`,而在报告该状态前就退出的 shell 改为追加 `[shell exited: code N]`、`[shell killed by signal: SIG]` 或 `[shell exited]`,然后重置。长输出保留最早的已保留前缀并附裁剪通知;若 terminal 已经丢弃该前缀,结果会明确说明,而不是把尾部当作完整输出呈现。
+命令共享每个 agent 一个 shell,因此状态一直保留到 `exit`、超时或重置——每一种都会关闭 shell 并告诉 agent 下一次调用从工作区的新目录与环境开始。结果排除私有完成标记;每条完成的命令都追加 `[Command finished with exit code N]`,而在报告该状态前就退出的 shell 改为追加 `[shell exited: code N]`、`[shell killed by signal: SIG]` 或 `[shell exited]`,然后重置。长输出保留最早的已保留前缀并附裁剪通知;若 terminal 已经丢弃该前缀,结果会明确说明,而不是把尾部当作完整输出呈现。
 
 ### 可能出什么问题
 
@@ -126,7 +126,7 @@ kind: "package-reference"
 
 #### 模型看到什么
 
-命令共享每个 Agent 一个 shell,因此 cwd、导出的变量、已激活的环境、函数与后台任务都会跨调用保留。结果排除私有完成标记。当 shell 在没有打印完成标记的情况下再次读取 stdin——`exec`、中断,或提供方证明其 stdin 等待的交互式前台子进程之后——调用返回捕获的部分输出,它可能以后端自己的提示词文本结尾。非零的包装命令追加 `[exit code: N]`;在报告该状态前就退出的 shell 改为追加 `[shell exited: code N]`、`[shell killed by signal: SIG]`,或后端两者都未提供时的 `[shell exited]`,然后重置并告诉模型下一次调用从全新状态开始。长输出保留最早的已保留前缀并附裁剪通知。若 PTY 已经丢弃该前缀,结果会明确说明,而不是把尾部当作完整输出呈现。超时返回有界部分输出、关闭不确定的 shell 并报告重置。
+命令共享每个 Agent 一个 shell,因此 cwd、导出的变量、已激活的环境、函数与后台任务都会跨调用保留。结果排除私有完成标记。当 shell 在没有打印完成标记的情况下再次读取 stdin——`exec`、中断,或提供方证明其 stdin 等待的交互式前台子进程之后——调用返回捕获的部分输出,它可能以后端自己的提示词文本结尾。每条完成的命令都追加 `[Command finished with exit code N]`;在报告该状态前就退出的 shell 改为追加 `[shell exited: code N]`、`[shell killed by signal: SIG]`,或后端两者都未提供时的 `[shell exited]`,然后重置并告诉模型下一次调用从全新状态开始。长输出保留最早的已保留前缀并附裁剪通知。若 PTY 已经丢弃该前缀,结果会明确说明,而不是把尾部当作完整输出呈现。超时返回有界部分输出并追加 `[Command timed out or OOM]`、关闭不确定的 shell 并报告重置。
 
 #### Token 影响
 

+ 7 - 4
packages/shell/tool-bash-persistent/src/index.ts

@@ -15,6 +15,9 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
 const TRUNCATED_MESSAGE = '<response clipped><NOTE>To save on context only part of this file has been shown to you. You should retry this tool after you have searched inside the file with `grep -n` in order to find the line numbers of what you are looking for.</NOTE>'
 const LOST_PREFIX_MESSAGE = '<response clipped><NOTE>The beginning of this command output was dropped by the terminal scrollback limit. The following text is the earliest retained output.</NOTE>\n'
 const SHELL_RESET_MESSAGE = 'The persistent bash shell was reset; the next bash call starts from the workspace with a fresh current directory and environment.'
+// Status trailer for a command that never reported an exit code; settled
+// commands append `[Command finished with exit code N]` instead (see renderCaptured).
+const TIMEOUT_STATUS_MARKER = '[Command timed out or OOM]'
 const TIMEOUT_CODE = 'PERSISTENT_BASH_TIMEOUT'
 // One page is enough to find a just-emitted completion marker; the full
 // scrollback is assembled only when a command settles or needs partial output.
@@ -82,7 +85,7 @@ function wrapCommand(command: string, marker: CommandMarkers): string {
 }
 
 function trimTrailingNewline(text: string): string {
-  return text.replace(/\r?\n$/, '')
+  return text.replace(/(?:\r?\n)+$/, '')
 }
 
 function commandOutput(
@@ -162,8 +165,8 @@ function renderCaptured(output: CapturedOutput, maxOutputChars: number): string
   const withPrefix = output.incomplete && output.text.length > 0
     ? LOST_PREFIX_MESSAGE + rendered
     : rendered
-  const marker = output.exitCode !== undefined && output.exitCode !== 0
-    ? `[exit code: ${output.exitCode}]`
+  const marker = output.exitCode !== undefined
+    ? `[Command finished with exit code ${output.exitCode}]`
     : undefined
   return appendStatusMarker(withPrefix, marker)
 }
@@ -345,7 +348,7 @@ async function executeCommand(
       return [
         // TODO: Report a timeout only; this signal does not establish an OOM.
         `Your command timed out after ${Math.round(timedOut.timeoutMs / 1000)} seconds or experienced an OOM error. Below is partial output:`,
-        partial,
+        appendStatusMarker(partial, TIMEOUT_STATUS_MARKER),
         SHELL_RESET_MESSAGE,
       ].join('\n')
     }

+ 10 - 4
packages/shell/tool-bash-persistent/tests/loader-composition.spec.ts

@@ -149,20 +149,26 @@ suite('persistent Bash through a real cordis.yml Loader composition', () => {
       'multiline',
       'value="line one"\nprintf "%s:%s\\n" "$value" "it\'s fine"',
     ))
-    expect(multiline).toBe("line one:it's fine")
+    expect(multiline).toBe("line one:it's fine\n[Command finished with exit code 0]")
     expect(multiline).not.toContain('DSH_PERSISTENT_BASH')
 
     const heredoc = text(await execute(
       'heredoc',
       "cat <<'EOF'\nalpha\nbeta\nEOF",
     ))
-    expect(heredoc).toBe('alpha\nbeta')
+    expect(heredoc).toBe('alpha\nbeta\n[Command finished with exit code 0]')
 
     const pipeline = text(await execute(
       'pipeline',
       '{ sleep 0.1; printf "delayed\\n"; } | cat',
     ))
-    expect(pipeline).toBe('delayed')
+    expect(pipeline).toBe('delayed\n[Command finished with exit code 0]')
+
+    // Every trailing newline is dropped before the status trailer.
+    const trailing = text(await execute('trailing-newlines', 'printf "tail\\n\\n\\n"'))
+    expect(trailing).toBe('tail\n[Command finished with exit code 0]')
+    expect(text(await execute('nonzero', 'exit_code() { return 3; }; exit_code')))
+      .toBe('[Command finished with exit code 3]')
 
     const large = text(await execute('large-output', 'seq 1 12050'))
     expect(large.startsWith('1\n2\n3\n')).toBe(true)
@@ -177,6 +183,6 @@ suite('persistent Bash through a real cordis.yml Loader composition', () => {
 
     const exited = text(await execute('exit', 'exit'))
     expect(exited).toContain('next bash call starts from the workspace')
-    expect(text(await execute('after-exit', 'printf "%s\\n" "$PWD"'))).toBe(root)
+    expect(text(await execute('after-exit', 'printf "%s\\n" "$PWD"'))).toBe(`${root}\n[Command finished with exit code 0]`)
   }, 20_000)
 })

+ 12 - 11
packages/shell/tool-bash-persistent/tests/tools.spec.ts

@@ -328,13 +328,13 @@ describe('tool-bash-persistent', () => {
     expect(ctx.tools.get('bash')?.presentCall?.({ command: 'pwd' }))
       .toEqual({ card: 'terminal', title: 'pwd' })
 
-    expect(text(await call(ctx, owner, 'echo one'))).toBe('hello from stub')
-    expect(text(await call(ctx, owner, 'echo two'))).toBe('hello from stub')
+    expect(text(await call(ctx, owner, 'echo one'))).toBe('hello from stub\n[Command finished with exit code 0]')
+    expect(text(await call(ctx, owner, 'echo two'))).toBe('hello from stub\n[Command finished with exit code 0]')
     expect(stub.sessions).toHaveLength(1)
     expect(stub.sessions[0]?.sends).toBe(3)
 
     const ownerWithoutCwd = agent(ctx, undefined)
-    expect(text(await call(ctx, ownerWithoutCwd, 'pwd'))).toBe('hello from stub')
+    expect(text(await call(ctx, ownerWithoutCwd, 'pwd'))).toBe('hello from stub\n[Command finished with exit code 0]')
     expect(stub.sessions).toHaveLength(2)
 
     await fiber.dispose()
@@ -377,13 +377,13 @@ describe('tool-bash-persistent', () => {
     expect(text(await call(ctx, owner, 'large'))).toContain('<response clipped>')
 
     session.mode = 'nonzero'
-    expect(text(await call(ctx, owner, 'false'))).toBe('[exit code: 7]')
+    expect(text(await call(ctx, owner, 'false'))).toBe('[Command finished with exit code 7]')
 
     session.mode = 'exit'
     const exited = text(await call(ctx, owner, 'exit'))
     expect(exited).toContain('hello from')
     expect(exited).toContain('[shell exited: code 9]')
-    expect(exited).not.toContain('[exit code: 9]')
+    expect(exited).not.toContain('[Command finished with exit code 9]')
     expect(exited).toContain('next bash call starts from the workspace')
     expect(session.closed).toContain('persistent bash shell exited')
 
@@ -409,7 +409,7 @@ describe('tool-bash-persistent', () => {
     stub.sessions[0]!.mode = 'torn-status'
     stub.sessions[0]!.scrollback = ''
 
-    expect(text(await call(ctx, owner, 'torn status'))).toBe('hello from stub\n[exit code: 7]')
+    expect(text(await call(ctx, owner, 'torn status'))).toBe('hello from stub\n[Command finished with exit code 7]')
   })
 
   it('reports the exit path when the shell exits between send settlement and the next poll', async () => {
@@ -423,7 +423,7 @@ describe('tool-bash-persistent', () => {
     expect(result).toContain('next bash call starts from the workspace')
     expect(session.closed).toContain('persistent bash shell exited')
 
-    expect(text(await call(ctx, owner, 'echo "$PWD"'))).toBe('hello from stub')
+    expect(text(await call(ctx, owner, 'echo "$PWD"'))).toBe('hello from stub\n[Command finished with exit code 0]')
     expect(stub.sessions).toHaveLength(2)
   })
 
@@ -462,7 +462,7 @@ describe('tool-bash-persistent', () => {
     session.mode = 'paged-scrollback'
     session.scrollback = 'older one\nolder two\nolder three\nolder four\n'
 
-    expect(text(await call(ctx, owner, 'paged output'))).toBe('hello from stub')
+    expect(text(await call(ctx, owner, 'paged output'))).toBe('hello from stub\n[Command finished with exit code 0]')
   })
 
   it('returns a stdin_read fallback reached after multiple polling rounds', async () => {
@@ -484,7 +484,7 @@ describe('tool-bash-persistent', () => {
     await call(ctx, owner, 'warm up')
     stub.sessions[0]!.historyTruncated = true
     const result = text(await call(ctx, owner, 'short command'))
-    expect(result).toBe('hello from stub')
+    expect(result).toBe('hello from stub\n[Command finished with exit code 0]')
     expect(result).not.toContain('<response clipped>')
     expect(result).not.toContain('beginning of this command output was dropped')
   })
@@ -496,6 +496,7 @@ describe('tool-bash-persistent', () => {
     const result = await call(ctx, owner, 'hang')
     expect(text(result)).toContain('timed out after 0 seconds or experienced an OOM error')
     expect(text(result)).toContain('partial output')
+    expect(text(result)).toContain('[Command timed out or OOM]')
     expect(text(result)).toContain('next bash call starts from the workspace')
     expect(stub.sessions[0]?.closed).toContain('persistent bash command timed out')
   })
@@ -514,7 +515,7 @@ describe('tool-bash-persistent', () => {
       }, 5)
 
       expect((await cancelled).isError).toBe(true)
-      expect(text(await queued)).toBe('hello from stub')
+      expect(text(await queued)).toBe('hello from stub\n[Command finished with exit code 0]')
       expect(stub.sessions[0]?.closed).toContain('persistent bash command aborted')
       expect(stub.sessions).toHaveLength(2)
     },
@@ -541,7 +542,7 @@ describe('tool-bash-persistent', () => {
     stub.sessions[0]!.mode = 'send-error'
     expect((await call(ctx, owner, 'fails')).isError).toBe(true)
     expect(stub.sessions[0]?.closed).toContain('persistent bash send failed')
-    expect(text(await call(ctx, owner, 'recovers'))).toBe('hello from stub')
+    expect(text(await call(ctx, owner, 'recovers'))).toBe('hello from stub\n[Command finished with exit code 0]')
     expect(stub.sessions).toHaveLength(2)
   })
 

+ 0 - 6
pnpm-lock.yaml

@@ -1668,9 +1668,6 @@ importers:
       '@deepseek-ai/dsh-deepseek-llm-api-extensions':
         specifier: workspace:^
         version: link:../../llm/deepseek-llm-api-extensions
-      '@deepseek-ai/dsh-fs-local':
-        specifier: workspace:^
-        version: link:../../fs/fs-local
       '@deepseek-ai/dsh-invariants':
         specifier: workspace:^
         version: link:../../runtime-diagnostics/invariants
@@ -1737,9 +1734,6 @@ importers:
       '@deepseek-ai/dsh-tool-pwsh-persistent':
         specifier: workspace:^
         version: link:../../shell/tool-pwsh-persistent
-      '@deepseek-ai/dsh-tool-str-replace-editor':
-        specifier: workspace:^
-        version: link:../../fs/tool-str-replace-editor
       '@deepseek-ai/dsh-tools':
         specifier: workspace:^
         version: link:../../core/tools

+ 2 - 2
python/sdk/README.i18n.yaml

@@ -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 python/sdk/README.md
-README.md: 77ae34e24f5aec0c767cbe6c367d6d44eefbbfef
-README.zh.md: 41b343895aa3d03174098ad5f856cc65197f063b
+README.md: bfca3b0dda6c97bbef255815f33ca81121832f4e
+README.zh.md: 3824b6d35bcaa0bdec92584c53ce7036130d85c0

+ 1 - 1
python/sdk/README.md

@@ -59,7 +59,7 @@ with DeepSeekHarness(
 
 `provider` selects a provider route registered by the chosen Cordis composition; `model` is the model id resolved by that adapter. `reasoning_effort` is an optional non-empty adapter-owned identifier for that exact route; omission preserves the model's own default. `max_tokens` is an optional positive per-request output-token cap for the root agent and its in-process descendants; omission leaves the provider default in control. Initialization rejects a missing adapter, unavailable model, or unsupported effort before a prompt runs. Compaction summaries keep the separate limit configured by their compaction plugin. The bundled default composition registers `deepseek-official`. A custom composition can mount `llm-pi-ai`, configure provider-specific credentials/endpoints there, and select any provider/model present in pi-ai's installed catalog.
 
-The shipped `sdk-minimal` profile is a standalone explicit tree rather than an overlay on `dsh-base`. Select it with `profile="sdk-minimal"`; the ordinary `model` argument is the sole runtime model selection, including for model ids outside the adapter's advisory catalog. It provides persistent Bash, the string-replace editor, local execution, and JSONL sessions; settings, managed credentials, telemetry, Web tools, and the full default tool roster remain available through the separate full `sdk` and `web` profiles.
+The shipped `sdk-minimal` profile is a standalone explicit tree rather than an overlay on `dsh-base`. Select it with `profile="sdk-minimal"`; the ordinary `model` argument is the sole runtime model selection, including for model ids outside the adapter's advisory catalog. It provides only a platform-selected persistent shell, local execution, and JSONL sessions; filesystem tools, settings, managed credentials, telemetry, Web tools, and the full default tool roster remain available through the separate full `sdk` and `web` profiles.
 
 ## Results and notifications
 

+ 1 - 1
python/sdk/README.zh.md

@@ -59,7 +59,7 @@ with DeepSeekHarness(
 
 `provider` 选择指定 Cordis 组合所注册的提供方路由;`model` 是该适配器解析出的模型 ID。`reasoning_effort` 是该确切路由可选的非空适配器自有标识符;省略时保留模型自身的默认值。`max_tokens` 是一个可选的正整数,用于限制根 agent 及其进程内后代在每次请求中输出的 token 数量;省略该参数时,由提供方的默认行为决定输出上限。缺少适配器、模型不可用或推理强度不受支持时,初始化会在提示词运行前拒绝。压缩摘要继续使用压缩插件单独配置的上限。内置默认组合注册 `deepseek-official`。自定义组合可以挂载 `llm-pi-ai`,在其中配置各提供方专属的凭据和端点,并选择 pi-ai 已安装 catalog 中存在的任意提供方/模型组合。
 
-随附的 `sdk-minimal` profile 是独立显式配置树,而不是 `dsh-base` 上的 overlay。使用 `profile="sdk-minimal"` 选择它;普通 `model` 参数是唯一运行时模型选择,也适用于不在适配器建议目录中的模型 id。它提供持久 Bash、字符串替换 editor、本地执行与 JSONL 会话;settings、托管凭据、遥测、Web 工具与完整默认工具清单仍由独立的完整 `sdk` 与 `web` profile 提供。
+随附的 `sdk-minimal` profile 是独立显式配置树,而不是 `dsh-base` 上的 overlay。使用 `profile="sdk-minimal"` 选择它;普通 `model` 参数是唯一运行时模型选择,也适用于不在适配器建议目录中的模型 id。它只提供按平台选择的持久 shell、本地执行与 JSONL 会话;文件系统工具、settings、托管凭据、遥测、Web 工具与完整默认工具清单仍由独立的完整 `sdk` 与 `web` profile 提供。
 
 ## 结果与通知
 

+ 2 - 2
python/sdk/examples/README.i18n.yaml

@@ -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 python/sdk/examples/README.md
-README.md: e79cb309318c1aa31d41bb50e52f5517b07ea485
-README.zh.md: 3208c5c1462a15a50a6990f5c989ef6b81da6ab4
+README.md: 82715c67cebe5d06decd3a6301cb1a8c245958c6
+README.zh.md: e241f9d7aa0174ce806e1d67b4183b9824a2f72b

+ 2 - 3
python/sdk/examples/README.md

@@ -22,11 +22,10 @@ Set `DEEPSEEK_BASE_URL` for a compatible proxy, `DSH_MODEL` for the script's def
 The shipped [`@deepseek-ai/dsh-sdk-minimal` bundle](../../../packages/bundle/sdk-minimal/README.md) is the complete explicit Cordis tree for this mode. It exposes exactly:
 
 - owner-scoped persistent `bash` on Linux/macOS or `pwsh` on Windows
-- `str_replace_editor` with `view`, `create`, `str_replace`, and `insert`
 
-The bundle does not include `dsh-base`, so every additional row is an explicit profile change. Runtime context, local instruction discovery, compaction, settings, managed credentials, telemetry, Web tools, subagents, and the full default tool roster are absent. The tree retains SDK startup and JSON-RPC serving, one environment-configured DeepSeek adapter, local execution, and JSONL persistence.
+The bundle does not include `dsh-base`, so every additional row is an explicit profile change. Runtime context, filesystem tools, local instruction discovery, compaction, settings, managed credentials, telemetry, Web tools, subagents, and the full default tool roster are absent. The tree retains SDK startup and JSON-RPC serving, one environment-configured DeepSeek adapter, local execution, and JSONL persistence.
 
-The persistent PTY and editor can modify any path available to the runtime process, so use a disposable checkout or container.
+The persistent PTY can modify any path available to the runtime process, so use a disposable checkout or container.
 
 ## Add plugins
 

+ 2 - 3
python/sdk/examples/README.zh.md

@@ -22,11 +22,10 @@ python python/sdk/examples/minimal.py \
 随附的 [`@deepseek-ai/dsh-sdk-minimal` 组合包](../../../packages/bundle/sdk-minimal/README.zh.md)是该模式完整且显式的 Cordis 配置树。它只暴露:
 
 - Linux/macOS 上 agent 所有的持久 `bash`,或 Windows 上的 `pwsh`
-- 支持 `view`、`create`、`str_replace` 与 `insert` 的 `str_replace_editor`
 
-该组合包不包含 `dsh-base`,因此每一个新增配置项都是显式 profile 变更。运行时上下文、本地指令发现、compaction、settings、托管凭据、遥测、Web 工具、subagent 与完整默认工具清单均不存在。配置树保留 SDK 启动与 JSON-RPC 服务、一个由环境配置的 DeepSeek 适配器、本地执行和 JSONL 持久化。
+该组合包不包含 `dsh-base`,因此每一个新增配置项都是显式 profile 变更。运行时上下文、文件系统工具、本地指令发现、compaction、settings、托管凭据、遥测、Web 工具、subagent 与完整默认工具清单均不存在。配置树保留 SDK 启动与 JSON-RPC 服务、一个由环境配置的 DeepSeek 适配器、本地执行和 JSONL 持久化。
 
-持久 PTY 与 editor 可以修改运行时进程可访问的任何路径,因此只应在一次性 checkout 或容器中使用。
+持久 PTY 可以修改运行时进程可访问的任何路径,因此只应在一次性 checkout 或容器中使用。
 
 ## 添加插件
 

+ 1 - 1
scripts/release/verify-packed-install.ts

@@ -104,7 +104,7 @@ function main(): void {
     // that cannot install them must still start — which is what optional means
     // here. Their entry package is a plain dependency of dsh-sandbox-local, so
     // its tarball is supplied through --from.
-    capture('npm', ['install', '--no-audit', '--no-fund', '--package-lock=false', '--omit=optional'],
+    capture('npm', ['install', '--no-audit', '--no-fund', '--package-lock=false', '--omit=optional', '--loglevel=http'],
       { cwd: consumerRoot, env: environment })
 
     const bin = join(consumerRoot, 'node_modules', ...entry.packageName.split('/'), entry.binPath)

+ 6 - 46
scripts/smoke-python-runtime.py

@@ -33,9 +33,8 @@ CODE_PROMPT = "Use run_code to compute the packaged worker smoke value."
 CODE_WORKER_TEXT = "code worker smoke ok"
 WORKFLOW_PROMPT = "Use workflow to compute the packaged worker smoke value without agents."
 WORKFLOW_WORKER_TEXT = "workflow worker smoke ok"
-MINIMAL_PROMPT = "Exercise the packaged minimal agent's persistent shell and string-replacement editor."
+MINIMAL_PROMPT = "Exercise the packaged minimal agent's persistent shell."
 MINIMAL_TEXT = "minimal agent smoke ok"
-MINIMAL_EDITOR_PATH_PREFIX = "Editor path: "
 FS_SEARCH_PROMPT = "Exercise the packaged filesystem search tools."
 FS_SEARCH_TEXT = "filesystem search smoke ok"
 FS_SEARCH_MARKER = "PACKAGED_FS_SEARCH_OK"
@@ -335,7 +334,7 @@ def completion_chunks(body: dict[str, object]) -> list[dict[str, object]]:
         spawn_node = spawn_node_tool_followup(call_id, tool_name, tool_text)
         if spawn_node is not None:
             return spawn_node
-        minimal = minimal_tool_followup(body, call_id, tool_name, tool_text)
+        minimal = minimal_tool_followup(call_id, tool_name, tool_text)
         if minimal is not None:
             return minimal
         advanced = advanced_tool_followup(body, call_id, tool_name, tool_text)
@@ -354,14 +353,7 @@ def completion_chunks(body: dict[str, object]) -> list[dict[str, object]]:
         for message in reversed(messages)
         if isinstance(message, dict) and message.get("role") == "user"
     ]
-    minimal_prompt = next(
-        (
-            prompt
-            for prompt in user_prompts
-            if prompt.startswith(f"{MINIMAL_PROMPT}\n{MINIMAL_EDITOR_PATH_PREFIX}")
-        ),
-        None,
-    )
+    minimal_prompt = next((prompt for prompt in user_prompts if prompt == MINIMAL_PROMPT), None)
     # The minimal composition's assembled system prompt, advertised tool schemas, and
     # model-visible messages are pinned by its snapshot, not asserted here.
     if minimal_prompt is not None:
@@ -534,12 +526,11 @@ def spawn_node_tool_followup(
 
 
 def minimal_tool_followup(
-    body: dict[str, object],
     call_id: str,
     tool_name: str,
     tool_text: str,
 ) -> list[dict[str, object]] | None:
-    """Verify the checked-in minimal composition's PTY and editor."""
+    """Verify the checked-in minimal composition's persistent PTY."""
     if not call_id.startswith("minimal-"):
         return None
     if call_id == "minimal-bash-1" and tool_name == MINIMAL_SHELL_TOOL:
@@ -554,33 +545,6 @@ def minimal_tool_followup(
         expected = f"COUNT=2 CWD={MINIMAL_SHELL_SECOND_CWD}"
         if expected.lower() not in tool_text.lower():
             raise AssertionError(f"persistent shell did not retain state: {tool_text}")
-        messages = body.get("messages")
-        if not isinstance(messages, list):
-            raise AssertionError("persistent editor smoke request has no messages")
-        editor_path = next(
-            (
-                text.split(MINIMAL_EDITOR_PATH_PREFIX, 1)[1].strip()
-                for message in messages
-                if isinstance(message, dict) and message.get("role") == "user"
-                for text in [message_text(message.get("content"))]
-                if MINIMAL_EDITOR_PATH_PREFIX in text
-            ),
-            None,
-        )
-        if editor_path is None:
-            raise AssertionError("persistent editor smoke prompt has no editor path")
-        return tool_call_chunks(
-            "minimal-editor",
-            "str_replace_editor",
-            {
-                "command": "create",
-                "path": editor_path,
-                "file_text": "created by packaged editor\n",
-            },
-        )
-    if call_id == "minimal-editor" and tool_name == "str_replace_editor":
-        if "New file created successfully" not in tool_text:
-            raise AssertionError(f"packaged editor did not create its file: {tool_text}")
         return text_chunks(MINIMAL_TEXT)
     raise AssertionError(f"unexpected minimal-agent follow-up: {call_id} {tool_name}: {tool_text}")
 
@@ -1083,8 +1047,6 @@ def smoke_sdk_minimal(
     first_request = len(MockModelHandler.requests)
     with tempfile.TemporaryDirectory(prefix="dsh-sdk-minimal-") as temporary:
         root = Path(temporary).resolve()
-        editor_path = root / "created.txt"
-        prompt = f"{MINIMAL_PROMPT}\n{MINIMAL_EDITOR_PATH_PREFIX}{editor_path}"
         dsh_home = root / "home"
         sessions = dsh_home / "sessions"
         patches = ()
@@ -1112,13 +1074,11 @@ def smoke_sdk_minimal(
             base_url=base_url,
             request_timeout_seconds=60,
         ) as harness:
-            result = harness.run(prompt, session_id="minimal-agent-smoke")
+            result = harness.run(MINIMAL_PROMPT, session_id="minimal-agent-smoke")
 
         event_text = json.dumps(result.events)
         if MINIMAL_TEXT not in event_text:
             raise AssertionError(f"minimal agent run emitted no final response: {result.events}")
-        if editor_path.read_text() != "created by packaged editor\n":
-            raise AssertionError(f"packaged editor wrote unexpected content: {editor_path.read_text()!r}")
         assert_session_log(sessions, root, MINIMAL_TEXT, "COUNT=1", "COUNT=2")
 
         requests = MockModelHandler.requests[first_request:]
@@ -1877,7 +1837,7 @@ def build_in_history_snapshot_files(
     assert all(event.get("surfaceOp") in (None, "append") for event in result.events)
     first_tool = next(index for index, event in enumerate(result.events) if event.get("type") == "tool/result")
     assert result.events.index(systems[1]) > first_tool
-    assert len(requests) == 4, requests
+    assert len(requests) == 3, requests
     request_prompts = []
     for index, request in enumerate(requests):
         messages = request["messages"]

+ 0 - 4
scripts/snapshots/python-sdk-single-exe/minimal-in-history/prompt-history.json

@@ -7,10 +7,6 @@
       "You are a helpful software engineer assistant.\n\nPython SDK prompt version 1.",
       "You are a helpful software engineer assistant.\n\nPython SDK prompt version 2."
     ],
-    [
-      "You are a helpful software engineer assistant.\n\nPython SDK prompt version 1.",
-      "You are a helpful software engineer assistant.\n\nPython SDK prompt version 2."
-    ],
     [
       "You are a helpful software engineer assistant.\n\nPython SDK prompt version 1.",
       "You are a helpful software engineer assistant.\n\nPython SDK prompt version 2."

+ 6 - 433
scripts/snapshots/python-sdk-single-exe/minimal/model-visible.json

@@ -5,7 +5,7 @@
         "type": "function",
         "function": {
           "name": "bash",
-          "description": "Run commands in a bash shell\n* When invoking this tool, the contents of the \"command\" parameter does NOT need to be XML-escaped.\n* You don't have access to the internet via this tool.\n* You do have access to a mirror of common linux and python packages via apt and pip.\n* State is persistent across command calls and discussions with the user.\n* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.\n* Please avoid commands that may produce a very large amount of output.\n* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.",
+          "description": "Run commands in a bash shell\n* When invoking this tool, the contents of the \"command\" parameter does NOT need to be XML-escaped.\n* Network access depends on the task environment. Prefer configured mirrors/proxies when they are available.\n* State is persistent across command calls and discussions with the user.\n* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.\n* Please avoid commands that may produce a very large amount of output.\n* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.",
           "parameters": {
             "type": "object",
             "properties": {
@@ -19,94 +19,6 @@
             ]
           }
         }
-      },
-      {
-        "type": "function",
-        "function": {
-          "name": "str_replace_editor",
-          "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
-          "parameters": {
-            "type": "object",
-            "properties": {
-              "command": {
-                "type": "string",
-                "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
-                "enum": [
-                  "view",
-                  "create",
-                  "str_replace",
-                  "insert"
-                ]
-              },
-              "path": {
-                "type": "string",
-                "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
-              },
-              "file_text": {
-                "oneOf": [
-                  {
-                    "type": "string"
-                  },
-                  {
-                    "type": "null"
-                  }
-                ],
-                "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
-              },
-              "insert_line": {
-                "oneOf": [
-                  {
-                    "type": "integer"
-                  },
-                  {
-                    "type": "null"
-                  }
-                ],
-                "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
-              },
-              "new_str": {
-                "oneOf": [
-                  {
-                    "type": "string"
-                  },
-                  {
-                    "type": "null"
-                  }
-                ],
-                "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
-              },
-              "old_str": {
-                "oneOf": [
-                  {
-                    "type": "string"
-                  },
-                  {
-                    "type": "null"
-                  }
-                ],
-                "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
-              },
-              "view_range": {
-                "oneOf": [
-                  {
-                    "type": "array",
-                    "items": {
-                      "type": "integer"
-                    }
-                  },
-                  {
-                    "type": "null"
-                  }
-                ],
-                "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
-              }
-            },
-            "required": [
-              "command",
-              "path"
-            ]
-          }
-        }
       }
     ],
     "messages": [
@@ -116,142 +28,7 @@
       },
       {
         "role": "user",
-        "text": "Exercise the packaged minimal agent's persistent shell and string-replacement editor.\nEditor path: {{cwd}}/created.txt"
-      }
-    ]
-  },
-  {
-    "tools": [
-      {
-        "type": "function",
-        "function": {
-          "name": "bash",
-          "description": "Run commands in a bash shell\n* When invoking this tool, the contents of the \"command\" parameter does NOT need to be XML-escaped.\n* You don't have access to the internet via this tool.\n* You do have access to a mirror of common linux and python packages via apt and pip.\n* State is persistent across command calls and discussions with the user.\n* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.\n* Please avoid commands that may produce a very large amount of output.\n* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.",
-          "parameters": {
-            "type": "object",
-            "properties": {
-              "command": {
-                "type": "string",
-                "description": "The bash command to run. Relative path is preferred in the command."
-              }
-            },
-            "required": [
-              "command"
-            ]
-          }
-        }
-      },
-      {
-        "type": "function",
-        "function": {
-          "name": "str_replace_editor",
-          "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
-          "parameters": {
-            "type": "object",
-            "properties": {
-              "command": {
-                "type": "string",
-                "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
-                "enum": [
-                  "view",
-                  "create",
-                  "str_replace",
-                  "insert"
-                ]
-              },
-              "path": {
-                "type": "string",
-                "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
-              },
-              "file_text": {
-                "oneOf": [
-                  {
-                    "type": "string"
-                  },
-                  {
-                    "type": "null"
-                  }
-                ],
-                "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
-              },
-              "insert_line": {
-                "oneOf": [
-                  {
-                    "type": "integer"
-                  },
-                  {
-                    "type": "null"
-                  }
-                ],
-                "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
-              },
-              "new_str": {
-                "oneOf": [
-                  {
-                    "type": "string"
-                  },
-                  {
-                    "type": "null"
-                  }
-                ],
-                "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
-              },
-              "old_str": {
-                "oneOf": [
-                  {
-                    "type": "string"
-                  },
-                  {
-                    "type": "null"
-                  }
-                ],
-                "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
-              },
-              "view_range": {
-                "oneOf": [
-                  {
-                    "type": "array",
-                    "items": {
-                      "type": "integer"
-                    }
-                  },
-                  {
-                    "type": "null"
-                  }
-                ],
-                "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
-              }
-            },
-            "required": [
-              "command",
-              "path"
-            ]
-          }
-        }
-      }
-    ],
-    "messages": [
-      {
-        "role": "system",
-        "text": "You are a helpful software engineer assistant."
-      },
-      {
-        "role": "user",
-        "text": "Exercise the packaged minimal agent's persistent shell and string-replacement editor.\nEditor path: {{cwd}}/created.txt"
-      },
-      {
-        "role": "assistant",
-        "toolCalls": [
-          {
-            "id": "minimal-bash-1",
-            "name": "bash"
-          }
-        ]
-      },
-      {
-        "role": "tool",
-        "toolCallId": "minimal-bash-1",
-        "text": "{{tool-result}}"
+        "text": "Exercise the packaged minimal agent's persistent shell."
       }
     ]
   },
@@ -261,7 +38,7 @@
         "type": "function",
         "function": {
           "name": "bash",
-          "description": "Run commands in a bash shell\n* When invoking this tool, the contents of the \"command\" parameter does NOT need to be XML-escaped.\n* You don't have access to the internet via this tool.\n* You do have access to a mirror of common linux and python packages via apt and pip.\n* State is persistent across command calls and discussions with the user.\n* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.\n* Please avoid commands that may produce a very large amount of output.\n* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.",
+          "description": "Run commands in a bash shell\n* When invoking this tool, the contents of the \"command\" parameter does NOT need to be XML-escaped.\n* Network access depends on the task environment. Prefer configured mirrors/proxies when they are available.\n* State is persistent across command calls and discussions with the user.\n* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.\n* Please avoid commands that may produce a very large amount of output.\n* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.",
           "parameters": {
             "type": "object",
             "properties": {
@@ -275,94 +52,6 @@
             ]
           }
         }
-      },
-      {
-        "type": "function",
-        "function": {
-          "name": "str_replace_editor",
-          "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
-          "parameters": {
-            "type": "object",
-            "properties": {
-              "command": {
-                "type": "string",
-                "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
-                "enum": [
-                  "view",
-                  "create",
-                  "str_replace",
-                  "insert"
-                ]
-              },
-              "path": {
-                "type": "string",
-                "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
-              },
-              "file_text": {
-                "oneOf": [
-                  {
-                    "type": "string"
-                  },
-                  {
-                    "type": "null"
-                  }
-                ],
-                "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
-              },
-              "insert_line": {
-                "oneOf": [
-                  {
-                    "type": "integer"
-                  },
-                  {
-                    "type": "null"
-                  }
-                ],
-                "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
-              },
-              "new_str": {
-                "oneOf": [
-                  {
-                    "type": "string"
-                  },
-                  {
-                    "type": "null"
-                  }
-                ],
-                "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
-              },
-              "old_str": {
-                "oneOf": [
-                  {
-                    "type": "string"
-                  },
-                  {
-                    "type": "null"
-                  }
-                ],
-                "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
-              },
-              "view_range": {
-                "oneOf": [
-                  {
-                    "type": "array",
-                    "items": {
-                      "type": "integer"
-                    }
-                  },
-                  {
-                    "type": "null"
-                  }
-                ],
-                "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
-              }
-            },
-            "required": [
-              "command",
-              "path"
-            ]
-          }
-        }
       }
     ],
     "messages": [
@@ -372,7 +61,7 @@
       },
       {
         "role": "user",
-        "text": "Exercise the packaged minimal agent's persistent shell and string-replacement editor.\nEditor path: {{cwd}}/created.txt"
+        "text": "Exercise the packaged minimal agent's persistent shell."
       },
       {
         "role": "assistant",
@@ -387,20 +76,6 @@
         "role": "tool",
         "toolCallId": "minimal-bash-1",
         "text": "{{tool-result}}"
-      },
-      {
-        "role": "assistant",
-        "toolCalls": [
-          {
-            "id": "minimal-bash-2",
-            "name": "bash"
-          }
-        ]
-      },
-      {
-        "role": "tool",
-        "toolCallId": "minimal-bash-2",
-        "text": "{{tool-result}}"
       }
     ]
   },
@@ -410,7 +85,7 @@
         "type": "function",
         "function": {
           "name": "bash",
-          "description": "Run commands in a bash shell\n* When invoking this tool, the contents of the \"command\" parameter does NOT need to be XML-escaped.\n* You don't have access to the internet via this tool.\n* You do have access to a mirror of common linux and python packages via apt and pip.\n* State is persistent across command calls and discussions with the user.\n* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.\n* Please avoid commands that may produce a very large amount of output.\n* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.",
+          "description": "Run commands in a bash shell\n* When invoking this tool, the contents of the \"command\" parameter does NOT need to be XML-escaped.\n* Network access depends on the task environment. Prefer configured mirrors/proxies when they are available.\n* State is persistent across command calls and discussions with the user.\n* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.\n* Please avoid commands that may produce a very large amount of output.\n* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.",
           "parameters": {
             "type": "object",
             "properties": {
@@ -424,94 +99,6 @@
             ]
           }
         }
-      },
-      {
-        "type": "function",
-        "function": {
-          "name": "str_replace_editor",
-          "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
-          "parameters": {
-            "type": "object",
-            "properties": {
-              "command": {
-                "type": "string",
-                "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
-                "enum": [
-                  "view",
-                  "create",
-                  "str_replace",
-                  "insert"
-                ]
-              },
-              "path": {
-                "type": "string",
-                "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
-              },
-              "file_text": {
-                "oneOf": [
-                  {
-                    "type": "string"
-                  },
-                  {
-                    "type": "null"
-                  }
-                ],
-                "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
-              },
-              "insert_line": {
-                "oneOf": [
-                  {
-                    "type": "integer"
-                  },
-                  {
-                    "type": "null"
-                  }
-                ],
-                "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
-              },
-              "new_str": {
-                "oneOf": [
-                  {
-                    "type": "string"
-                  },
-                  {
-                    "type": "null"
-                  }
-                ],
-                "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
-              },
-              "old_str": {
-                "oneOf": [
-                  {
-                    "type": "string"
-                  },
-                  {
-                    "type": "null"
-                  }
-                ],
-                "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
-              },
-              "view_range": {
-                "oneOf": [
-                  {
-                    "type": "array",
-                    "items": {
-                      "type": "integer"
-                    }
-                  },
-                  {
-                    "type": "null"
-                  }
-                ],
-                "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
-              }
-            },
-            "required": [
-              "command",
-              "path"
-            ]
-          }
-        }
       }
     ],
     "messages": [
@@ -521,7 +108,7 @@
       },
       {
         "role": "user",
-        "text": "Exercise the packaged minimal agent's persistent shell and string-replacement editor.\nEditor path: {{cwd}}/created.txt"
+        "text": "Exercise the packaged minimal agent's persistent shell."
       },
       {
         "role": "assistant",
@@ -550,20 +137,6 @@
         "role": "tool",
         "toolCallId": "minimal-bash-2",
         "text": "{{tool-result}}"
-      },
-      {
-        "role": "assistant",
-        "toolCalls": [
-          {
-            "id": "minimal-editor",
-            "name": "str_replace_editor"
-          }
-        ]
-      },
-      {
-        "role": "tool",
-        "toolCallId": "minimal-editor",
-        "text": "{{tool-result}}"
       }
     ]
   }

+ 3 - 430
scripts/snapshots/python-sdk-single-exe/minimal/win-x64/model-visible.json

@@ -19,94 +19,6 @@
             ]
           }
         }
-      },
-      {
-        "type": "function",
-        "function": {
-          "name": "str_replace_editor",
-          "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
-          "parameters": {
-            "type": "object",
-            "properties": {
-              "command": {
-                "type": "string",
-                "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
-                "enum": [
-                  "view",
-                  "create",
-                  "str_replace",
-                  "insert"
-                ]
-              },
-              "path": {
-                "type": "string",
-                "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
-              },
-              "file_text": {
-                "oneOf": [
-                  {
-                    "type": "string"
-                  },
-                  {
-                    "type": "null"
-                  }
-                ],
-                "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
-              },
-              "insert_line": {
-                "oneOf": [
-                  {
-                    "type": "integer"
-                  },
-                  {
-                    "type": "null"
-                  }
-                ],
-                "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
-              },
-              "new_str": {
-                "oneOf": [
-                  {
-                    "type": "string"
-                  },
-                  {
-                    "type": "null"
-                  }
-                ],
-                "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
-              },
-              "old_str": {
-                "oneOf": [
-                  {
-                    "type": "string"
-                  },
-                  {
-                    "type": "null"
-                  }
-                ],
-                "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
-              },
-              "view_range": {
-                "oneOf": [
-                  {
-                    "type": "array",
-                    "items": {
-                      "type": "integer"
-                    }
-                  },
-                  {
-                    "type": "null"
-                  }
-                ],
-                "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
-              }
-            },
-            "required": [
-              "command",
-              "path"
-            ]
-          }
-        }
       }
     ],
     "messages": [
@@ -116,7 +28,7 @@
       },
       {
         "role": "user",
-        "text": "Exercise the packaged minimal agent's persistent shell and string-replacement editor.\nEditor path: {{cwd}}\\created.txt"
+        "text": "Exercise the packaged minimal agent's persistent shell."
       }
     ]
   },
@@ -140,94 +52,6 @@
             ]
           }
         }
-      },
-      {
-        "type": "function",
-        "function": {
-          "name": "str_replace_editor",
-          "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
-          "parameters": {
-            "type": "object",
-            "properties": {
-              "command": {
-                "type": "string",
-                "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
-                "enum": [
-                  "view",
-                  "create",
-                  "str_replace",
-                  "insert"
-                ]
-              },
-              "path": {
-                "type": "string",
-                "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
-              },
-              "file_text": {
-                "oneOf": [
-                  {
-                    "type": "string"
-                  },
-                  {
-                    "type": "null"
-                  }
-                ],
-                "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
-              },
-              "insert_line": {
-                "oneOf": [
-                  {
-                    "type": "integer"
-                  },
-                  {
-                    "type": "null"
-                  }
-                ],
-                "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
-              },
-              "new_str": {
-                "oneOf": [
-                  {
-                    "type": "string"
-                  },
-                  {
-                    "type": "null"
-                  }
-                ],
-                "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
-              },
-              "old_str": {
-                "oneOf": [
-                  {
-                    "type": "string"
-                  },
-                  {
-                    "type": "null"
-                  }
-                ],
-                "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
-              },
-              "view_range": {
-                "oneOf": [
-                  {
-                    "type": "array",
-                    "items": {
-                      "type": "integer"
-                    }
-                  },
-                  {
-                    "type": "null"
-                  }
-                ],
-                "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
-              }
-            },
-            "required": [
-              "command",
-              "path"
-            ]
-          }
-        }
       }
     ],
     "messages": [
@@ -237,7 +61,7 @@
       },
       {
         "role": "user",
-        "text": "Exercise the packaged minimal agent's persistent shell and string-replacement editor.\nEditor path: {{cwd}}\\created.txt"
+        "text": "Exercise the packaged minimal agent's persistent shell."
       },
       {
         "role": "assistant",
@@ -275,94 +99,6 @@
             ]
           }
         }
-      },
-      {
-        "type": "function",
-        "function": {
-          "name": "str_replace_editor",
-          "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
-          "parameters": {
-            "type": "object",
-            "properties": {
-              "command": {
-                "type": "string",
-                "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
-                "enum": [
-                  "view",
-                  "create",
-                  "str_replace",
-                  "insert"
-                ]
-              },
-              "path": {
-                "type": "string",
-                "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
-              },
-              "file_text": {
-                "oneOf": [
-                  {
-                    "type": "string"
-                  },
-                  {
-                    "type": "null"
-                  }
-                ],
-                "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
-              },
-              "insert_line": {
-                "oneOf": [
-                  {
-                    "type": "integer"
-                  },
-                  {
-                    "type": "null"
-                  }
-                ],
-                "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
-              },
-              "new_str": {
-                "oneOf": [
-                  {
-                    "type": "string"
-                  },
-                  {
-                    "type": "null"
-                  }
-                ],
-                "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
-              },
-              "old_str": {
-                "oneOf": [
-                  {
-                    "type": "string"
-                  },
-                  {
-                    "type": "null"
-                  }
-                ],
-                "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
-              },
-              "view_range": {
-                "oneOf": [
-                  {
-                    "type": "array",
-                    "items": {
-                      "type": "integer"
-                    }
-                  },
-                  {
-                    "type": "null"
-                  }
-                ],
-                "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
-              }
-            },
-            "required": [
-              "command",
-              "path"
-            ]
-          }
-        }
       }
     ],
     "messages": [
@@ -372,7 +108,7 @@
       },
       {
         "role": "user",
-        "text": "Exercise the packaged minimal agent's persistent shell and string-replacement editor.\nEditor path: {{cwd}}\\created.txt"
+        "text": "Exercise the packaged minimal agent's persistent shell."
       },
       {
         "role": "assistant",
@@ -403,168 +139,5 @@
         "text": "{{tool-result}}"
       }
     ]
-  },
-  {
-    "tools": [
-      {
-        "type": "function",
-        "function": {
-          "name": "pwsh",
-          "description": "Run commands in a PowerShell shell\n* When invoking this tool, the contents of the \"command\" parameter does NOT need to be XML-escaped.\n* You don't have access to the internet via this tool.\n* State is persistent across command calls and discussions with the user.\n* Use native Windows paths (C:\\...) and $env:NAME variables; this is PowerShell, not bash.\n* Please avoid commands that may produce a very large amount of output.\n* Please run long lived commands in the background, e.g. 'Start-Job' or start a server with Start-Process.",
-          "parameters": {
-            "type": "object",
-            "properties": {
-              "command": {
-                "type": "string",
-                "description": "The PowerShell command to run. Relative path is preferred in the command."
-              }
-            },
-            "required": [
-              "command"
-            ]
-          }
-        }
-      },
-      {
-        "type": "function",
-        "function": {
-          "name": "str_replace_editor",
-          "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
-          "parameters": {
-            "type": "object",
-            "properties": {
-              "command": {
-                "type": "string",
-                "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
-                "enum": [
-                  "view",
-                  "create",
-                  "str_replace",
-                  "insert"
-                ]
-              },
-              "path": {
-                "type": "string",
-                "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
-              },
-              "file_text": {
-                "oneOf": [
-                  {
-                    "type": "string"
-                  },
-                  {
-                    "type": "null"
-                  }
-                ],
-                "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
-              },
-              "insert_line": {
-                "oneOf": [
-                  {
-                    "type": "integer"
-                  },
-                  {
-                    "type": "null"
-                  }
-                ],
-                "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
-              },
-              "new_str": {
-                "oneOf": [
-                  {
-                    "type": "string"
-                  },
-                  {
-                    "type": "null"
-                  }
-                ],
-                "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
-              },
-              "old_str": {
-                "oneOf": [
-                  {
-                    "type": "string"
-                  },
-                  {
-                    "type": "null"
-                  }
-                ],
-                "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
-              },
-              "view_range": {
-                "oneOf": [
-                  {
-                    "type": "array",
-                    "items": {
-                      "type": "integer"
-                    }
-                  },
-                  {
-                    "type": "null"
-                  }
-                ],
-                "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
-              }
-            },
-            "required": [
-              "command",
-              "path"
-            ]
-          }
-        }
-      }
-    ],
-    "messages": [
-      {
-        "role": "system",
-        "text": "You are a helpful software engineer assistant."
-      },
-      {
-        "role": "user",
-        "text": "Exercise the packaged minimal agent's persistent shell and string-replacement editor.\nEditor path: {{cwd}}\\created.txt"
-      },
-      {
-        "role": "assistant",
-        "toolCalls": [
-          {
-            "id": "minimal-bash-1",
-            "name": "pwsh"
-          }
-        ]
-      },
-      {
-        "role": "tool",
-        "toolCallId": "minimal-bash-1",
-        "text": "{{tool-result}}"
-      },
-      {
-        "role": "assistant",
-        "toolCalls": [
-          {
-            "id": "minimal-bash-2",
-            "name": "pwsh"
-          }
-        ]
-      },
-      {
-        "role": "tool",
-        "toolCallId": "minimal-bash-2",
-        "text": "{{tool-result}}"
-      },
-      {
-        "role": "assistant",
-        "toolCalls": [
-          {
-            "id": "minimal-editor",
-            "name": "str_replace_editor"
-          }
-        ]
-      },
-      {
-        "role": "tool",
-        "toolCallId": "minimal-editor",
-        "text": "{{tool-result}}"
-      }
-    ]
   }
 ]

+ 3 - 3
snapshots/sdk/persistent-tools/notifications.expected.jsonl

@@ -11,17 +11,17 @@
 {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"session/title","seq":9,"time":0,"data":{"title":"Prove that bash state persists.","messageSeqs":[5],"source":{"kind":"fallback"}}}}}
 {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":10,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"bash-1","name":"bash","args":["{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}}}
 {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":11,"time":0,"data":{"turn":1,"step":1,"callId":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":12,"time":0,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"bash-1"},"content":[{"type":"tool-result","toolCallId":"bash-1","content":[{"type":"text","text":"COUNT=1 CWD=/tmp"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[11],"surfaceOp":"append"}}}
+{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":12,"time":0,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"bash-1"},"content":[{"type":"tool-result","toolCallId":"bash-1","content":[{"type":"text","text":"COUNT=1 CWD=/tmp\n[Command finished with exit code 0]"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[11],"surfaceOp":"append"}}}
 {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":13,"time":0,"data":{"turn":1,"step":1}}}}
 {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":14,"time":0,"data":{"turn":1,"step":2}}}}
 {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":15,"time":0,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"bash-2","name":"bash","args":["{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}}}
 {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":16,"time":0,"data":{"turn":1,"step":2,"callId":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":17,"time":0,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"bash-2"},"content":[{"type":"tool-result","toolCallId":"bash-2","content":[{"type":"text","text":"COUNT=2 CWD=/tmp"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[16],"surfaceOp":"append"}}}
+{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":17,"time":0,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"bash-2"},"content":[{"type":"tool-result","toolCallId":"bash-2","content":[{"type":"text","text":"COUNT=2 CWD=/tmp\n[Command finished with exit code 0]"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[16],"surfaceOp":"append"}}}
 {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":18,"time":0,"data":{"turn":1,"step":2}}}}
 {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":19,"time":0,"data":{"turn":1,"step":3}}}}
 {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":20,"time":0,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-delayed-pipeline","name":"bash","arguments":"{\"command\":\"{ sleep 0.1; echo delayed; } | cat\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"bash-delayed-pipeline","name":"bash","args":["{\"command\":\"{ sleep 0.1; echo delayed; } | cat\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-delayed-pipeline","name":"bash","arguments":"{\"command\":\"{ sleep 0.1; echo delayed; } | cat\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}}}
 {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":21,"time":0,"data":{"turn":1,"step":3,"callId":"bash-delayed-pipeline","name":"bash","arguments":"{\"command\":\"{ sleep 0.1; echo delayed; } | cat\"}"}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":22,"time":0,"data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"bash-delayed-pipeline"},"content":[{"type":"tool-result","toolCallId":"bash-delayed-pipeline","content":[{"type":"text","text":"delayed"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[21],"surfaceOp":"append"}}}
+{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":22,"time":0,"data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"bash-delayed-pipeline"},"content":[{"type":"tool-result","toolCallId":"bash-delayed-pipeline","content":[{"type":"text","text":"delayed\n[Command finished with exit code 0]"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[21],"surfaceOp":"append"}}}
 {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":23,"time":0,"data":{"turn":1,"step":3}}}}
 {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":24,"time":0,"data":{"turn":1,"step":4}}}}
 {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":25,"time":0,"data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"editor-create","name":"str_replace_editor","arguments":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\",\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"editor-create","name":"str_replace_editor","args":["{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\",\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"editor-create","name":"str_replace_editor","arguments":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\",\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}}}

+ 3 - 3
snapshots/sdk/persistent-tools/session.v3.jsonl

@@ -11,17 +11,17 @@
 {"type":"session/title","data":{"title":"Prove that bash state persists.","messageSeqs":[5],"source":{"kind":"fallback"}}}
 {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788699456512,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699456512,"index":0,"dt":[],"id":"bash-1","name":"bash","args":["{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"]},{"type":"chunk","time":1788699456512,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}},{"type":"chunk","time":1788699456512,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788699456512,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
 {"type":"tool/call","data":{"turn":1,"step":1,"callId":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}
-{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"bash-1"},"content":[{"type":"tool-result","toolCallId":"bash-1","content":[{"type":"text","text":"COUNT=1 CWD=/tmp"}],"isError":false}],"role":"user","id":"{{message:5}}"}},"sourceEventSeqs":[11],"surfaceOp":"append"}
+{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"bash-1"},"content":[{"type":"tool-result","toolCallId":"bash-1","content":[{"type":"text","text":"COUNT=1 CWD=/tmp\n[Command finished with exit code 0]"}],"isError":false}],"role":"user","id":"{{message:5}}"}},"sourceEventSeqs":[11],"surfaceOp":"append"}
 {"type":"step/end","data":{"turn":1,"step":1}}
 {"type":"step/start","data":{"turn":1,"step":2}}
 {"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788699456882,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699456882,"index":0,"dt":[],"id":"bash-2","name":"bash","args":["{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"]},{"type":"chunk","time":1788699456882,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}},{"type":"chunk","time":1788699456882,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788699456882,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
 {"type":"tool/call","data":{"turn":1,"step":2,"callId":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}
-{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"bash-2"},"content":[{"type":"tool-result","toolCallId":"bash-2","content":[{"type":"text","text":"COUNT=2 CWD=/tmp"}],"isError":false}],"role":"user","id":"{{message:7}}"}},"sourceEventSeqs":[16],"surfaceOp":"append"}
+{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"bash-2"},"content":[{"type":"tool-result","toolCallId":"bash-2","content":[{"type":"text","text":"COUNT=2 CWD=/tmp\n[Command finished with exit code 0]"}],"isError":false}],"role":"user","id":"{{message:7}}"}},"sourceEventSeqs":[16],"surfaceOp":"append"}
 {"type":"step/end","data":{"turn":1,"step":2}}
 {"type":"step/start","data":{"turn":1,"step":3}}
 {"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-delayed-pipeline","name":"bash","arguments":"{\"command\":\"{ sleep 0.1; echo delayed; } | cat\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788699456983,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699456983,"index":0,"dt":[],"id":"bash-delayed-pipeline","name":"bash","args":["{\"command\":\"{ sleep 0.1; echo delayed; } | cat\"}"]},{"type":"chunk","time":1788699456983,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-delayed-pipeline","name":"bash","arguments":"{\"command\":\"{ sleep 0.1; echo delayed; } | cat\"}"}}},{"type":"chunk","time":1788699456983,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788699456983,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
 {"type":"tool/call","data":{"turn":1,"step":3,"callId":"bash-delayed-pipeline","name":"bash","arguments":"{\"command\":\"{ sleep 0.1; echo delayed; } | cat\"}"}}
-{"type":"tool/result","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"bash-delayed-pipeline"},"content":[{"type":"tool-result","toolCallId":"bash-delayed-pipeline","content":[{"type":"text","text":"delayed"}],"isError":false}],"role":"user","id":"{{message:9}}"}},"sourceEventSeqs":[21],"surfaceOp":"append"}
+{"type":"tool/result","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"bash-delayed-pipeline"},"content":[{"type":"tool-result","toolCallId":"bash-delayed-pipeline","content":[{"type":"text","text":"delayed\n[Command finished with exit code 0]"}],"isError":false}],"role":"user","id":"{{message:9}}"}},"sourceEventSeqs":[21],"surfaceOp":"append"}
 {"type":"step/end","data":{"turn":1,"step":3}}
 {"type":"step/start","data":{"turn":1,"step":4}}
 {"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"editor-create","name":"str_replace_editor","arguments":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\",\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:10}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788699457224,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699457224,"index":0,"dt":[],"id":"editor-create","name":"str_replace_editor","args":["{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\",\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"]},{"type":"chunk","time":1788699457225,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"editor-create","name":"str_replace_editor","arguments":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\",\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}}},{"type":"chunk","time":1788699457225,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788699457225,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}

+ 1 - 1
snapshots/web/minimal-preset/session.v3.jsonl

@@ -13,7 +13,7 @@
 {"type":"session/title","data":{"title":"Use the bash tool to","messageSeqs":[8],"source":{"kind":"fallback"}}}
 {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"minimal-bash-card","name":"bash","arguments":"{\"command\":\"printf 'MINIMAL_BASH_CARD_OK\\\\n'\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":4},"stream":[{"type":"chunk","time":1788699487232,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699487243,"index":0,"dt":[],"id":"minimal-bash-card","name":"bash","args":["{\"command\":\"printf 'MINIMAL_BASH_CARD_OK\\\\n'\"}"]},{"type":"chunk","time":1788699487254,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"minimal-bash-card","name":"bash","arguments":"{\"command\":\"printf 'MINIMAL_BASH_CARD_OK\\\\n'\"}"}}},{"type":"chunk","time":1788699487265,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":4}}},{"type":"chunk","time":1788699487276,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
 {"type":"tool/call","data":{"turn":1,"step":1,"callId":"minimal-bash-card","name":"bash","arguments":"{\"command\":\"printf 'MINIMAL_BASH_CARD_OK\\\\n'\"}"}}
-{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"minimal-bash-card"},"content":[{"type":"tool-result","toolCallId":"minimal-bash-card","content":[{"type":"text","text":"MINIMAL_BASH_CARD_OK"}],"isError":false}],"role":"user","id":"{{message:4}}"}},"sourceEventSeqs":[13],"surfaceOp":"append"}
+{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"minimal-bash-card"},"content":[{"type":"tool-result","toolCallId":"minimal-bash-card","content":[{"type":"text","text":"MINIMAL_BASH_CARD_OK\n[Command finished with exit code 0]"}],"isError":false}],"role":"user","id":"{{message:4}}"}},"sourceEventSeqs":[13],"surfaceOp":"append"}
 {"type":"step/end","data":{"turn":1,"step":1}}
 {"type":"step/start","data":{"turn":1,"step":2}}
 {"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"MINIMAL_PRESET_REQUEST_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":4},"stream":[{"type":"chunk","time":1788699487708,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788699487719,"index":0,"dt":[],"texts":["MINIMAL_PRESET_REQUEST_OK"]},{"type":"chunk","time":1788699487730,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"MINIMAL_PRESET_REQUEST_OK"}}},{"type":"chunk","time":1788699487741,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":4}}},{"type":"chunk","time":1788699487753,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}

+ 1 - 86
snapshots/web/minimal-preset/tool-schemas.expected.json

@@ -2,7 +2,7 @@
   "initial": [
     {
       "name": "bash",
-      "description": "Run commands in a bash shell\n* When invoking this tool, the contents of the \"command\" parameter does NOT need to be XML-escaped.\n* You don't have access to the internet via this tool.\n* You do have access to a mirror of common linux and python packages via apt and pip.\n* State is persistent across command calls and discussions with the user.\n* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.\n* Please avoid commands that may produce a very large amount of output.\n* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.",
+      "description": "Run commands in a bash shell\n* When invoking this tool, the contents of the \"command\" parameter does NOT need to be XML-escaped.\n* Network access depends on the task environment. Prefer configured mirrors/proxies when they are available.\n* State is persistent across command calls and discussions with the user.\n* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.\n* Please avoid commands that may produce a very large amount of output.\n* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.",
       "parameters": {
         "type": "object",
         "properties": {
@@ -15,91 +15,6 @@
           "command"
         ]
       }
-    },
-    {
-      "name": "str_replace_editor",
-      "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
-      "parameters": {
-        "type": "object",
-        "properties": {
-          "command": {
-            "type": "string",
-            "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
-            "enum": [
-              "view",
-              "create",
-              "str_replace",
-              "insert"
-            ]
-          },
-          "path": {
-            "type": "string",
-            "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
-          },
-          "file_text": {
-            "oneOf": [
-              {
-                "type": "string"
-              },
-              {
-                "type": "null"
-              }
-            ],
-            "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
-          },
-          "insert_line": {
-            "oneOf": [
-              {
-                "type": "integer"
-              },
-              {
-                "type": "null"
-              }
-            ],
-            "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
-          },
-          "new_str": {
-            "oneOf": [
-              {
-                "type": "string"
-              },
-              {
-                "type": "null"
-              }
-            ],
-            "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
-          },
-          "old_str": {
-            "oneOf": [
-              {
-                "type": "string"
-              },
-              {
-                "type": "null"
-              }
-            ],
-            "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
-          },
-          "view_range": {
-            "oneOf": [
-              {
-                "type": "array",
-                "items": {
-                  "type": "integer"
-                }
-              },
-              {
-                "type": "null"
-              }
-            ],
-            "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
-          }
-        },
-        "required": [
-          "command",
-          "path"
-        ]
-      }
     }
   ],
   "changes": []

+ 1 - 1
snapshots/web/minimal-preset/ui.expected.md

@@ -24,7 +24,7 @@
 - button "Bash printf 'MINIMAL_BASH_CARD_OK\\n'" [expanded]:
   - img
   - text: Bash printf 'MINIMAL_BASH_CARD_OK\n'
-- text: "IN { \"command\": \"printf 'MINIMAL_BASH_CARD_OK\\\\n'\" } OUT MINIMAL_BASH_CARD_OK"
+- text: "IN { \"command\": \"printf 'MINIMAL_BASH_CARD_OK\\\\n'\" } OUT MINIMAL_BASH_CARD_OK [Command finished with exit code 0]"
 - button "Inspect"
 - paragraph: MINIMAL_PRESET_REQUEST_OK
 - button "Copy":

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů