diff --git a/.agents/notes/implemented/architecture/2026-08-11-cmdline-seam-trim.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-11-cmdline-seam-trim.i18n.yaml index 5780c5196a..d763257134 100644 --- a/.agents/notes/implemented/architecture/2026-08-11-cmdline-seam-trim.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-11-cmdline-seam-trim.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-11-cmdline-seam-trim.md -2026-08-11-cmdline-seam-trim.md: e9d30c94baed0e0e76c36d7561f50353a4b3eace -2026-08-11-cmdline-seam-trim.zh.md: c4ee26d25b72b4d77d6ec2affbb4647e08c7cb1a +2026-08-11-cmdline-seam-trim.md: 3fb2f3e0941ad4cf6e2fb7e1afbe6cf31af92f41 +2026-08-11-cmdline-seam-trim.zh.md: 002a76fc57e1d26e15c2619a380874a0eecd435f diff --git a/.agents/notes/implemented/architecture/2026-08-11-cmdline-seam-trim.md b/.agents/notes/implemented/architecture/2026-08-11-cmdline-seam-trim.md index e9d30c94ba..3fb2f3e094 100644 --- a/.agents/notes/implemented/architecture/2026-08-11-cmdline-seam-trim.md +++ b/.agents/notes/implemented/architecture/2026-08-11-cmdline-seam-trim.md @@ -12,7 +12,7 @@ The app-owned command line ([note](2026-08-06-app-owned-command-line.md)) shippe Express all three with interfaces that already exist: -- **Conditional dev row.** `dsh-web-app` no longer ships a disabled `client-hmr` row; in development mode its runtime plugin creates the row in the root tree after Loader settlement with plain `loader.create`, guarded for reload idempotence. A root-tree row is outside the include, so user-patch reapplication cannot restore it to disabled — the property the in-memory override existed for. The incremental client-module scan adds it to the roster before any page loads; a browser arrives only after a human reads the URL line, and its `EventSource` reconnects by spec. `Entry.enableRuntime`, its two state fields, and `enableRow` are deleted. +- **Conditional dev row.** `dsh-web-app` no longer ships a disabled `client-hmr` row; in development mode its runtime plugin creates the row in the root tree after Loader settlement with plain `loader.create`; a whole-tree name scan makes the creation reload-idempotent and defers to a user-configured `dsh-client-hmr` row (even a disabled one). A root-tree row is outside the include, so user-patch reapplication cannot restore it to disabled — the property the in-memory override existed for. The incremental client-module scan adds it to the roster before any page loads; a browser arrives only after a human reads the URL line, and its `EventSource` reconnects by spec. `Entry.enableRuntime`, its two state fields, and `enableRow` are deleted. - **Tree-carrier config.** Include declares the existing `EntryGroup.key` marker instead of implementing `EntryConfigResolver`; the Loader hook keeps every tree carrier's config literal. Include's own `path` loses `!!js` support — no configuration ever used it, and the pinning test now asserts the literal tree-carrier contract instead. - **Launcher app-knowledge.** The launcher recognizes no app row. SIGTERM is a supervisor's ordinary stop request and exits 0 on every surface (SIGINT stays 130); the launcher cannot know whether the app considered its work complete, and the previous 143 depended on naming the headless row. Every boot watches its user patch layers — a one-shot surface exits through bounded shutdown, which disposes the watchers before the loop drains. The headless runner exits through `ctx.appExit` like any other app; its output streams are a package-internal `internals` test seam, and `ctx.headlessIo` is deleted. diff --git a/.agents/notes/implemented/architecture/2026-08-11-cmdline-seam-trim.zh.md b/.agents/notes/implemented/architecture/2026-08-11-cmdline-seam-trim.zh.md index c4ee26d25b..002a76fc57 100644 --- a/.agents/notes/implemented/architecture/2026-08-11-cmdline-seam-trim.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-11-cmdline-seam-trim.zh.md @@ -12,7 +12,7 @@ Status: implemented 三者全部改用已经存在的接口表达: -- **条件 dev 行。** `dsh-web-app` 不再随附禁用的 `client-hmr` 行;开发模式下其 runtime 插件在 Loader 结算后用普通的 `loader.create` 在根树中创建该行,并带重载幂等保护。根树的行在 include 之外,用户 patch 的重新应用无法把它恢复为禁用 —— 这正是内存覆盖机制存在的理由。增量式客户端模块扫描会在任何页面加载之前把它加入名录;浏览器只会在人读到 URL 行之后到来,其 `EventSource` 按规范自动重连。`Entry.enableRuntime`、它的两个状态字段和 `enableRow` 一并删除。 +- **条件 dev 行。** `dsh-web-app` 不再随附禁用的 `client-hmr` 行;开发模式下其 runtime 插件在 Loader 结算后用普通的 `loader.create` 在根树中创建该行;全树名称扫描让创建具备重载幂等性,并让位于用户自行配置的 `dsh-client-hmr` 行(即便该行被禁用)。根树的行在 include 之外,用户 patch 的重新应用无法把它恢复为禁用 —— 这正是内存覆盖机制存在的理由。增量式客户端模块扫描会在任何页面加载之前把它加入名录;浏览器只会在人读到 URL 行之后到来,其 `EventSource` 按规范自动重连。`Entry.enableRuntime`、它的两个状态字段和 `enableRow` 一并删除。 - **树载体配置。** Include 改为声明已有的 `EntryGroup.key` 标记,不再实现 `EntryConfigResolver`;Loader 钩子让每个树载体的配置保持字面值。Include 自己的 `path` 失去 `!!js` 支持 —— 从未有配置用过它,固定该行为的测试改为断言字面值树载体约定。 - **启动器的应用知识。** 启动器不再识别任何应用行。SIGTERM 是监督进程的普通停止请求,在所有 surface 上以 0 退出(SIGINT 仍为 130);启动器无从知道应用是否认为工作已完成,而之前的 143 依赖于点名 headless 行。每次启动都监视用户 patch 层 —— 一次性 surface 经由有界关闭退出,关闭会先 dispose 监视器再排空事件循环。headless runner 像任何应用一样经 `ctx.appExit` 退出;其输出流是包内 `internals` 测试接缝,`ctx.headlessIo` 删除。 diff --git a/packages/bundle/web-app/src/index.ts b/packages/bundle/web-app/src/index.ts index fd72ad4e6c..dcac201ba0 100644 --- a/packages/bundle/web-app/src/index.ts +++ b/packages/bundle/web-app/src/index.ts @@ -160,8 +160,11 @@ export function apply(ctx: Context, config: Config): void { } else { void loader.await().then(async () => { // The tree can be disposed while settlement was in flight (early - // SIGTERM); re-check before mutating it. A reload of this fiber must - // not duplicate the row a previous generation created. + // SIGTERM); re-check before mutating it. The name scan spans every + // tree (entries() recurses into subtrees), so a row the user + // configured in a patch layer — enabled, reconfigured, or + // deliberately disabled — wins over this default, and a reload of + // this fiber never duplicates the row a previous generation created. if (ctx.get('loader') === undefined) return const mounted = [...ctx.loader.entries()].some(entry => entry.options.name === HMR_ROW_NAME) if (!mounted) await ctx.loader.create({ name: HMR_ROW_NAME }) diff --git a/packages/bundle/web-app/tests/web-app.spec.ts b/packages/bundle/web-app/tests/web-app.spec.ts index 59286a07ff..272ae54258 100644 --- a/packages/bundle/web-app/tests/web-app.spec.ts +++ b/packages/bundle/web-app/tests/web-app.spec.ts @@ -187,6 +187,28 @@ describe('web-app runtime glue', () => { await ctx.fiber.dispose() }) + it('defers to a user-configured client-hmr row anywhere in the tree', async () => { + stageDist() + const ctx = new Context() + ctx.provide('httpServer', fakeHttpServer().server) + const created: string[] = [] + // The user's own row — possibly patched into an include subtree and even + // disabled there — already carries the name; the runtime must not create + // a second one beside it. + ctx.provide('loader', { + entries: () => [{ options: { id: 'my-hmr', name: '@deepseek-ai/dsh-client-hmr', disabled: true } }][Symbol.iterator](), + create: (options: { name: string }) => { + created.push(options.name) + return Promise.resolve(options.name) + }, + await: () => Promise.resolve(), + } as never) + apply(ctx, new Config({ mode: 'development', printUrl: false, surfaceContext: false, trustedHosts: [] })) + await new Promise(resolve => setTimeout(resolve, 0)) + expect(created).toEqual([]) + await ctx.fiber.dispose() + }) + it('skips the dev row when the tree is disposed during settlement and logs a creation failure', async () => { stageDist() const raced = new Context()