Jelajahi Sumber

refactor(profiles): make module HMR opt-in

Move the shared module-reload policy into dsh-base by inserting its HMR row disabled, then remove the redundant disabled overrides from Web, headless, SDK, and ACP. No shipped profile enables server module reload; live profile patch watching continues through the launcher-owned config-only fallback, and browser client HMR remains a separate mechanism.

A later profile layer can opt into source-module reload explicitly with disabled: false while retaining the base root configuration. Composition tests cover every shipped mode and the explicit enable path, and the bundle references plus launcher Agent Notes document the resulting ownership and safety rationale.
Tianyi Cui 2 minggu lalu
induk
melakukan
fd814589fb
31 mengubah file dengan 96 tambahan dan 51 penghapusan
  1. 2 2
      .agents/notes/implemented/architecture/2026-08-09-headless-direct-core-entry-point.i18n.yaml
  2. 1 1
      .agents/notes/implemented/architecture/2026-08-09-headless-direct-core-entry-point.md
  3. 1 1
      .agents/notes/implemented/architecture/2026-08-09-headless-direct-core-entry-point.zh.md
  4. 2 2
      .agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.i18n.yaml
  5. 5 1
      .agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.md
  6. 5 1
      .agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.zh.md
  7. 6 7
      apps/cli/src/profile-boot.ts
  8. 42 0
      apps/cli/tests/profile-hmr.spec.ts
  9. 2 2
      packages/bundle/acp-app/README.i18n.yaml
  10. 1 1
      packages/bundle/acp-app/README.md
  11. 1 1
      packages/bundle/acp-app/README.zh.md
  12. 0 3
      packages/bundle/acp-app/cordis.patch.yml
  13. 2 2
      packages/bundle/acp-app/tests/acp-app.spec.ts
  14. 2 2
      packages/bundle/base/README.i18n.yaml
  15. 2 0
      packages/bundle/base/README.md
  16. 2 0
      packages/bundle/base/README.zh.md
  17. 3 0
      packages/bundle/base/cordis.patch.yml
  18. 5 1
      packages/bundle/base/tests/base.spec.ts
  19. 2 2
      packages/bundle/headless/README.i18n.yaml
  20. 1 1
      packages/bundle/headless/README.md
  21. 1 1
      packages/bundle/headless/README.zh.md
  22. 0 5
      packages/bundle/headless/cordis.patch.yml
  23. 2 2
      packages/bundle/sdk-app/README.i18n.yaml
  24. 1 1
      packages/bundle/sdk-app/README.md
  25. 1 1
      packages/bundle/sdk-app/README.zh.md
  26. 0 3
      packages/bundle/sdk-app/cordis.patch.yml
  27. 2 2
      packages/bundle/sdk-app/tests/sdk-app.spec.ts
  28. 2 2
      packages/bundle/web-app/README.i18n.yaml
  29. 0 0
      packages/bundle/web-app/README.md
  30. 0 0
      packages/bundle/web-app/README.zh.md
  31. 0 4
      packages/bundle/web-app/cordis.patch.yml

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-09-headless-direct-core-entry-point.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-09-headless-direct-core-entry-point.md
-2026-08-09-headless-direct-core-entry-point.md: cf6b4a92a6e9b390c7fcaca17f56b4c652cc9319
-2026-08-09-headless-direct-core-entry-point.zh.md: 9f45fcdaf87ddcccbd331eeacce0dc7035c61f19
+2026-08-09-headless-direct-core-entry-point.md: 8ed979794afa008588d1b849f0074e8696e6e43f
+2026-08-09-headless-direct-core-entry-point.zh.md: 512d4b88c921431fe26afd9f62c34a1939ac5bdd

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-09-headless-direct-core-entry-point.md

@@ -12,7 +12,7 @@ The direct entry point still needs the same deployment model state as Web-create
 
 ## Decision
 
-The shipped `headless` profile contains `dsh-base` and `dsh-headless`. The headless bundle supplies its persona and tool mode, disables HMR, mounts the Code Mode worker explicitly, and inserts `headless-runner`. Its tree contains no `@deepseek-ai/dsh-host-*` package, ApiProxy, HTTP server, Web runtime, or browser client. Code Mode and Session persistence are one-shot Agent capabilities independent of Web presentation.
+The shipped `headless` profile contains `dsh-base` and `dsh-headless`. The base supplies the disabled module-HMR default; the headless bundle supplies its persona and tool mode, mounts the Code Mode worker explicitly, and inserts `headless-runner` without overriding that policy. Its tree contains no `@deepseek-ai/dsh-host-*` package, ApiProxy, HTTP server, Web runtime, or browser client. Code Mode and Session persistence are one-shot Agent capabilities independent of Web presentation.
 
 `headless-runner` is a direct core entry point. After Loader settlement, it reads `ctx.agentDefaultModel.currentSelection()`, creates a fresh persisted Agent through `ctx.agents.create`, installs that `ModelSelection` in the Agent scope, waits for startup quiescence, anchors the Session sequence, submits one ordinary user message, and waits for quiescence again. It awaits `ctx.sessions.flush`, folds its durable event interval for the last non-empty assistant text and final `turn/end` reason, writes the text plus one newline to stdout, and requests bounded launcher shutdown with exit 0 exactly when the reason is `completed`. A terminal `error` reason writes its durable code and message to stderr; unexpected driver failures also use stderr and exit 1.
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-09-headless-direct-core-entry-point.zh.md

@@ -12,7 +12,7 @@ Status: implemented
 
 ## 决策
 
