fix(diagnostics): retain observed plugin versions for recovery
This commit is contained in:
1 parent
0596c66fbe
commit
d136ac9f6f
11 files changed
+57
-17
No files matched your search
@@ -56,10 +56,10 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), `remotes` |
|
||||
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:75`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
|
||||
| `permission-presets/catalog-changed` | `emit` | [`packages/interaction/permission-presets/src/types.ts:48`](../packages/interaction/permission-presets/src/types.ts) | [`permission-presets`](../packages/interaction/permission-presets) (`events.dispatch`) | `remotes` |
|
||||
| `plugin-manager/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:301`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
|
||||
| `plugin-manager/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:307`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
|
||||
| `plugin-manager/install-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:314`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
|
||||
| `plugin-manager/operation` | `emit` | [`packages/boot/plugin-manager/src/types.ts:293`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | - |
|
||||
| `plugin-manager/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:302`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
|
||||
| `plugin-manager/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:308`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
|
||||
| `plugin-manager/install-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:315`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
|
||||
| `plugin-manager/operation` | `emit` | [`packages/boot/plugin-manager/src/types.ts:294`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | - |
|
||||
| `plugin-market/changed` | `emit` | [`packages/boot/plugin-market/src/types.ts:136`](../packages/boot/plugin-market/src/types.ts) | [`plugin-market`](../packages/boot/plugin-market) (`emit`) | `remotes` |
|
||||
| `schedule/changed` | `emit` | [`packages/schedule/schedule/src/types.ts:478`](../packages/schedule/schedule/src/types.ts) | [`schedule`](../packages/schedule/schedule) (`emit`) | `remotes` |
|
||||
| `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:44`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
|
||||
|
||||
@@ -58,10 +58,10 @@
|
||||
| `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), `remotes` |
|
||||
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:75`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
|
||||
| `permission-presets/catalog-changed` | `emit` | [`packages/interaction/permission-presets/src/types.ts:48`](../packages/interaction/permission-presets/src/types.ts) | [`permission-presets`](../packages/interaction/permission-presets) (`events.dispatch`) | `remotes` |
|
||||
| `plugin-manager/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:301`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
|
||||
| `plugin-manager/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:307`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
|
||||
| `plugin-manager/install-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:314`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
|
||||
| `plugin-manager/operation` | `emit` | [`packages/boot/plugin-manager/src/types.ts:293`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | - |
|
||||
| `plugin-manager/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:302`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
|
||||
| `plugin-manager/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:308`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
|
||||
| `plugin-manager/install-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:315`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
|
||||
| `plugin-manager/operation` | `emit` | [`packages/boot/plugin-manager/src/types.ts:294`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | - |
|
||||
| `plugin-market/changed` | `emit` | [`packages/boot/plugin-market/src/types.ts:136`](../packages/boot/plugin-market/src/types.ts) | [`plugin-market`](../packages/boot/plugin-market) (`emit`) | `remotes` |
|
||||
| `schedule/changed` | `emit` | [`packages/schedule/schedule/src/types.ts:478`](../packages/schedule/schedule/src/types.ts) | [`schedule`](../packages/schedule/schedule) (`emit`) | `remotes` |
|
||||
| `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:44`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
en: d5643a06f300c30a
|
||||
zh: 9348b6a7676a2849
|
||||
/deepseek-ai-dsh-plugin-manager/understand-the-implementation:
|
||||
en: 531d50d67168f82b
|
||||
zh: 295fbc453029f0c4
|
||||
en: 86c5eddc73fb60ca
|
||||
zh: 3b9819ab0e336611
|
||||
/deepseek-ai-dsh-plugin-manager/further-exploration:
|
||||
en: c6538243d8702a0d
|
||||
zh: 04a82d923353c75f
|
||||
|
||||
@@ -103,7 +103,7 @@ A service run whose captured output stays silent for `idleTimeoutMs` is terminat
|
||||
|
||||
Results contain the last attempted stage, target, saved-state change, application status and error codes. Web dictionaries render management text; pnpm and Loader diagnostics remain unmodified. Unrelated pre-existing inactive entries return warnings; new or changed failures and inactive explicit enablement targets fail the operation. A failed or cancelled package run, or failed bundle validation, restores the manifest and lockfile snapshotted before pnpm ran ([rationale](../../../.agents/notes/implemented/architecture/2026-09-15-guided-plugin-installation.md)); later failures retain saved changes as described in [Failure behavior](#failure-behavior). Installations are tracked by request id until their call settles, so a cancellation names one run and joins its settlement without taking the profile lock. The CLI inherits authentication variables and terminal descriptors; service operations use a scrubbed environment and captured output.
|
||||
|
||||
The `plugin-manager/operation` event correlates each service invocation with its start and observed result, or an unknown outcome when an exception escapes result construction. It excludes raw installation specs and error text. Diagnostic consumers own persistence and delivery limits; synchronous subscriber failures do not change management results. Direct CLI package operations do not emit this service event.
|
||||
The `plugin-manager/operation` event correlates each service invocation with its start and observed result, or an unknown outcome when an exception escapes result construction. It retains the package version observed before a toggle or removal, and the installed version when installation resolves it; missing metadata leaves the version unknown without blocking recovery. It excludes raw installation specs and error text. Diagnostic consumers own persistence and delivery limits; synchronous subscriber failures do not change management results. Direct CLI package operations do not emit this service event.
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ service 运行若在 `idleTimeoutMs` 内没有任何捕获输出即被终止,
|
||||
|
||||
结果包含最后尝试的阶段、目标、磁盘变化、应用状态和错误码。Web 词典呈现管理文案;pnpm 与 Loader 的诊断保持原样。无关的已有故障作为警告返回;新出现、配置变化后的故障,以及显式启用目标未激活,都会使操作失败。包管理运行失败或被取消,或组合包校验失败时,会恢复 pnpm 运行前快照的 manifest 与 lockfile([理由](../../../.agents/notes/implemented/architecture/2026-09-15-guided-plugin-installation.zh.md));后续失败按[失败行为](#failure-behavior)所述保留已保存的改动。安装按 request id 跟踪到调用结束,因此取消只针对一次运行,并且不取 profile 锁就能等待它结束。CLI 继承认证环境和终端描述符;service 使用清理后的环境并捕获输出。管理器直接读取文件和 Loader 状态,不维护第二份目标状态注册表,因此不发布单独的运行时不变式伴生入口。
|
||||
|
||||
`plugin-manager/operation` 事件关联每次服务调用的开始和已观察到的结果;若异常使结果无法构造,则记录结果未知。事件不包含原始安装 spec 和错误文本。诊断消费者负责持久化和传输限额;同步订阅者失败不会改变管理结果。直接执行的 CLI 包操作不发送此服务事件。
|
||||
`plugin-manager/operation` 事件关联每次服务调用的开始和已观察到的结果;若异常使结果无法构造,则记录结果未知。事件保留启停或移除前观察到的包版本,以及安装完成后解析出的版本;元数据缺失时版本未知,不阻止恢复操作。事件不包含原始安装 spec 和错误文本。诊断消费者负责持久化和传输限额;同步订阅者失败不会改变管理结果。直接执行的 CLI 包操作不发送此服务事件。
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -20,13 +20,14 @@ const fields = {
|
||||
target: z.string().max(256).nullable(),
|
||||
stage: z.enum(['install', 'enable', 'remove']),
|
||||
enabled: z.boolean().optional(),
|
||||
version: z.string().max(128).optional(),
|
||||
}
|
||||
const operation: z.ZodType<PluginOperation> = z.discriminatedUnion('phase', [
|
||||
z.object({ ...fields, phase: z.literal('started') }),
|
||||
z.object({ ...fields, phase: z.literal('unknown') }),
|
||||
z.object({ ...fields, phase: z.literal('settled'),
|
||||
application: z.enum(['applied', 'restart-required', 'overridden', 'failed', 'cancelled']),
|
||||
changed: z.boolean(), version: z.string().max(128).optional(),
|
||||
changed: z.boolean(),
|
||||
errorCode: z.enum(['management-required', 'unaddressable', 'unknown-plugin', 'invalid-spec', 'already-installed',
|
||||
'ambiguous-install', 'not-bundle', 'not-removable', 'stop-profile', 'bundle-in-use', 'stale-approval',
|
||||
'incompatible-version', 'operation-error']).optional(),
|
||||
|
||||
@@ -902,8 +902,10 @@ export class PluginManager extends TypertRemoteService {
|
||||
reason: PluginChange['reason'],
|
||||
): Promise<ChangeResult> {
|
||||
const id = randomUUID() as PluginOperationId
|
||||
const version = this.operationVersion(reason, request.target)
|
||||
const facts = { id, reason, stage: request.stage,
|
||||
target: reason === 'install' ? null : request.target,
|
||||
...version === undefined ? {} : { version },
|
||||
...request.enabled === undefined ? {} : { enabled: request.enabled } }
|
||||
this.reportOperation({ ...facts, time: new Date().toISOString(), phase: 'started' })
|
||||
let settled: ChangeResult
|
||||
@@ -952,6 +954,21 @@ export class PluginManager extends TypertRemoteService {
|
||||
}
|
||||
}
|
||||
|
||||
/** Read the pre-operation package version without preventing recovery from damaged metadata. */
|
||||
private operationVersion(reason: PluginChange['reason'], target: string): string | undefined {
|
||||
if (reason === 'install') return undefined
|
||||
try {
|
||||
if (reason !== 'plugin') return bundleManifest(target, this.profile.dir, this.profile.installAnchor)?.version
|
||||
const entry = [...this.ctx.loader.entries()].find(row => row.id === target)
|
||||
const source = entry?.parent.tree.ctx.baseUrl
|
||||
if (entry === undefined || source === undefined) return undefined
|
||||
return this.ownerContext.get('pluginPackages')?.packageOf(entry.options.name, source)?.version
|
||||
} catch (_error) {
|
||||
// Disabling or removing a broken package must not depend on diagnostic metadata being readable.
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
private diskState(): string {
|
||||
return ['package.json', 'cordis.patch.yml', 'pnpm-workspace.yaml', PROFILE_COMPATIBILITY_FILENAME].map((file) => {
|
||||
try { return readFileSync(join(this.profile.dir, file), 'utf8') }
|
||||
|
||||
@@ -269,13 +269,14 @@ export type PluginOperation = {
|
||||
readonly target: string | null
|
||||
readonly stage: ChangeResult['stage']
|
||||
readonly enabled?: boolean | undefined
|
||||
/** Observed package version; absence means unavailable, never the latest registry version. */
|
||||
readonly version?: string | undefined
|
||||
} & (
|
||||
| { readonly phase: 'started' }
|
||||
| {
|
||||
readonly phase: 'settled'
|
||||
readonly application: ChangeResult['application']
|
||||
readonly changed: boolean
|
||||
readonly version?: string | undefined
|
||||
readonly errorCode?: ManagementError['code'] | undefined
|
||||
readonly restartReason?: RestartReason | undefined
|
||||
}
|
||||
|
||||
@@ -21,3 +21,9 @@ it('rejects unsupported phases, oversized identifiers and invalid result fields'
|
||||
expect(parsePluginOperationRecord({ ...input, transportDropped: -1 })).toBeUndefined()
|
||||
expect(parsePluginOperationRecord({ ...input, runId: '' })).toBeUndefined()
|
||||
})
|
||||
|
||||
it('retains an observed version before an operation settles', () => {
|
||||
const started = { ...input, operation: { ...input.operation, phase: 'started', version: '1.2.3' } }
|
||||
expect(parsePluginOperationRecord(started)?.operation).toMatchObject({ phase: 'started', version: '1.2.3' })
|
||||
expect(parsePluginOperationRecord({ ...started, operation: { ...started.operation, version: 'x'.repeat(129) } })).toBeUndefined()
|
||||
})
|
||||
@@ -466,12 +466,27 @@ it('correlates diagnostic operation starts with persisted toggle results', async
|
||||
await manager.setBundleEnabled('extra', false)
|
||||
expect(readProfileManifest('test', dir).dsh?.profile?.bundles).not.toContain('extra')
|
||||
expect(facts).toHaveLength(2)
|
||||
expect(facts[0]).toMatchObject({ phase: 'started', reason: 'bundle', target: 'extra', enabled: false })
|
||||
expect(facts[1]).toMatchObject({ id: facts[0]!.id, phase: 'settled', changed: true, application: 'restart-required' })
|
||||
expect(facts[0]).toMatchObject({ phase: 'started', reason: 'bundle', target: 'extra', enabled: false, version: '1.0.0' })
|
||||
expect(facts[1]).toMatchObject({ id: facts[0]!.id, phase: 'settled', changed: true, application: 'restart-required', version: '1.0.0' })
|
||||
await manager.setBundleEnabled('extra', true)
|
||||
expect(facts[2]!.id).not.toBe(facts[0]!.id)
|
||||
})
|
||||
|
||||
it('retains an observed version for a refused removal and leaves damaged metadata unknown', async () => {
|
||||
const { manager, ctx, dir } = await fixture('startup')
|
||||
const facts: PluginOperation[] = []
|
||||
ctx.on('plugin-manager/operation', (event) => { facts.push(event) })
|
||||
expect(await manager.removeBundle('extra')).toMatchObject({ application: 'failed', error: { code: 'stop-profile' } })
|
||||
expect(facts).toEqual([
|
||||
expect.objectContaining({ phase: 'started', reason: 'remove', version: '1.0.0' }),
|
||||
expect.objectContaining({ phase: 'settled', reason: 'remove', version: '1.0.0', application: 'failed' }),
|
||||
])
|
||||
writeFileSync(join(dir, 'node_modules', 'extra', 'package.json'), '{')
|
||||
expect(await manager.setBundleEnabled('extra', false)).toMatchObject({ changed: true, application: 'restart-required' })
|
||||
expect(facts[2]).not.toHaveProperty('version')
|
||||
expect(facts[3]).not.toHaveProperty('version')
|
||||
})
|
||||
|
||||
it('excludes raw install specs and error diagnostics from operation facts', async () => {
|
||||
const { manager, ctx } = await fixture('startup')
|
||||
const facts: PluginOperation[] = []
|
||||
|
||||
@@ -6439,7 +6439,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
},
|
||||
{
|
||||
name: 'PluginOperation',
|
||||
declaration: 'export type PluginOperation = {\n readonly id: PluginOperationId;\n readonly time: string;\n readonly reason: PluginChange[\'reason\'];\n readonly target: string | null;\n readonly stage: ChangeResult[\'stage\'];\n readonly enabled?: boolean | undefined;\n} & ({\n readonly phase: \'started\';\n} | {\n readonly phase: \'settled\';\n readonly application: ChangeResult[\'application\'];\n readonly changed: boolean;\n readonly version?: string | undefined;\n readonly errorCode?: ManagementError[\'code\'] | undefined;\n readonly restartReason?: RestartReason | undefined;\n} | {\n readonly phase: \'unknown\';\n});',
|
||||
declaration: 'export type PluginOperation = {\n readonly id: PluginOperationId;\n readonly time: string;\n readonly reason: PluginChange[\'reason\'];\n readonly target: string | null;\n readonly stage: ChangeResult[\'stage\'];\n readonly enabled?: boolean | undefined;\n readonly version?: string | undefined;\n} & ({\n readonly phase: \'started\';\n} | {\n readonly phase: \'settled\';\n readonly application: ChangeResult[\'application\'];\n readonly changed: boolean;\n readonly errorCode?: ManagementError[\'code\'] | undefined;\n readonly restartReason?: RestartReason | undefined;\n} | {\n readonly phase: \'unknown\';\n});',
|
||||
},
|
||||
{
|
||||
name: 'PluginOperationId',
|
||||
|
||||
Reference in new issue
Block a user