diff --git a/docs/features/deepseek-harness/technical-design.md b/docs/features/deepseek-harness/technical-design.md index fd622ad1..7b344f8a 100644 --- a/docs/features/deepseek-harness/technical-design.md +++ b/docs/features/deepseek-harness/technical-design.md @@ -338,7 +338,7 @@ GoodBuddy conversationId -> Harness sessionId + process generation ### 9.4 释放与退出 -- 原生能力清单通过一次性 Runtime 探测,取得有界快照后立即 dispose,不得因浏览设置或切换项目把 Host 缓存在执行 Runtime 池中。 +- 原生能力清单由 `SelectedRuntimeManager` 缓存的执行 Runtime 提供,与随后的对话请求共用同一实例,不再为清单单独启动并销毁一次 Host;该实例遵循执行缓存的空闲淘汰、设置变更 reset 和退出 dispose 规则。 - 删除或释放对话时调用 `goodbuddy/session/release`。 - Runtime dispose 时先拒绝新请求,再取消所有 Session。 - Harness Control Plane 完成 Agent、工具和会话清理,Host 完成 Cordis Fiber 与子进程的反向清理。 diff --git a/src/main/agent/selected-runtime-manager.test.ts b/src/main/agent/selected-runtime-manager.test.ts index 26cbcf22..4a836669 100644 --- a/src/main/agent/selected-runtime-manager.test.ts +++ b/src/main/agent/selected-runtime-manager.test.ts @@ -318,13 +318,9 @@ describe('SelectedRuntimeManager', () => { await manager.dispose() }) - it('disposes a native-inventory runtime without caching it', async () => { - const inspected = runtime() - const cached = runtime() - const create = vi - .fn() - .mockResolvedValueOnce(inspected.value) - .mockResolvedValueOnce(cached.value) + it('serves the native inventory from the cached execution runtime', async () => { + const shared = runtime() + const create = vi.fn(async () => shared.value) const manager = new SelectedRuntimeManager(create) const selection = { provider: 'opencode' as const } @@ -337,15 +333,42 @@ describe('SelectedRuntimeManager', () => { provider: 'opencode', inventoryStatus: 'available' }) - expect(inspected.getNativeSnapshot).toHaveBeenCalledOnce() - expect(inspected.dispose).toHaveBeenCalledOnce() + expect(shared.getNativeSnapshot).toHaveBeenCalledOnce() + expect(shared.dispose).not.toHaveBeenCalled() - await manager.getRuntime( + const controller = await manager.getRuntime( selection, executionSpace('local:project-one') ) + expect(create).toHaveBeenCalledOnce() + await expect(controller.getStatus()).resolves.toMatchObject({ + available: true + }) + + await manager.getNativeSnapshot( + selection, + executionSpace('local:project-two') + ) expect(create).toHaveBeenCalledTimes(2) await manager.dispose() + expect(shared.dispose).toHaveBeenCalled() + }) + + it('rejects the native inventory when the runtime lacks it', async () => { + const plain = runtime() + const withoutInventory: AgentRuntime = { ...plain.value } + delete withoutInventory.getNativeSnapshot + const manager = new SelectedRuntimeManager( + vi.fn(async () => withoutInventory) + ) + + await expect( + manager.getNativeSnapshot({ + provider: 'model', + profileId: '00000000-0000-4000-8000-000000000001' + }) + ).rejects.toThrow('当前 Runtime 不支持原生能力清单') + await manager.dispose() }) it('waits for a pending connection-test runtime during shutdown', async () => { diff --git a/src/main/agent/selected-runtime-manager.ts b/src/main/agent/selected-runtime-manager.ts index f37aef4e..c8ed5c7e 100644 --- a/src/main/agent/selected-runtime-manager.ts +++ b/src/main/agent/selected-runtime-manager.ts @@ -355,18 +355,11 @@ export class SelectedRuntimeManager implements SelectedRuntimeResolver { selection: AgentRuntimeSelection, executionSpace?: ExecutionSpaceDescriptor ): Promise { - const runtime = await this.createRuntime(selection, executionSpace) - try { - if (this.disposed) { - throw new Error('Agent Runtime 正在关闭') - } - if (!runtime.getNativeSnapshot) { - throw new Error('当前 Runtime 不支持原生能力清单') - } - return await runtime.getNativeSnapshot() - } finally { - await runtime.dispose() - } + // The inventory runs on the same cached Runtime that later serves the + // conversation, so a slow cold start (embedded OpenCode on Linux ARM) + // is paid once instead of once per inventory and again per request. + const controller = await this.getRuntime(selection, executionSpace) + return controller.getNativeSnapshot() } private async startRetiring(