diff --git a/.agents/notes/implemented/architecture/2026-10-05-official-on-demand-bundles.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-05-official-on-demand-bundles.i18n.yaml index 080505215a..3060e831f9 100644 --- a/.agents/notes/implemented/architecture/2026-10-05-official-on-demand-bundles.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-10-05-official-on-demand-bundles.i18n.yaml @@ -9,11 +9,11 @@ en: a590bcef6c9760c0 zh: 19e6f61c2c7d8d3b /agent-note-discover-official-bundles-without-installing-their-runtimes/decision: - en: 3a3dbad398a418be - zh: 9ef707e746cdfd94 + en: f0fabc0d4fbd1178 + zh: cf19938a7a7dea93 /agent-note-discover-official-bundles-without-installing-their-runtimes/alternatives-considered: en: 42cb0f808a145871 zh: 225012615d1ecdf5 /agent-note-discover-official-bundles-without-installing-their-runtimes/consequences: - en: 899b07d90024e019 - zh: 01d22ac4b2444feb + en: 293286dcd7e3325e + zh: c6c15212e85d050b diff --git a/.agents/notes/implemented/architecture/2026-10-05-official-on-demand-bundles.md b/.agents/notes/implemented/architecture/2026-10-05-official-on-demand-bundles.md index ded1378dcd..a67acd25f9 100644 --- a/.agents/notes/implemented/architecture/2026-10-05-official-on-demand-bundles.md +++ b/.agents/notes/implemented/architecture/2026-10-05-official-on-demand-bundles.md @@ -18,7 +18,7 @@ The running installation owns the target. Published applications offer the exact Every Official package remains in the DSH release family. Packed DSH dependencies, optional dependencies, and peers use the exact family version. Publication prerequisites place catalog packages before the DSH package advertising them without adding their runtimes to DSH production dependencies. Catalog verification rejects missing metadata, unsupported membership, and stale generated content. -The [shipped optional-bundle policy](../process/2026-09-15-shipped-optional-bundles.md) still owns lightweight installation-provided bundles. The [optional-composition decision](2026-09-21-experimental-capabilities-as-optional-bundles.md) owns shared patch and preset-generation behavior. Native bundles require the full Web preset targets; custom profiles mount their providers and tools explicitly. The [identity compatibility decision](2026-10-06-web-desktop-plugin-identity-compatibility.md) owns retained names, selectors, and deferred naming cleanup. +The [shipped optional-bundle policy](../process/2026-09-15-shipped-optional-bundles.md) still owns lightweight installation-provided bundles. The [optional-composition decision](2026-09-21-experimental-capabilities-as-optional-bundles.md) owns shared patch behavior and global optional tools. Native bundles insert ordinary Host rows, so any profile can select them; profiles that keep the package without its bundle mount the same rows explicitly. The [identity compatibility decision](2026-10-06-web-desktop-plugin-identity-compatibility.md) owns retained names, selectors, and deferred naming cleanup. ## Alternatives considered @@ -34,4 +34,4 @@ The [shipped optional-bundle policy](../process/2026-09-15-shipped-optional-bund ## Consequences -Registry delivery requires a reachable registry; development delivery requires a prepared, built checkout and normal rebuild/restart discipline. Equal source version strings do not certify identical build outputs. Off only deselects; Remove remains separate and respects retained preset modules. Activation failure can retain the saved dependency and selection; there is no rollback. Offline metadata, release pins, source-target selection, same-version source mismatches, clean installed artifacts, and release ordering have dedicated checks. Native-provider authentication is not part of catalog discovery. +Registry delivery requires a reachable registry; development delivery requires a prepared, built checkout and normal rebuild/restart discipline. Equal source version strings do not certify identical build outputs. Off only deselects; Remove remains separate and refuses while the bundle's rows are in use. Activation failure can retain the saved dependency and selection; there is no rollback. Offline metadata, release pins, source-target selection, same-version source mismatches, clean installed artifacts, and release ordering have dedicated checks. Native-provider authentication is not part of catalog discovery. diff --git a/.agents/notes/implemented/architecture/2026-10-05-official-on-demand-bundles.zh.md b/.agents/notes/implemented/architecture/2026-10-05-official-on-demand-bundles.zh.md index 0404dd4f7c..0949f6b72c 100644 --- a/.agents/notes/implemented/architecture/2026-10-05-official-on-demand-bundles.zh.md +++ b/.agents/notes/implemented/architecture/2026-10-05-official-on-demand-bundles.zh.md @@ -18,7 +18,7 @@ Status: implemented 所有 Official 包仍属于 DSH 发布家族。打包后的 DSH 依赖、可选依赖与 peer 均使用家族精确版本。发布前置关系要求先发布目录包,再发布宣传它们的 DSH 包,同时不把这些运行时加入 DSH 生产依赖。目录校验拒绝缺失的元信息、不受支持的成员和过期生成内容。 -[随附可选 bundle 策略](../process/2026-09-15-shipped-optional-bundles.zh.md)仍管理安装随附的轻量 bundle。[可选组合决策](2026-09-21-experimental-capabilities-as-optional-bundles.zh.md)管理共享 patch 与预设代际行为。原生 bundle 要求完整 Web 预设目标存在;自定义 profile 显式挂载提供方和工具。[身份兼容决策](2026-10-06-web-desktop-plugin-identity-compatibility.zh.md)管理保留名称、选择器和暂缓的命名清理。 +[随附可选 bundle 策略](../process/2026-09-15-shipped-optional-bundles.zh.md)仍管理安装随附的轻量 bundle。[可选组合决策](2026-09-21-experimental-capabilities-as-optional-bundles.zh.md)管理共享 patch 行为与全局可选工具。原生 bundle 插入普通 Host 行,因此任何 profile 都可以选择它们;保留包但不选择其 bundle 的 profile 显式挂载相同的行。[身份兼容决策](2026-10-06-web-desktop-plugin-identity-compatibility.zh.md)管理保留名称、选择器和暂缓的命名清理。 ## 考虑过的替代方案 @@ -34,4 +34,4 @@ Status: implemented ## 后果 -注册表交付要求注册表可达;开发交付要求准备并构建 checkout,遵循正常的重新构建和重启流程。源码版本字符串相同不证明构建输出相同。关闭仅取消选择;移除保持独立,并尊重预设仍保留的模块。激活失败可以保留已保存的依赖和选择,不加入回滚。离线元信息、发布版本锁定、源码目标选择、同版本来源差异、干净安装产物和发布顺序有专门检查。原生提供方认证不属于目录发现过程。 +注册表交付要求注册表可达;开发交付要求准备并构建 checkout,遵循正常的重新构建和重启流程。源码版本字符串相同不证明构建输出相同。关闭仅取消选择;移除保持独立,并在 bundle 的行仍在使用时拒绝执行。激活失败可以保留已保存的依赖和选择,不加入回滚。离线元信息、发布版本锁定、源码目标选择、同版本来源差异、干净安装产物和发布顺序有专门检查。原生提供方认证不属于目录发现过程。 diff --git a/.agents/notes/implemented/architecture/2026-10-06-web-desktop-plugin-identity-compatibility.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-06-web-desktop-plugin-identity-compatibility.i18n.yaml index 0a22e7a68f..b22fc768ec 100644 --- a/.agents/notes/implemented/architecture/2026-10-06-web-desktop-plugin-identity-compatibility.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-10-06-web-desktop-plugin-identity-compatibility.i18n.yaml @@ -15,8 +15,8 @@ en: 4c428f65ef611d07 zh: 1fdc5467b0203d08 /agent-note-preserve-web-and-desktop-plugin-identities-across-experimental-consolidation/alternatives-considered: - en: 1bf03723fea6c9a4 - zh: 2d0d2fc997f7c022 + en: 65af55b5775c9bc5 + zh: 2ea75b3fd33ac0c2 /agent-note-preserve-web-and-desktop-plugin-identities-across-experimental-consolidation/consequences: - en: 60f7c823b5d2ed6f - zh: 75f375487cab3c56 + en: e8e96b0354f1a3c6 + zh: ad1bfe3600e14ff1 diff --git a/.agents/notes/implemented/architecture/2026-10-06-web-desktop-plugin-identity-compatibility.md b/.agents/notes/implemented/architecture/2026-10-06-web-desktop-plugin-identity-compatibility.md index 945c36796b..c39f289a39 100644 --- a/.agents/notes/implemented/architecture/2026-10-06-web-desktop-plugin-identity-compatibility.md +++ b/.agents/notes/implemented/architecture/2026-10-06-web-desktop-plugin-identity-compatibility.md @@ -31,10 +31,10 @@ These naming changes are deferred, not prerequisites for experimental classifica **Publish aliases or keep duplicate compatibility rows.** Parallel identities need continuing resolution and removal rules and can register the same capability twice. Retaining the established identity avoids that additional mechanism. -**Keep provider-only bundles through additional wrapper packages.** Web and Desktop already provide the required presets. Extra delivery packages would preserve compositions outside the selected compatibility scope and increase catalog and release maintenance. +**Keep provider-only bundles through additional wrapper packages.** Native bundles insert ordinary Host rows that compose into any profile. Extra delivery packages would preserve compositions outside the selected compatibility scope and increase catalog and release maintenance. ## Consequences -The repository carries a bounded naming-exception map and tests that classify retained names through direct imports, aliases, transitive dependencies, and packed or bundled artifacts. An unprefixed name does not grant admission to the default product. Existing row identities have Web/Desktop composition coverage for user overrides, disabled state, and preset replacement. No general alias or automatic profile migration mechanism is introduced. +The repository carries a bounded naming-exception map and tests that classify retained names through direct imports, aliases, transitive dependencies, and packed or bundled artifacts. An unprefixed name does not grant admission to the default product. Existing row identities have Web/Desktop composition coverage for user overrides and disabled state. No general alias or automatic profile migration mechanism is introduced. The [classification decision](../process/2026-10-05-product-package-classification.md), [optional-composition decision](2026-09-21-experimental-capabilities-as-optional-bundles.md), and [on-demand discovery decision](2026-10-05-official-on-demand-bundles.md) retain their separate responsibilities. Released Session generations and persisted hook event names remain protected independently of this profile compatibility scope. diff --git a/.agents/notes/implemented/architecture/2026-10-06-web-desktop-plugin-identity-compatibility.zh.md b/.agents/notes/implemented/architecture/2026-10-06-web-desktop-plugin-identity-compatibility.zh.md index 36b6f8c19c..4b58743cb5 100644 --- a/.agents/notes/implemented/architecture/2026-10-06-web-desktop-plugin-identity-compatibility.zh.md +++ b/.agents/notes/implemented/architecture/2026-10-06-web-desktop-plugin-identity-compatibility.zh.md @@ -31,10 +31,10 @@ Status: implemented **发布别名或保留重复兼容行。** 并行身份需要持续维护解析和移除规则,还可能重复注册同一能力。保留既有身份可以避免增加这种机制。 -**通过额外包装包保留仅提供方 bundle。** Web 和 Desktop 已提供所需预设。额外交付包会保留选定兼容范围之外的组合,并增加目录和发布维护成本。 +**通过额外包装包保留仅提供方 bundle。** 原生 bundle 插入普通 Host 行,可组合到任何 profile。额外交付包会保留选定兼容范围之外的组合,并增加目录和发布维护成本。 ## 后果 -仓库维护范围明确的命名例外映射,并测试通过直接导入、别名、传递依赖及打包产物识别这些保留名称。没有实验前缀的名称不因此获准进入默认产品。既有行身份具有覆盖用户配置、禁用状态和预设替换的 Web/Desktop 组合测试。不引入通用别名或自动 profile 迁移机制。 +仓库维护范围明确的命名例外映射,并测试通过直接导入、别名、传递依赖及打包产物识别这些保留名称。没有实验前缀的名称不因此获准进入默认产品。既有行身份具有覆盖用户配置和禁用状态的 Web/Desktop 组合测试。不引入通用别名或自动 profile 迁移机制。 [分类决策](../process/2026-10-05-product-package-classification.zh.md)、[可选组合决策](2026-09-21-experimental-capabilities-as-optional-bundles.zh.md)和[按需发现决策](2026-10-05-official-on-demand-bundles.zh.md)继续承担各自独立的职责。已发布 Session 代际和持久化 hook 事件名的保护独立于此 profile 兼容范围。 diff --git a/docs/subsystems/session-title.md b/docs/subsystems/session-title.md index 699b0148a6..207af94795 100644 --- a/docs/subsystems/session-title.md +++ b/docs/subsystems/session-title.md @@ -193,7 +193,8 @@ async refresh(session: Session, signal?: AbortSignal): Promise item.insert !== undefined)]))) { - if (typeof row.id !== 'string' || typeof row.name !== 'string') continue + // Group rows are containers without a plugin inventory entry; their children carry the switches. + if (typeof row.id !== 'string' || typeof row.name !== 'string' || row.group === true) continue const active = live.get(row.id) const entryId = active?.entryId const base = active?.baseUrl ?? pathToFileURL(join(dir, 'package.json')).href diff --git a/packages/boot/plugin-manager/tests/manager.spec.ts b/packages/boot/plugin-manager/tests/manager.spec.ts index 52cd122dce..be914515fd 100644 --- a/packages/boot/plugin-manager/tests/manager.spec.ts +++ b/packages/boot/plugin-manager/tests/manager.spec.ts @@ -761,6 +761,7 @@ it('addresses children inside profile groups and marks ambiguous ids read-only', config: [{ id: 'child', name: './plugin.mjs', config: { service: 'child' } }] }]) expect(await manager.setBundleEnabled('grouped', true)).toMatchObject({ application: 'applied' }) expect((await manager.listPlugins()).find(row => row.patchId === 'child')).toBeDefined() + expect((await manager.listBundles()).find(item => item.name === 'grouped')?.rows.map(row => row.rowId)).toEqual(['child']) const entries = composeEntries([readProfilePatches('test', profile)]) const duplicate = entries.find(row => row.id === 'managed')! writeFileSync(profile.patchPath, JSON.stringify([{ insert: [duplicate] }])) diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index 31f7120764..192b8ba7dd 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -2522,7 +2522,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ }, { signature: 'register(provider: SessionTitleProvider): () => Promise', - description: 'Register the sole optional title provider. Disposal aborts its pending and active work before another provider may register.', + description: 'Register the sole optional title provider. Disposal aborts its pending and active work; a replacement may register once disposal has started, and the closing provider\'s late results never commit.', parameters: [{ name: 'provider', description: 'provider identity, cadence, and generation function.' }], returns: 'exact Cordis effect disposer, which settles after active calls quiesce.', }, diff --git a/packages/session/session-title/README.i18n.yaml b/packages/session/session-title/README.i18n.yaml index cfff372123..e184d12dc1 100644 --- a/packages/session/session-title/README.i18n.yaml +++ b/packages/session/session-title/README.i18n.yaml @@ -24,8 +24,8 @@ en: f72239b38a91783b zh: 07e22375c09906a6 /deepseek-ai-dsh-session-title/use-this-package/adding-a-provider: - en: 909209d339538963 - zh: fb6350642ec30cc1 + en: 2ed8217f95490c73 + zh: e3456cb027ce3fcc /deepseek-ai-dsh-session-title/use-this-package/reading-titles: en: 34148c23c20ee6f4 zh: b0d10677722f3f63 diff --git a/packages/session/session-title/README.md b/packages/session/session-title/README.md index 4af53add67..42a11a345a 100644 --- a/packages/session/session-title/README.md +++ b/packages/session/session-title/README.md @@ -54,7 +54,7 @@ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-a ### Adding a provider -One optional asynchronous provider may be registered through `ctx.sessionTitle.register(provider)`; a second registration throws. The shipped model-backed providers are [first-prompt](../session-title-first-prompt-llm/README.md) and [all-prompts](../../experimental/session-title-all-prompts-llm/README.md), both using the shared [LLM generation policy](../session-title-llm/README.md). A provider starts only after a marked loop-built request's exact route matches the logged `request/header`, and a newer revision supersedes and aborts older work. For unchanged routes, an active marked request starts pending automatic generation without a new header event; accepted human messages may follow the current `step/start`. +One optional asynchronous provider may be registered through `ctx.sessionTitle.register(provider)`; a second registration throws until the first provider's disposal starts, so a replacement can activate while the previous provider drains, and the previous provider's late results never commit. The shipped model-backed providers are [first-prompt](../session-title-first-prompt-llm/README.md) and [all-prompts](../../experimental/session-title-all-prompts-llm/README.md), both using the shared [LLM generation policy](../session-title-llm/README.md). A provider starts only after a marked loop-built request's exact route matches the logged `request/header`, and a newer revision supersedes and aborts older work. For unchanged routes, an active marked request starts pending automatic generation without a new header event; accepted human messages may follow the current `step/start`. ### Reading titles diff --git a/packages/session/session-title/README.zh.md b/packages/session/session-title/README.zh.md index 6798e510d8..95a5c81f01 100644 --- a/packages/session/session-title/README.zh.md +++ b/packages/session/session-title/README.zh.md @@ -54,7 +54,7 @@ kind: "package-reference" ### 添加提供方 -可选异步提供方可通过 `ctx.sessionTitle.register(provider)` 注册一个;第二次注册会立即抛出。随附的模型支持提供方是[首消息](../session-title-first-prompt-llm/README.zh.md)与[全消息](../../experimental/session-title-all-prompts-llm/README.zh.md),两者都使用共享的 [LLM(大语言模型)生成策略](../session-title-llm/README.zh.md)。提供方只有在带标记、由循环构建的请求的确切路由与已记录 `request/header` 匹配时才启动,较新的修订会取代并中止旧工作。路由不变时,活跃步骤中带标记的请求会启动待处理的自动生成,无需新的 header 事件;已接受的人类消息可以位于当前 `step/start` 之后。 +可选异步提供方可通过 `ctx.sessionTitle.register(provider)` 注册一个;在前一个提供方开始释放之前,第二次注册会立即抛出。因此替代提供方可以在前一个提供方排空期间激活,前一个提供方迟到的结果不会提交。随附的模型支持提供方是[首消息](../session-title-first-prompt-llm/README.zh.md)与[全消息](../../experimental/session-title-all-prompts-llm/README.zh.md),两者都使用共享的 [LLM(大语言模型)生成策略](../session-title-llm/README.zh.md)。提供方只有在带标记、由循环构建的请求的确切路由与已记录 `request/header` 匹配时才启动,较新的修订会取代并中止旧工作。路由不变时,活跃步骤中带标记的请求会启动待处理的自动生成,无需新的 header 事件;已接受的人类消息可以位于当前 `step/start` 之后。 ### 读取标题 diff --git a/packages/session/session-title/src/index.ts b/packages/session/session-title/src/index.ts index 45ca83a19d..1bdfda9d76 100644 --- a/packages/session/session-title/src/index.ts +++ b/packages/session/session-title/src/index.ts @@ -464,13 +464,14 @@ export class SessionTitleService extends Service { /** * Register the sole optional title provider. Disposal aborts its pending and - * active work before another provider may register. + * active work; a replacement may register once disposal has started, and the + * closing provider's late results never commit. * @param provider - provider identity, cadence, and generation function. * @returns exact Cordis effect disposer, which settles after active calls quiesce. */ register(provider: SessionTitleProvider): () => Promise { this.validateProvider(provider) - if (this.registration !== undefined) { + if (this.registration !== undefined && !this.registration.closing) { throw new Error(`session-title provider "${this.registration.provider.id}" is already registered`) } const registration: ProviderRegistration = { diff --git a/packages/session/session-title/tests/provider.spec.ts b/packages/session/session-title/tests/provider.spec.ts index 1f740e38b8..6658c8f4aa 100644 --- a/packages/session/session-title/tests/provider.spec.ts +++ b/packages/session/session-title/tests/provider.spec.ts @@ -289,6 +289,41 @@ describe('SessionTitleService Provider lifecycle', () => { await disposeReplacement() }) + it('accepts a replacement while the previous provider drains and discards its late result', async () => { + const ctx = new Context() + await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(SessionTitleService, CONFIG) + const pending = deferred() + const dispose = ctx.sessionTitle.register({ + id: SessionTitleProviderId('retiring'), + automatic: 'all-prompts', + generate: () => pending.promise, + }) + const session = ctx.sessions.create(SessionId('replace-while-draining')) + session.append('turn/start', { turn: 1 }) + const message = appendHumanPrompt(session, 'Generate this title') + await settle() + appendRoute(session) + await settle() + + const disposal = dispose() + const disposeReplacement = ctx.sessionTitle.register({ + id: SessionTitleProviderId('replacement'), + automatic: 'all-prompts', + generate: async () => ({ title: 'replacement', messageSeqs: [message.seq] }), + }) + expect(() => ctx.sessionTitle.register({ + id: SessionTitleProviderId('duplicate'), + automatic: 'first-prompt', + generate: async () => ({ title: 'duplicate', messageSeqs: [message.seq] }), + })).toThrow(/"replacement" is already registered/) + pending.resolve({ title: 'stale provider result', messageSeqs: [message.seq] }) + await disposal + expect(ctx.sessionTitle.get(session)?.title).not.toBe('stale provider result') + await disposeReplacement() + }) + it('supersedes an older all-messages revision and cannot commit an ignored abort', async () => { const ctx = new Context() await ctx.plugin(SessionStore) diff --git a/packages/subagent/subagent-claude-code/README.md b/packages/subagent/subagent-claude-code/README.md index fba3bf371c..0c3f584048 100644 --- a/packages/subagent/subagent-claude-code/README.md +++ b/packages/subagent/subagent-claude-code/README.md @@ -64,7 +64,7 @@ The bundle inserts `tool-subagent-claude-code` as a Host row, so `subagent_claud - insert: - id: subagent-claude-code name: '@deepseek-ai/dsh-subagent-claude-code' - - id: tool-subagent-claude + - id: tool-subagent-claude-code name: '@deepseek-ai/dsh-tool-subagent' config: provider: claude-code diff --git a/packages/subagent/subagent-claude-code/README.zh.md b/packages/subagent/subagent-claude-code/README.zh.md index 27776d86d9..f1fd54db4b 100644 --- a/packages/subagent/subagent-claude-code/README.zh.md +++ b/packages/subagent/subagent-claude-code/README.zh.md @@ -64,7 +64,7 @@ Bundle 将 `tool-subagent-claude-code` 作为 Host 条目插入,因此 `subage - insert: - id: subagent-claude-code name: '@deepseek-ai/dsh-subagent-claude-code' - - id: tool-subagent-claude + - id: tool-subagent-claude-code name: '@deepseek-ai/dsh-tool-subagent' config: provider: claude-code diff --git a/scripts/verify-product-use.spec.ts b/scripts/verify-product-use.spec.ts index 62ce5d36e7..002af7c3b2 100644 --- a/scripts/verify-product-use.spec.ts +++ b/scripts/verify-product-use.spec.ts @@ -243,14 +243,6 @@ describe('product package use', () => { expect(failures(root, declarations())).toContain(`${candidate} must declare dsh.bundle.patch`) }) - it('rejects an on-demand contribution whose preset target is missing', () => { - const root = fixture() - pkg(root, candidateDir, candidate, { dsh: { bundle: { patch: './cordis.patch.yml' } } }) - write(root, `${candidateDir}/cordis.patch.yml`, [{ preset: 'missing-preset', insert: [{ id: 'optional', name: core }] }]) - profiles(root, [], [candidate]) - expect(() => verifyProductUse(root, {})).toThrow(/missing-preset/) - }) - it('rejects a missing workspace source mapping instead of consulting built exports', () => { const root = fixture() pkg(root, candidateDir, candidate)