fix(schedule-bundle): correct review findings on row semantics and the archived note

Cross-link the archived decision the opt-in note supersedes, state the Loader's one
entry per id and last-declaration-wins behavior in the overlay and extraction
alternatives, scope the missing-row warning to profiles that mount no Plugins page,
and move the switch-on-but-ineffective case to the list-order paragraph. Assert one
top-level composed row per id and correct the stale assembled-remote comment.
This commit is contained in:
Chinesezjc committed 2026-09-25 15:12:14 +08:00
1 parent 1145adee07
commit 1065f0afd1
11 files changed
+35 -27

No files matched your search

@@ -15,5 +15,5 @@
en: a65d84004e48b3ef
zh: 17221f08e7a8a89b
/agent-note-ship-experimental-capabilities-as-optional-bundles/consequences:
en: 71482af50ad31d54
zh: 12d4849104842f0d
en: e7fb2a93a89ba9b6
zh: 37ed9f0a3ca44583
@@ -22,4 +22,4 @@ An optional bundle is a runtime dependency of the installation, so its dependenc
## Consequences
The Official group grows from two to four entries, each tagged experimental by its experimental name. The runtime dependency closure of the installation is unchanged. A browser-use or computer-use provider still needs its Service Definition and the provider in a profile patch or composition.
The Official group carries four optional-bundle entries, each tagged experimental by its experimental name. The runtime dependency closure of the installation is unchanged. A browser-use or computer-use provider still needs its Service Definition and the provider in a profile patch or composition.
@@ -22,4 +22,4 @@ Web 插件页只提供两个可选 bundle:Agent Teams 与语音输入。Auto r
## 后果
官方分组从两项增至四项,每项都因实验包名而带实验性标签。安装的运行时依赖闭包保持不变。浏览器操作或电脑操作提供方仍需在 profile patch 或组合中同时挂载其 Service Definition 与提供方。
官方分组包含四个可选 bundle 条目,每项都因实验包名而带实验性标签。安装的运行时依赖闭包保持不变。浏览器操作或电脑操作提供方仍需在 profile patch 或组合中同时挂载其 Service Definition 与提供方。
@@ -3,8 +3,8 @@
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-24-schedule-opt-in-optional-bundle.md
/agent-note-schedule-as-an-opt-in-optional-bundle:
en: 8c2381fd778a9888
zh: 8877255cb980eea1
en: d234bf064c5990f1
zh: c54b941429b4658c
/agent-note-schedule-as-an-opt-in-optional-bundle/problem:
en: 2409ea3a006ea5d9
zh: dbcda58a4c6b5b9f
@@ -12,8 +12,8 @@
en: 360867ff77c6b727
zh: 4eb4b47439c21d5d
/agent-note-schedule-as-an-opt-in-optional-bundle/alternatives-considered:
en: d12affa9097a1a50
zh: 4190d195c6a6eb8d
en: cef562365cc8ab3a
zh: 2d43ba5839b97474
/agent-note-schedule-as-an-opt-in-optional-bundle/consequences:
en: 8ea1d9aaf25f1702
zh: 32e721347ea55475
@@ -4,6 +4,8 @@ Status: implemented
English | [中文](2026-09-24-schedule-opt-in-optional-bundle.zh.md)
> Supersedes [Schedule in the shipped Web composition](../../archived/architecture/2026-09-24-web-default-schedule-composition.md): the three rows now ship disabled, and switching them on is this bundle's job.
## Problem
`packages/bundle/web-app/cordis.patch.yml` inserts `time-context`, `schedule`, and `ui-schedule` with `disabled: true`, so the shipped Web composition exposes no Schedule surface: no `schedule_*` tools, no Session reminder catalog, no Automation tasks page, and no per-step clock reading. The rows have no switch on the product surface; a deployment that wants them edits a profile patch layer or passes a `--patch` overlay, and both are configuration files a person using the shipped Web profile cannot reach.
@@ -20,9 +22,9 @@ Enabling the bundle turns on `time-context` (a per-step clock reading with the s
**Ship Schedule on by default.** Every Web session then pays four tool schemas in each request header and one durable user message per eligible step, and a conversation that never creates a reminder pays both costs. The arrangement also left the capability without an off switch on the product surface.
**Keep a `--patch` overlay file.** An overlay is a launch-time argument rather than a product-surface switch, so a person using the shipped Web profile cannot reach it. It cannot re-insert the rows either: `applyEntryPatches` appends an `insert` list without de-duplicating ids, so an overlay that inserts `time-context` and `schedule` mounts each Host plugin a second time.
**Keep a `--patch` overlay file.** An overlay is a launch-time argument rather than a product-surface switch, so a person using the shipped Web profile cannot reach it. It also cannot set fields on the shipped rows: `applyEntryPatches` appends an `insert` list without de-duplicating ids, and the Loader keeps one entry per id with the last declaration winning, so an overlay that re-declares `time-context` and `schedule` replaces the Web bundle's rows instead of overriding them.
**Extract the three rows out of the Web composition into the bundle.** The bundle would insert the rows instead of overriding them, so any profile that also inserts a row would mount the same Host plugin twice for the same reason. The disabled rows are what an override-style switch targets.
**Extract the three rows out of the Web composition into the bundle.** The bundle would insert the rows instead of overriding them, so a profile that also declares one of those ids would have two declarations for it, of which the Loader keeps only the last. The disabled rows are what an override-style switch targets.
## Consequences
@@ -4,6 +4,8 @@ Status: implemented
[English](2026-09-24-schedule-opt-in-optional-bundle.md) | 中文
> 取代[默认 Web 组合中的 Schedule](../../archived/architecture/2026-09-24-web-default-schedule-composition.md):这三行现在默认禁用,打开它们是此 bundle 的职责。
## Problem
`packages/bundle/web-app/cordis.patch.yml` 以 `disabled: true` 插入 `time-context`、`schedule` 与 `ui-schedule`,因此随发行版交付的 Web 组合不提供任何定时能力:没有 `schedule_*` 工具、没有 Session 提醒目录、没有自动化任务页面,也没有逐步时钟读数。这些条目在产品界面上没有开关;想要它们的部署要改 profile patch 层或传 `--patch` overlay,而两者都是使用随发行版 Web profile 的人无法触及的配置文件。
@@ -20,9 +22,9 @@ Status: implemented
**默认发布 Schedule。** 那样每个 Web 会话都要在每个请求头中带上四个工具 schema,并在每个符合条件的步骤追加一条持久 user 消息,从不创建提醒的对话也要承担这两项成本。这种安排还让该能力在产品界面上没有关闭开关。
**保留一个 `--patch` overlay 文件。** overlay 是启动时的参数,而不是产品界面上的开关,因此使用随发行版 `web` profile 的人无法触及它。它也无法重新插入这些行:`applyEntryPatches` 追加 `insert` 列表时不对 id 去重,插入 `time-context` 与 `schedule` 的 overlay 会把每个 Host 插件挂载两次。
**保留一个 `--patch` overlay 文件。** overlay 是启动时的参数,而不是产品界面上的开关,因此使用随发行版 `web` profile 的人无法触及它。它也无法给出厂条目设值:`applyEntryPatches` 追加 `insert` 列表时不对 id 去重,而 Loader 每个 id 只保留一个条目、最后一个声明生效,因此重新声明 `time-context` 与 `schedule` 的 overlay 会替换 Web Bundle 的条目,而不是覆盖它们。
**把这三行从 Web 组合中抽出、移入 bundle。** 这样 bundle 会插入这些行而不是覆盖它们,任何同样插入某行的 profile 都会因同样的原因把同一个 Host 插件挂载两次。按需开关针对的正是这些禁用行。
**把这三行从 Web 组合中抽出、移入 bundle。** 这样 bundle 会插入这些行而不是覆盖它们;某个 profile 若也声明了这些 id,同一个 id 就会有两份声明,而 Loader 只保留最后一份。按需开关针对的正是这些禁用行。
## Consequences
+3 -2
View File
@@ -223,8 +223,9 @@ export function createAssembledRemote(options: AssembledRemoteOptions = {}): Ass
return ok(undefined)
})
mock.unary('session/list', () => ok({ items: structuredClone(sessions) }))
// The shipped Web composition mounts the Schedule client, whose catalog and
// per-Session list read empty here: these scenarios drive no reminder.
// The shipped bundle layers this graph composes carry the `ui-schedule` row
// disabled, so the Schedule client does not mount and no case here reads the
// catalog or the per-Session list; both stay declared empty.
mock.unary('schedule/catalog', () => ok([]))
mock.unary('schedule/list', () => ok([]))
mock.unary('session/projections', (request: unknown) => {
@@ -18,8 +18,8 @@
en: ffaa4b18e10a1df9
zh: b675fd8195ebb057
/deepseek-ai-dsh-experimental-schedule-bundle/understand-the-implementation:
en: 744024d71c8fca24
zh: 22f939f9484e1ad7
en: b17882af7c7eaf2c
zh: bfc71d0152557af5
/deepseek-ai-dsh-experimental-schedule-bundle/further-exploration:
en: 99a29dddf9805540
zh: 13a6df3ee31abfc1
@@ -39,8 +39,8 @@
en: 0b01d8c40b422f5b
zh: 57fede4db59a62b8
/deepseek-ai-dsh-experimental-schedule-bundle/known-limitations-and-deferred-work:
en: 6f2812ce847b2654
zh: ac8ef109662f67c0
en: a4a2674fb3c5a101
zh: cb4c54ba37730708
/deepseek-ai-dsh-experimental-schedule-bundle/known-limitations-and-deferred-work/dev-note:
en: 0225ba087c5266f3
zh: 4e706f1b2b984e63
@@ -35,7 +35,7 @@ Open Plugins in the Web sidebar and enable Scheduling and time context, marked b
<details>
<summary>Maintainer details — click to expand</summary>
`cordis.patch.yml` carries three id-targeted patches, each setting `disabled: false` on a row the Web bundle already inserts. `OPTIONAL_BUNDLES` in `packages/boot/app-boot/src/profile.ts` names this package and `apps/cli` depends on it, so every installation ships it switched off and the plugin manager offers it in the Official group. Selecting it appends the bundle to the tail of the profile's `dsh.profile.bundles` list; bundle layers apply in list order, so the patches reach the rows only because that list places `@deepseek-ai/dsh-web-app` before this package. No runtime invariant companion is published because this configuration-only package owns no mutable runtime state.
`cordis.patch.yml` carries three id-targeted patches, each setting `disabled: false` on a row the Web bundle already inserts. `OPTIONAL_BUNDLES` in `packages/boot/app-boot/src/profile.ts` names this package and `apps/cli` depends on it, so every installation ships it switched off and the plugin manager offers it in the Official group. Selecting it appends the bundle to the tail of the profile's `dsh.profile.bundles` list; bundle layers apply in list order, so the patches reach the rows only because that list places `@deepseek-ai/dsh-web-app` before this package. A hand-ordered list with this package first leaves the rows disabled while the Plugins page still shows the switch as on. No runtime invariant companion is published because this configuration-only package owns no mutable runtime state.
| File | Role |
|---|---|
@@ -78,8 +78,8 @@ The tool schemas change the request prefix once when the bundle mounts; each app
<a id="known-limitations-and-deferred-work"></a>
- The switch targets rows `@deepseek-ai/dsh-web-app` inserts. In a profile without that bundle the three patches match no row: the loader reports one `patch: entry <id> not found` warning per row and skips it, while the Plugins page still shows the switch as on.
- The bundle's detail page has no per-row switches. The Host reports the three overridden ids under `overrides`, and the Web client renders only the rows a bundle inserts, so opening this card shows "This plugin pack contains no components." The switches for the three rows are on the Official-group list row.
- The switch targets rows `@deepseek-ai/dsh-web-app` inserts. In a profile without that bundle the three patches match no row: the loader reports one `patch: entry <id> not found` warning per row and mounts nothing from this bundle; such a profile mounts no Plugins page either.
- The bundle's detail page has no per-row switches. The Host reports the three overridden ids under `overrides`, and the Web client renders only the rows a bundle inserts, so opening this card shows "This plugin pack contains no components." The switch for the three rows is on the Official-group list row.
-----
@@ -35,7 +35,7 @@ kind: "package-bundle"
<details>
<summary>维护者信息 — 点击展开</summary>
`cordis.patch.yml` 包含三个按 id 定位的补丁,各自把 Web Bundle 已插入条目的 `disabled` 设为 `false`。`packages/boot/app-boot/src/profile.ts` 的 `OPTIONAL_BUNDLES` 列出此包,`apps/cli` 依赖它,因此每次安装都随包携带且默认禁用,插件管理页在 Official 分组中提供它。选中后会把该 Bundle 追加到 profile 的 `dsh.profile.bundles` 列表末尾;Bundle 层按列表顺序应用,因此这些补丁能命中条目,只是因为该列表把 `@deepseek-ai/dsh-web-app` 排在此包之前。此纯配置包不拥有可变的运行时状态,因此不发布不变量伴随模块。
`cordis.patch.yml` 包含三个按 id 定位的补丁,各自把 Web Bundle 已插入条目的 `disabled` 设为 `false`。`packages/boot/app-boot/src/profile.ts` 的 `OPTIONAL_BUNDLES` 列出此包,`apps/cli` 依赖它,因此每次安装都随包携带且默认禁用,插件管理页在 Official 分组中提供它。选中后会把该 Bundle 追加到 profile 的 `dsh.profile.bundles` 列表末尾;Bundle 层按列表顺序应用,因此这些补丁能命中条目,只是因为该列表把 `@deepseek-ai/dsh-web-app` 排在此包之前。手写顺序把此包排在最前时,这些条目保持禁用,而插件管理页仍把该开关显示为已启用。此纯配置包不拥有可变的运行时状态,因此不发布不变量伴随模块。
| 文件 | 作用 |
|---|---|
@@ -78,8 +78,8 @@ kind: "package-bundle"
<a id="known-limitations-and-deferred-work"></a>
- 此开关针对 `@deepseek-ai/dsh-web-app` 插入的条目。在没有该 Bundle 的 profile 中,三个补丁匹配不到任何条目:加载器为每个条目报告一条 `patch: entry <id> not found` 警告并跳过它,而插件管理页仍把该开关显示为已启用。
- 该 Bundle 的详情页没有逐条目开关。Host 把被覆盖的三个 id 报告在 `overrides` 下,而 Web 客户端只渲染 Bundle 插入的条目,因此打开这张卡片会显示“这个插件包不包含任何组件。”。三个条目的开关在 Official 分组列表行上。
- 此开关针对 `@deepseek-ai/dsh-web-app` 插入的条目。在没有该 Bundle 的 profile 中,三个补丁匹配不到任何条目:加载器为每个条目报告一条 `patch: entry <id> not found` 警告,且不从该 Bundle 挂载任何东西;这样的 profile 也不挂载插件管理页。
- 该 Bundle 的详情页没有逐条目开关。Host 把被覆盖的三个 id 报告在 `overrides` 下,而 Web 客户端只渲染 Bundle 插入的条目,因此打开这张卡片会显示“这个插件包不包含任何组件。”。控制这三个条目的开关在 Official 分组列表行上。
-----
+6 -3
View File
@@ -49,13 +49,16 @@ describe('optional bundles', () => {
expect(typeof row.id).toBe('string')
expect(ids.has(row.id)).toBe(true)
}
// An id-targeted patch reaches a row another layer inserted: the composed row keeps that row's package
// name, appears once, and carries the patched values.
// One top-level row per id: a duplicate declaration leaves the Loader with the last one, silently
// replacing the layer that declared the id first.
const topLevelIds = composed.flatMap(entry => typeof entry.id === 'string' ? [entry.id] : [])
expect(topLevelIds).toHaveLength(new Set(topLevelIds).size)
// An id-targeted patch reaches a row another layer inserted: the id resolves to exactly one top-level
// row that keeps its declared package name.
for (const patch of patches) {
if (patch.insert !== undefined || typeof patch.id !== 'string') continue
const matches = composed.filter(entry => entry.id === patch.id)
expect(matches).toHaveLength(1)
expect(matches[0]).toMatchObject({ ...patch })
expect(typeof matches[0]?.name).toBe('string')
}
})