-随附的 `headless` profile 包含 `dsh-base` 与 `dsh-headless`。headless 组合包提供自身的 persona 与工具模式、禁用 HMR(热模块替换)、显式挂载 Code Mode worker,并插入 `headless-runner`。其插件树不包含任何 `@deepseek-ai/dsh-host-*` 包、ApiProxy、HTTP server、Web 运行时或浏览器客户端。Code Mode 与会话持久化均为独立于 Web 呈现的一次性 Agent 能力。
+随附的 `headless` profile 包含 `dsh-base` 与 `dsh-headless`。base 提供默认禁用模块 HMR(热模块替换)的策略;headless 组合包提供自身的 persona 与工具模式、显式挂载 Code Mode worker,并在不覆盖该策略的情况下插入 `headless-runner`。其插件树不包含任何 `@deepseek-ai/dsh-host-*` 包、ApiProxy、HTTP server、Web 运行时或浏览器客户端。Code Mode 与会话持久化均为独立于 Web 呈现的一次性 Agent 能力。
 
 `headless-runner` 是直接使用核心服务的入口。Loader 完全加载后,它读取 `ctx.agentDefaultModel.currentSelection()`,通过 `ctx.agents.create` 创建一个新的持久化 Agent,在 Agent 作用域中安装该 `ModelSelection`,等待启动工作完全停稳,锚定会话事件序号,提交一条普通用户消息,再次等待完全停稳。随后,它等待 `ctx.sessions.flush`,折叠自身持有的持久事件区间,以取得最后一条非空 assistant 文本和最终 `turn/end` 结束原因,将文本连同一个换行写入 stdout,并且仅在结束原因为 `completed` 时请求启动器以退出状态 0 有界关闭。结束原因为 `error` 时,其持久化错误码与消息写入 stderr;驱动器的意外失败也写入 stderr 并以 1 退出。
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.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-22-single-dsh-application-launcher.md
-2026-08-22-single-dsh-application-launcher.md: 102d8d80ae16a2af27622aeed57a1cae4e2a3986
-2026-08-22-single-dsh-application-launcher.zh.md: 22b8a0affde118380af53b0b9608a346dd8a8db1
+2026-08-22-single-dsh-application-launcher.md: 69188e806d9192d230d1f1b52daf27a3b30481be
+2026-08-22-single-dsh-application-launcher.zh.md: a6bb1909b019ea0d386bd2f4a1bb8f5199ef1974

+ 5 - 1
.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.md

@@ -31,7 +31,7 @@ Profile manifests own patch reload:
 | `sdk` | `startup` |
 | `acp` | `startup` |
 
-Custom profiles default to `live`. A startup profile still applies its bundle, profile, home-level, and invocation `--patch` layers, but it does not watch them after boot. SDK and ACP also disable module HMR because one owned stdio connection cannot safely replace its server, agents, persistence, or tool registry in place.
+Custom profiles default to `live`. A startup profile still applies its bundle, profile, home-level, and invocation `--patch` layers, but it does not watch them after boot. `dsh-base` inserts the module-HMR row disabled; a profile with a tested source-module reload lifecycle must enable it explicitly. None of the shipped profiles enable server module HMR: `patchReload: live` uses the launcher's config-only watcher while the startup profiles install no watcher. SDK and ACP cannot safely replace their server, agents, persistence, or tool registry inside one owned stdio connection.
 
 The shipped protocol profiles reserve stdout for protocol frames, expose help without starting transport, and route stdin EOF and signals through bounded root disposal. ACP remains automation-only. The SDK JSON-RPC methods, notification fields, and `initialize.serverInfo.name` remain stable. Model-visible tool and persistence defaults come from `dsh-base`, and runnable snapshots own those assembled application outputs.
 
