fix(plugin): address review on the preset-patch revert

Let a title provider replace one that is still draining, so switching the
titles bundle during an in-flight request no longer leaves no provider.
Omit group container rows from bundle inventories, since they have no
plugin entry to switch. Drop the obsolete preset-target product-use case,
fix the Claude Code row id example, and refresh stale Agent Note text.
This commit is contained in:
Turtle committed 2026-10-07 21:27:38 +08:00
1 parent b173f3e2a2
commit a8e9e9b546
19 files changed
+68 -36

No files matched your search

@@ -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
@@ -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.
@@ -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 的行仍在使用时拒绝执行。激活失败可以保留已保存的依赖和选择,不加入回滚。离线元信息、发布版本锁定、源码目标选择、同版本来源差异、干净安装产物和发布顺序有专门检查。原生提供方认证不属于目录发现过程。
@@ -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
@@ -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.
@@ -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 兼容范围。
+2 -1
View File
@@ -193,7 +193,8 @@ async refresh(session: Session, signal?: AbortSignal): Promise<SessionTitleSnaps
/**
* 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.
*/
+2 -1
View File
@@ -193,7 +193,8 @@ async refresh(session: Session, signal?: AbortSignal): Promise<SessionTitleSnaps
/**
* 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.
*/
+2 -1
View File
@@ -690,7 +690,8 @@ export class PluginManager extends TypertRemoteService {
const rows: BundleRowInfo[] = []
const packages = this.ctx.get('pluginPackages')
for (const row of flatten(composeEntries([patches.filter(item => 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
@@ -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] }]))
@@ -2522,7 +2522,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
},
{
signature: 'register(provider: SessionTitleProvider): () => Promise<void>',
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.',
},
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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` 之后。
### 读取标题
+3 -2
View File
@@ -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<void> {
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 = {
@@ -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<SessionTitleProviderResult>()
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)
@@ -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
@@ -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
-8
View File
@@ -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)