@@ -75,6 +75,8 @@ The [ACP automation-only protocol](../simplification/2026-07-23-acp-automation-o
 
 **Resolve `dsh` only from `PATH`.** Rejected: ordinary Node processes do not reliably inherit a project-local `.bin` path. A same-version package dependency provides a deterministic runtime.
 
+**Enable module HMR in `dsh-base` and make unsafe profiles disable it.** Rejected: the shared base also underlies custom profiles, so an enabled default makes every new application remember to opt out of source-module replacement. A disabled base makes module HMR an explicit profile capability while leaving `patchReload: live` config watching available.
+
 **Hot-reload protocol profiles.** Rejected: replacing a protocol server or its dependencies can invalidate pending frames and SDK-owned agents. Process restart is the adoption boundary for SDK and ACP configuration changes.
 
 **Move the Python executable through profiles without a separate packaging proof.** Rejected: the native VFS closure, three platform wheels, ripgrep and spawn-helper sidecars, default config discovery, and clean-install behavior require their own migration evidence.
@@ -82,6 +84,7 @@ The [ACP automation-only protocol](../simplification/2026-07-23-acp-automation-o
 ## Verification
 
 - Source and built CLI acceptance cover `sdk` and `acp` help, transport startup, stdout purity, EOF, signals, and root disposal.
+- Bundle configuration tests pin module HMR disabled in `dsh-base` and absent from shipped mode overrides; the custom live-profile e2e pins config reload through the launcher's watch-only fallback.
 - Focused unit suites cover profile launch resolution, initialization bounds, SDK retries, server readiness, and nested isolated homes with 100% coverage on the changed runtime sources.
 - Keyless ACP and SDK snapshots boot real `dsh` profiles and pin protocol output plus persisted logs; the nested SDK composition boots a second real profile runtime.
 - The real-API workflow caps file parallelism at four because one profile e2e file can own several complete `dsh` subprocess trees; workflow tests pin that resource bound.
@@ -91,6 +94,7 @@ The [ACP automation-only protocol](../simplification/2026-07-23-acp-automation-o
 ## Consequences
 
 - A user changes an SDK application's plugin composition through a named profile and ordered patches, using the same installation and resolution model as every other dsh application.
+- A custom profile receives live config watching without server module HMR and opts into source-module replacement only through an explicit row override.
 - SDK and ACP share the complete base application and one set of policy and tools; snapshots present intentional assembled differences explicitly.
 - Adding `@deepseek-ai/dsh` increases the TypeScript client's install size in exchange for a deterministic same-version runtime.
 - Trusted user patches can add a plugin that writes to stdout and corrupt their own protocol stream; shipped profiles guarantee purity, not arbitrary third-party composition.

+ 5 - 1
.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.zh.md

@@ -31,7 +31,7 @@ Profile manifest 负责 patch 重载:
 | `sdk` | `startup` |
 | `acp` | `startup` |
 
-自定义 profile 默认为 `live`。`startup` profile 仍会应用组合包、profile、home 级与调用时 `--patch` 各层,但启动后不会监视这些文件。SDK 与 ACP 还会禁用模块 HMR(热模块替换),因为一个自有 stdio 连接无法安全地原地替换其服务器、agent、持久化或工具注册表。
+自定义 profile 默认为 `live`。`startup` profile 仍会应用组合包、profile、home 级与调用时 `--patch` 各层,但启动后不会监视这些文件。`dsh-base` 插入的模块 HMR(热模块替换)配置项默认禁用;具有经过验证的源码模块重载生命周期的 profile 必须显式启用它。随附 profile 均不启用服务器模块 HMR:`patchReload: live` 使用启动器的仅配置 watcher,`startup` profile 则不安装 watcher。SDK 与 ACP 无法在一个自有 stdio 连接内安全替换其服务器、agent、持久化或工具注册表。
 
 随附协议 profile 将 stdout 保留给协议帧,显示帮助时不启动 transport,并通过有界根节点 dispose(资源释放)处理 stdin EOF 与信号。ACP 继续仅用于自动化。SDK JSON-RPC 方法、通知字段与 `initialize.serverInfo.name` 保持稳定。模型可见工具与持久化默认值来自 `dsh-base`,可运行快照负责钉住这些已组装的应用输出。
 
@@ -75,6 +75,8 @@ Python 运行时后续工作必须把打包进程迁移到 `dsh --profile sdk`
 
 **只从 `PATH` 解析 `dsh`。** 拒绝:普通 Node 进程不一定继承项目本地 `.bin` 路径。同版本包依赖可以提供确定的运行时。
 
+**在 `dsh-base` 中启用模块 HMR,再由不安全的 profile 逐一禁用。** 拒绝:共享 base 同样承载自定义 profile;默认启用会要求每个新应用都记得退出源码模块替换。base 默认禁用会让模块 HMR 成为显式的 profile 能力,同时保留 `patchReload: live` 配置监视。
+
 **热重载协议 profile。** 拒绝:替换协议服务器或其依赖可能破坏待处理协议帧与 SDK 自有 agent。进程重启是 SDK 与 ACP 配置变更的采用边界。
 
 **不做独立打包证明就把 Python 可执行文件迁移到 profile。** 拒绝:原生 VFS 闭包、三个平台 wheel 包、ripgrep 与 spawn-helper 伴随文件、默认配置发现和干净安装行为都需要自己的迁移证据。
@@ -82,6 +84,7 @@ Python 运行时后续工作必须把打包进程迁移到 `dsh --profile sdk`
 ## 验证
 
 - 源码与构建后 CLI 验收覆盖 `sdk` 和 `acp` 的帮助、transport 启动、stdout 纯净性、EOF、信号与根节点 dispose。
+- 组合包配置测试钉住 `dsh-base` 默认禁用模块 HMR,随附模式覆盖层不再重复该策略;自定义 live profile 的 e2e 钉住启动器仅监视 fallback 提供的配置重载。
 - 聚焦单元套件覆盖 profile 启动解析、初始化时限、SDK 重试、服务器就绪和嵌套隔离 home,并对变更后的运行时源码实现 100% 覆盖率。
 - 免密钥 ACP 与 SDK 快照启动真实 `dsh` profile,并钉住协议输出与持久化日志;嵌套 SDK 组合会启动第二个真实 profile 运行时。
 - 真实 API 工作流把文件并行度限制为 4,因为一个 profile e2e 文件可能拥有多个完整 `dsh` 子进程树;工作流测试会钉住该资源上限。
@@ -91,6 +94,7 @@ Python 运行时后续工作必须把打包进程迁移到 `dsh --profile sdk`
 ## 影响
 
 - 用户通过具名 profile 与有序 patch 更改 SDK 应用的插件组合,使用与其他所有 dsh 应用相同的安装与解析模型。
+- 自定义 profile 可以在不启用服务器模块 HMR 的情况下获得实时配置监视,只有显式覆盖配置项才会启用源码模块替换。
 - SDK 与 ACP 共享完整 base 应用和同一份策略与工具;快照以显式差异呈现刻意采用的组装变化。
 - 增加 `@deepseek-ai/dsh` 会扩大 TypeScript 客户端的安装体积,换来确定的同版本运行时。
 - 受信任用户 patch 可以增加写入 stdout 的插件并破坏自己的协议流;随附 profile 保证纯净,不为任意第三方组合提供保证。

+ 6 - 7
apps/cli/src/profile-boot.ts

@@ -294,13 +294,12 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
     && ctx.fiber.state === FiberState.ACTIVE
     && ctx.get('loader') !== undefined) {
     try {
-      // Config-only HMR for the live profile patch layer: the web bundle
-      // disables the shared module-reload `hmr` row (its reload lifecycle is
-      // untested), so when the composition leaves no HMR service, mount a
-      // watch-only instance with no module roots — cordis.patch.yml edits stay
-      // live for the profiles that select it. A silent skip would break their
-      // documented reload contract. HMR injects the timer service, which a
-      // bare custom profile may not mount either.
+      // Config-only HMR for the live profile patch layer: dsh-base disables
+      // module reload by default, so when no profile explicitly enabled that
+      // service, mount a watch-only instance with no module roots —
+      // cordis.patch.yml edits stay live without replacing source modules. A
+      // silent skip would break the documented reload contract. HMR injects
+      // the timer service, which a bare custom profile may not mount either.
       if (ctx.get('hmr') === undefined) {
         if (ctx.get('timer') === undefined) {
           await ctx.loader.create({ name: '@deepseek-ai/cordis-plugin-timer' })

+ 42 - 0
apps/cli/tests/profile-hmr.spec.ts

@@ -0,0 +1,42 @@
+/** Module-HMR ownership across the real shipped profile bundle layers. */
+
+import { join } from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { describe, expect, it } from 'vitest'
+import { composeEntries, loadOverlayPatches } from '@deepseek-ai/dsh-app-boot'
+import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
+
+const REPOSITORY_ROOT = fileURLToPath(new URL('../../../', import.meta.url))
+
+/** Load one shipped bundle patch through the same parser as profile boot. */
+function bundle(name: 'acp-app' | 'base' | 'headless' | 'sdk-app' | 'web-app'): PatchOptions[] {
+  return loadOverlayPatches('profile-hmr test', join(REPOSITORY_ROOT, 'packages', 'bundle', name, 'cordis.patch.yml'))
+}
+
+/** Resolve the effective HMR row after the supplied layers. */
+function hmr(layers: PatchOptions[][]) {
+  const row = composeEntries(layers).find(entry => entry.id === 'hmr')
+  if (row === undefined) throw new Error('the base bundle must insert the hmr row')
+  return row
+}
+
+describe('profile module-HMR policy', () => {
+  it.each(['web-app', 'headless', 'sdk-app', 'acp-app'] as const)(
+    '%s inherits the disabled base row without a mode override',
+    (mode) => {
+      const modePatches = bundle(mode)
+      expect(modePatches.some(patch => patch.id === 'hmr')).toBe(false)
+      expect(hmr([bundle('base'), modePatches])).toMatchObject({
+        disabled: true,
+        config: { root: ['.'] },
+      })
+    },
+  )
+
+  it('requires an explicit later layer to enable source-module reload', () => {
+    expect(hmr([bundle('base'), [{ id: 'hmr', disabled: false }]])).toMatchObject({
+      disabled: false,
+      config: { root: ['.'] },
+    })
+  })
+})

+ 2 - 2
packages/bundle/acp-app/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/acp-app/README.md
-README.md: 15890b8d13446613bbddc1b764290470abf28d1a
-README.zh.md: 5bf85d224ee07d8fa013f7cd0dd67a67a8d70dbf
+README.md: d00458d3e23cfd9ff8984454aace991d3c2f8dd9
+README.zh.md: e32eac45d407af25c474b6df483dc587dd4a5038

+ 1 - 1
packages/bundle/acp-app/README.md

@@ -2,7 +2,7 @@
 
 English | [中文](README.zh.md)
 
-The automation-only ACP stdio application as a `dsh` profile bundle over [`dsh-base`](../base/README.md). Its patch sets the coding-agent persona and default model route, disables module HMR, mounts an app-owned zero-option command provider, and starts [`dsh-acp`](../../acp/acp/README.md) only after that provider accepts the invocation. `dsh --profile acp --help` therefore writes help and exits without claiming stdin or stdout.
+The automation-only ACP stdio application as a `dsh` profile bundle over [`dsh-base`](../base/README.md). It inherits the base's disabled module-HMR policy; its patch sets the coding-agent persona and default model route, mounts an app-owned zero-option command provider, and starts [`dsh-acp`](../../acp/acp/README.md) only after that provider accepts the invocation. `dsh --profile acp --help` therefore writes help and exits without claiming stdin or stdout.
 
 The startup provider binds stdin EOF to the launcher's bounded successful shutdown. ACP connection close, SIGINT, and SIGTERM drain the bridge-owned agents and the root profile tree before exit. Stdout is reserved for newline-delimited ACP JSON-RPC frames. The bundle disables model-generated session titles because ACP exposes no title surface; deterministic fallback titles remain durable without an auxiliary model request. A deployment selects a different complete composition through profile bundles and patch files, not another app bin.
 

+ 1 - 1
packages/bundle/acp-app/README.zh.md

@@ -2,7 +2,7 @@
 
 [English](README.md) | 中文
 
-以 [`dsh-base`](../base/README.zh.md) 为基础的 automation-only ACP stdio 应用 `dsh` profile 组合包。其 patch 设置 coding agent(编程智能体)persona 与默认模型路由、禁用模块 HMR(热模块替换)、挂载应用自有的零选项命令提供方,并且只在该提供方接受调用后启动 [`dsh-acp`](../../acp/acp/README.zh.md)。因此,`dsh --profile acp --help` 会写出 help 并退出,不会占用 stdin 或 stdout。
+以 [`dsh-base`](../base/README.zh.md) 为基础的 automation-only ACP stdio 应用 `dsh` profile 组合包。它继承 base 默认禁用模块 HMR(热模块替换)的策略;其 patch 设置 coding agent(编程智能体)persona 与默认模型路由、挂载应用自有的零选项命令提供方,并且只在该提供方接受调用后启动 [`dsh-acp`](../../acp/acp/README.zh.md)。因此,`dsh --profile acp --help` 会写出 help 并退出,不会占用 stdin 或 stdout。
 
 启动提供方把 stdin EOF 绑定到启动器的有界成功关闭。ACP 连接关闭、SIGINT 与 SIGTERM 会在退出前排空 bridge 自有 agent 以及根 profile 树。Stdout 仅保留给换行分隔的 ACP JSON-RPC frame。ACP 不提供 title 表层,因此本组合包禁用模型生成的 session title;确定性的 fallback title 仍会持久化,但不发起辅助模型请求。部署方通过 profile 组合包与 patch 文件选择另一套完整组合,而不是使用另一个 app bin。
 

+ 0 - 3
packages/bundle/acp-app/cordis.patch.yml

@@ -5,9 +5,6 @@
     persona: >-
       You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.
 
-- id: hmr
-  disabled: true
-
 - id: session-title-llm
   disabled: true
 

+ 2 - 2
packages/bundle/acp-app/tests/acp-app.spec.ts

@@ -8,7 +8,7 @@ import { describe, expect, it } from 'vitest'
 import { entryListSchema } from '@deepseek-ai/cordis-plugin-include'
 
 describe('dsh-acp-app bundle', () => {
-  it('declares startup-gated ACP serving with module HMR disabled', () => {
+  it('declares startup-gated ACP serving without overriding base HMR policy', () => {
     const root = fileURLToPath(new URL('..', import.meta.url))
     const manifest = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')) as {
       dependencies?: Record<string, string>
@@ -24,7 +24,7 @@ describe('dsh-acp-app bundle', () => {
       disabled?: boolean
       insert?: Array<{ config?: { model?: string; provider?: string }; id?: string; inject?: string[]; name?: string }>
     }>
-    expect(patches.find(patch => patch.id === 'hmr')).toMatchObject({ disabled: true })
+    expect(patches.find(patch => patch.id === 'hmr')).toBeUndefined()
     expect(patches.find(patch => patch.id === 'session-title-llm')).toMatchObject({ disabled: true })
     const rows = patches.flatMap(patch => patch.insert ?? [])
     expect(rows.find(row => row.id === 'acp-app-startup')?.name).toBe('@deepseek-ai/dsh-acp-app')

+ 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: 8487426ee7bf1b39a79b4e80b9c7bd661f317998
-README.zh.md: 3c62d9841ae809b4ce502efbfe886e46ab1e158f
+README.md: 74f1288b46dd20643a494acb1829dbe38c367622
+README.zh.md: dda46a89f3c2b161d7358109e4317a976eaa65c8

+ 2 - 0
packages/bundle/base/README.md

@@ -4,6 +4,8 @@ English | [中文](README.zh.md)
 
 The shared dsh core as a profile bundle: [`cordis.patch.yml`](cordis.patch.yml) inserts every base plugin row — model adapters, the shared [`agent-default-model`](../../core/agent-default-model/README.md) selection, tools, persistence, policy, settings/credentials, telemetry, and the core spawn/fork subagent providers — over the empty profile root, as the first layer of every profile's `dsh.profile.bundles` list. The optional Codex and Claude Code providers stay outside this package and its production dependency closure; a Profile installs either [product provider Bundle](../../subagent/README.md) only when needed. The default `@deepseek-ai/dsh` production closure therefore includes neither product provider, the Claude Agent SDK, nor the Codex wrapper and platform payloads. Later bundle layers (e.g. [`dsh-web-app`](../web-app/README.md)) and the user's profile `cordis.patch.yml` override these rows by id; a patch replaces a row's whole `config`, so mode-specific values live in mode bundles, not here. The package has no runtime API; the profile composer resolves the patch through the `dsh.bundle.patch` manifest field, never through code.
 
+The base module-HMR row is disabled. A profile with a tested source-module reload lifecycle enables that row explicitly; `patchReload: live` config watching is independent and uses the launcher's watch-only fallback while module HMR remains disabled.
+
 The patch gates both shell stacks by platform on its own rows: `bash-sandbox`/`tool-bash` carry `disabled: !!js process.platform === 'win32'` (bash has no Windows runner), and their twins `pwsh-sandbox`/`tool-pwsh` mount on win32 only with the inverted expression — one shared patch file, exactly one shell stack per host. The permission surface stays exactly as on POSIX: `sandbox`/`sandbox-policy` enforce the file-effect policy through the Windows ACL restricted-token runner (the win32 chain of `dsh-sandbox-local` → `@deepseek-ai/dsh-sandbox-windows-acl`), the permission switcher and the approval service run unchanged, and `fs-sandbox` keeps fencing `ctx.fs` writes — mounting `dsh-fs-local` alongside it would double-register `ctx.fs` and fail the load. A Windows host that prefers the unconfined local pwsh executor or full access overrides these rows through its profile or home `cordis.patch.yml` (the bash-restore recipe must be complete: disable `pwsh-sandbox`/`tool-pwsh` AND re-enable `bash-sandbox`/`tool-bash` — both executor families register the same `bash` service, so an incomplete recipe fails loud at load). POSIX hosts see the pwsh rows disabled.
 
 The row set and its rationale are documented inline in the patch file; the [generated composition graph](../../../apps/cli/composition.md) renders it.

+ 2 - 0
packages/bundle/base/README.zh.md

@@ -4,6 +4,8 @@
 
 以 profile 组合包形式交付的共享 dsh 核心:[`cordis.patch.yml`](cordis.patch.yml) 在空的 profile 根之上插入全部基础插件行——模型适配器、共享的 [`agent-default-model`](../../core/agent-default-model/README.zh.md) 选择、工具、持久化、策略、settings/credentials、遥测与核心 spawn/fork subagent provider——作为每个 profile 的 `dsh.profile.bundles` 列表中的第一层。可选的 Codex 与 Claude Code provider 不属于本包及其生产依赖闭包;Profile 仅在需要时安装任一[产品 provider Bundle](../../subagent/README.zh.md)。因此,默认的 `@deepseek-ai/dsh` 生产依赖闭包既不包含任一产品 provider、Claude Agent SDK,也不包含 Codex wrapper 及其平台载荷。后续的组合包层(例如 [`dsh-web-app`](../web-app/README.zh.md))和用户 profile 的 `cordis.patch.yml` 按 id 覆盖这些行;patch 会替换目标行的整个 `config`,因此模式专属的值放在各模式组合包中,而不是这里。该包没有运行时 API;profile 组合器通过 manifest(元数据清单)的 `dsh.bundle.patch` 字段解析 patch,绝不通过代码。
 
+base 的模块 HMR 配置项默认禁用。具有经过验证的源码模块重载生命周期的 profile 必须显式启用该配置项;`patchReload: live` 配置监视与之独立,在模块 HMR 保持禁用时使用启动器的仅监视 fallback。
+
 patch 在自身上按平台门控两个 shell 栈:`bash-sandbox`/`tool-bash` 携带 `disabled: !!js process.platform === 'win32'`(bash 没有 Windows runner),它们的孪生行 `pwsh-sandbox`/`tool-pwsh` 以取反的表达式仅在 win32 挂载——同一份 patch 文件,每个宿主恰好挂载一个 shell 栈。权限面与 POSIX 完全一致:`sandbox`/`sandbox-policy` 通过 Windows ACL 受限令牌 runner(`dsh-sandbox-local` 的 win32 链 → `@deepseek-ai/dsh-sandbox-windows-acl`)执行文件效果策略,权限切换器与 approval 服务原样运行,`fs-sandbox` 继续围栏 `ctx.fs` 写入——在其旁再挂载 `dsh-fs-local` 会重复注册 `ctx.fs` 并在加载时失败。偏好不受沙盒约束的本地 pwsh 执行器或完整访问的 Windows 主机通过其 profile 或 home 的 `cordis.patch.yml` 覆盖这些行(bash 恢复配方必须完整:禁用 `pwsh-sandbox`/`tool-pwsh` 并重新启用 `bash-sandbox`/`tool-bash`——两个执行器家族注册同一个 `bash` 服务,配方不完整会在加载时直接报错)。POSIX 主机看到的是被禁用的 pwsh 行。
 
 行集合及其设计依据以行内注释写在 patch 文件里;[生成的组合图](../../../apps/cli/composition.md)负责渲染它。

+ 3 - 0
packages/bundle/base/cordis.patch.yml

@@ -16,8 +16,11 @@
     - id: timer
       name: '@deepseek-ai/cordis-plugin-timer'
 
+    # Module reload is opt-in per profile. `patchReload: live` config watching
+    # uses the launcher's watch-only fallback and does not require this row.
     - id: hmr
       name: '@deepseek-ai/cordis-plugin-hmr'
+      disabled: true
       config:
         root: ['.']
 

+ 5 - 1
packages/bundle/base/tests/base.spec.ts

@@ -27,7 +27,7 @@ describe('dsh-base bundle', () => {
     )
     expect(Array.isArray(parsed)).toBe(true)
     // The base layer is one insert list over the empty profile root.
-    const rows = (parsed as { insert?: { id?: string; config?: Record<string, unknown> }[] }[]).flatMap(
+    const rows = (parsed as { insert?: { id?: string; config?: Record<string, unknown>; disabled?: boolean }[] }[]).flatMap(
       patch => patch.insert ?? [],
     )
     expect(rows.length).toBeGreaterThan(50)
@@ -35,6 +35,10 @@ describe('dsh-base bundle', () => {
     expect(rows.find(row => row.id === 'session-telemetry-otel')?.config?.['mode']).toEqual({
       __jsExpr: "process.env.DSH_TELEMETRY_MODE || 'DISABLED'",
     })
+    expect(rows.find(row => row.id === 'hmr')).toMatchObject({
+      disabled: true,
+      config: { root: ['.'] },
+    })
     expect(rows.filter(row => row.id === 'subagent-codex')).toHaveLength(0)
     expect(rows.filter(row => row.id === 'subagent-claude-code')).toHaveLength(0)
     expect(manifest.dependencies).not.toHaveProperty('@deepseek-ai/dsh-subagent-codex')

+ 2 - 2
packages/bundle/headless/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/headless/README.md
-README.md: 3d9ca350f5f8891e60cfc57c9ca89ef57d9790d3
-README.zh.md: 2c7ea71025aa68db08b10d9faff8f546d12911c6
+README.md: 22b4ac8ecbbaabc1d5268230ea99a5d3a89aff14
+README.zh.md: a57e29dc947c0c368165af0ad4342a748711500b

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

@@ -2,7 +2,7 @@
 
 English | [中文](README.zh.md)
 
-The dsh one-shot bundle. [`cordis.patch.yml`](cordis.patch.yml) rides directly over [`dsh-base`](../base/README.md): it supplies the coding persona and tool mode, disables HMR, mounts Code Mode's worker as a core execution capability, and inserts this package's `headless-runner` plugin (config `{task}`, resolved from the injected `headlessStartup` provider). It mounts no Host, HTTP server, Web runtime, or browser plugin.
+The dsh one-shot bundle. [`cordis.patch.yml`](cordis.patch.yml) rides directly over [`dsh-base`](../base/README.md): it inherits the base's disabled module-HMR policy, supplies the coding persona and tool mode, mounts Code Mode's worker as a core execution capability, and inserts this package's `headless-runner` plugin (config `{task}`, resolved from the injected `headlessStartup` provider). It mounts no Host, HTTP server, Web runtime, or browser plugin.
 
 After the Loader settles, the runner reads the shared [`ctx.agentDefaultModel`](../../core/agent-default-model/README.md), creates one fresh persisted Agent through `ctx.agents`, submits the task as an ordinary user message, and waits for quiescence. It flushes the Session before folding the owned durable event interval, writes the last non-empty assistant text to stdout, and requests exit through the launcher-provided `ctx.appExit` host hook ([`dsh-cmdline`](../../boot/cmdline/README.md)) (final `turn/end` completed → 0, otherwise 1). A terminal `error` reason also writes its code and message to stderr; successful runs keep stderr empty. The process opens no listening port. The task text is this app's command line: the ordinary `headless-startup` provider ([`src/startup.ts`](src/startup.ts)) injects `ctx.cmdlineArgs` ([`dsh-cmdline`](../../boot/cmdline/README.md)), reads the positional argument of `dsh --profile headless "task"`, prints the app's `--help`, and provides `headlessStartup`; the runner injects that service and reads its task from lazy config. A missing or whitespace-only task is rejected before the runner activates.
 

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

@@ -2,7 +2,7 @@
 
 [English](README.md) | 中文
 
-dsh 一次性任务组合包。[`cordis.patch.yml`](cordis.patch.yml) 直接叠加在 [`dsh-base`](../base/README.zh.md) 之上:提供编码 persona 和工具模式、禁用 HMR(热模块替换)、将 Code Mode 的 worker 作为核心执行能力挂载,并插入本包的 `headless-runner` 插件(配置为 `{task}`,从注入的 `headlessStartup` 提供方解析)。它不挂载任何 Host、HTTP server、Web runtime 或浏览器插件。
+dsh 一次性任务组合包。[`cordis.patch.yml`](cordis.patch.yml) 直接叠加在 [`dsh-base`](../base/README.zh.md) 之上:继承 base 默认禁用模块 HMR(热模块替换)的策略,提供编码 persona 和工具模式,将 Code Mode 的 worker 作为核心执行能力挂载,并插入本包的 `headless-runner` 插件(配置为 `{task}`,从注入的 `headlessStartup` 提供方解析)。它不挂载任何 Host、HTTP server、Web runtime 或浏览器插件。
 
 Loader 结算后,runner 读取共享的 [`ctx.agentDefaultModel`](../../core/agent-default-model/README.zh.md),通过 `ctx.agents` 创建一个全新的持久化 Agent(智能体),将任务作为普通用户消息提交,并等待完全停稳。它对 Session 执行 flush 后再汇总自身持有的持久化事件区间,将最后一条非空 assistant 文本写入 stdout,再经启动器提供的 `ctx.appExit` 宿主钩子([`dsh-cmdline`](../../boot/cmdline/README.zh.md))请求退出(最终 `turn/end` 完成 → 0,否则为 1)。最终结束原因为 `error` 时,还会将 code 与 message 写入 stderr;成功运行时 stderr 保持为空。进程不会打开监听端口。任务文本就是这个应用的命令行:普通 `headless-startup` 提供方([`src/startup.ts`](src/startup.ts))注入 `ctx.cmdlineArgs`([`dsh-cmdline`](../../boot/cmdline/README.zh.md)),读取 `dsh --profile headless "task"` 的位置参数、打印应用自己的 `--help`,并提供 `headlessStartup`;runner 注入该服务,再从惰性配置中读取任务。缺失或只有空白的任务会在 runner 激活前被拒绝。
 

+ 0 - 5
packages/bundle/headless/cordis.patch.yml

@@ -9,11 +9,6 @@
     persona: >-
       You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.
 
-# The shared module-reload HMR row stays off. This startup profile freezes
-# every patch layer after boot; a later config change applies to the next run.
-- id: hmr
-  disabled: true
-
 - id: tools
   config:
     # Keep the same temporary process-wide Code Mode opt-in as the Web surface.

+ 2 - 2
packages/bundle/sdk-app/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-app/README.md
-README.md: c6f24571f3c7afc9d002ca36873cc9e2660f37c5
-README.zh.md: 62dc11238c2f738eaee396795a0953316569a4f6
+README.md: 0356d6f4a99d7baef6ff7619d505392ff7f7f1d2
+README.zh.md: c70eb685954ebff42bca6c3d289ab58e46298d50

+ 1 - 1
packages/bundle/sdk-app/README.md

@@ -2,7 +2,7 @@
 
 English | [中文](README.zh.md)
 
-The SDK stdio application as a `dsh` profile bundle over [`dsh-base`](../base/README.md). Its patch sets the coding-agent persona, disables module HMR, mounts an app-owned zero-option command provider, and starts [`dsh-sdk-jsonrpc-server`](../../sdk/server/README.md) only after that provider accepts the invocation. `dsh --profile sdk --help` therefore writes help and exits without claiming stdin or stdout.
+The SDK stdio application as a `dsh` profile bundle over [`dsh-base`](../base/README.md). It inherits the base's disabled module-HMR policy; its patch sets the coding-agent persona, mounts an app-owned zero-option command provider, and starts [`dsh-sdk-jsonrpc-server`](../../sdk/server/README.md) only after that provider accepts the invocation. `dsh --profile sdk --help` therefore writes help and exits without claiming stdin or stdout.
 
 The startup provider binds stdin EOF to the launcher's bounded successful shutdown. SDK protocol `shutdown`, SIGINT, and SIGTERM retain their owning server or launcher paths; disposal drains the root profile tree and persistence. Stdout is reserved for newline-delimited JSON-RPC frames. The bundle disables model-generated session titles because the SDK exposes no title surface; deterministic fallback titles remain durable without an auxiliary model request. A deployment selects a different complete composition through profile bundles and patch files, not another app bin.
 

+ 1 - 1
packages/bundle/sdk-app/README.zh.md

@@ -2,7 +2,7 @@
 
 [English](README.md) | 中文
 
-以 [`dsh-base`](../base/README.zh.md) 为基础的 SDK stdio 应用 `dsh` profile 组合包。其 patch 设置 coding agent(编程智能体)persona、禁用模块 HMR(热模块替换)、挂载应用自有的零选项命令提供方,并且只在该提供方接受调用后启动 [`dsh-sdk-jsonrpc-server`](../../sdk/server/README.zh.md)。因此,`dsh --profile sdk --help` 会写出 help 并退出,不会占用 stdin 或 stdout。
+以 [`dsh-base`](../base/README.zh.md) 为基础的 SDK stdio 应用 `dsh` profile 组合包。它继承 base 默认禁用模块 HMR(热模块替换)的策略;其 patch 设置 coding agent(编程智能体)persona、挂载应用自有的零选项命令提供方,并且只在该提供方接受调用后启动 [`dsh-sdk-jsonrpc-server`](../../sdk/server/README.zh.md)。因此,`dsh --profile sdk --help` 会写出 help 并退出,不会占用 stdin 或 stdout。
 
 启动提供方把 stdin EOF 接到启动器的有界成功关闭流程。SDK 协议 `shutdown`、SIGINT 与 SIGTERM 继续使用各自所属的 server 或启动器路径;dispose(资源释放)会排空根 profile 配置树与持久化。stdout 专用于按换行分隔的 JSON-RPC 帧。SDK 不提供 title 表层,因此本组合包禁用模型生成的 session title;确定性的 fallback title 仍会持久化,但不发起辅助模型请求。部署通过 profile 组合包与 patch 文件选择另一套完整组合,而不是使用另一个应用 bin。
 

+ 0 - 3
packages/bundle/sdk-app/cordis.patch.yml

@@ -5,9 +5,6 @@
     persona: >-
       You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.
 
-- id: hmr
-  disabled: true
-
 - id: session-title-llm
   disabled: true
 

+ 2 - 2
packages/bundle/sdk-app/tests/sdk-app.spec.ts

@@ -8,7 +8,7 @@ import { describe, expect, it } from 'vitest'
 import { entryListSchema } from '@deepseek-ai/cordis-plugin-include'
 
 describe('dsh-sdk-app bundle', () => {
-  it('declares startup-gated JSON-RPC serving with module HMR disabled', () => {
+  it('declares startup-gated JSON-RPC serving without overriding base HMR policy', () => {
     const root = fileURLToPath(new URL('..', import.meta.url))
     const manifest = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')) as {
       dependencies?: Record<string, string>
@@ -20,7 +20,7 @@ describe('dsh-sdk-app bundle', () => {
       readFileSync(resolve(root, manifest.dsh!.bundle!.patch!), 'utf8'),
       { schema: entryListSchema },
     ) as Array<{ id?: string; disabled?: boolean; insert?: Array<{ id?: string; inject?: string[]; name?: string }> }>
-    expect(patches.find(patch => patch.id === 'hmr')).toMatchObject({ disabled: true })
+    expect(patches.find(patch => patch.id === 'hmr')).toBeUndefined()
     expect(patches.find(patch => patch.id === 'session-title-llm')).toMatchObject({ disabled: true })
     const rows = patches.flatMap(patch => patch.insert ?? [])
     expect(rows.find(row => row.id === 'sdk-app-startup')?.name).toBe('@deepseek-ai/dsh-sdk-app')

+ 2 - 2
packages/bundle/web-app/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/web-app/README.md
-README.md: c8a6874bc01696fc7c9ca65faf772da81ac1e964
-README.zh.md: cb58177daa9eb166e29e4169409bbc6a558437e0
+README.md: 4092cf4fd2985027f3c7e59909f58a5dc1ef4244
+README.zh.md: 5c0f3a109b8d5261ab2a5e2ae0219cb06d493c04

File diff ditekan karena terlalu besar
+ 0 - 0
packages/bundle/web-app/README.md


File diff ditekan karena terlalu besar
+ 0 - 0
packages/bundle/web-app/README.zh.md


+ 0 - 4
packages/bundle/web-app/cordis.patch.yml

@@ -18,10 +18,6 @@
     persona: >-
       You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.
 
-# TODO: Re-enable shared HMR for Web after its reload lifecycle is tested.
-- id: hmr
-  disabled: true
-
 # Full-text session search is opt-in (the base row's `openAt: never`). This
 # restatement keeps the Web values on one ephemeral in-memory index; a
 # deployment enabling content search overrides `openAt` to `first-search` in a

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini