perf(startup): build the local tool environment in the background and gate its consumers

Runtime creation, MCP launches, Obsidian, native clients and the tool-environment IPC await the initialization promise instead of startup awaiting it before the window path. A failure still fails startup with the same diagnostic.
This commit is contained in:
mesalogo
2026-10-03 16:56:13 +08:00
parent b28ddd4ba8
commit 68cc0792e1
11 changed files with 149 additions and 19 deletions
+11 -1
View File
@@ -81,7 +81,10 @@ import {
listAllMcpTools,
normalizeMcpToolSchema
} from './mcp-tool-utils'
import type { LaunchEnvironmentProvider } from '../local-tool-environment'
import type {
LaunchEnvironmentProvider,
LaunchEnvironmentReady
} from '../local-tool-environment'
import {
BrowserModelTools,
browserNavigateInputSchema,
@@ -270,6 +273,8 @@ export type KnowledgeMcpGatewayOptions = {
): BrowserTabUsageLease
}
launchEnvironmentProvider?: LaunchEnvironmentProvider
/** Awaited before reading the provider; startup builds the environment in the background. */
launchEnvironmentReady?: LaunchEnvironmentReady
}
function toMagicNoteToolSummary(
@@ -400,6 +405,8 @@ export class KnowledgeMcpGateway {
): BrowserTabUsageLease
}
private readonly launchEnvironmentProvider?: LaunchEnvironmentProvider
private readonly launchEnvironmentReady?: LaunchEnvironmentReady
private readonly observeFailure?: DesktopDiagnosticFailureObserver
private server?: Server
private endpoint?: string
@@ -416,6 +423,8 @@ export class KnowledgeMcpGateway {
this.obsidianService = options.obsidianService
this.browserService = options.browserService
this.launchEnvironmentProvider = options.launchEnvironmentProvider
this.launchEnvironmentReady = options.launchEnvironmentReady
this.observeFailure = options.observeFailure
}
@@ -1048,6 +1057,7 @@ export class KnowledgeMcpGateway {
capability.brokerController.signal
])
try {
await this.launchEnvironmentReady?.()
await client.connect(
createMcpTransport(server, this.launchEnvironmentProvider),
{
@@ -10,7 +10,7 @@ afterEach(async () => {
vi.restoreAllMocks()
})
function fixture() {
function fixture(whenReady: () => Promise<void> = async () => undefined) {
const profile = { id: 'selected', name: 'Selected', protocol: 'openai-chat-completions',
authentication: 'api-key', apiKey: 'secret', baseUrl: 'https://example.com/v1', modelName: 'selected-model' }
const settings = { provider: 'deepseek-harness', modelProfiles: [profile], deepseekHarnessModelProfile: profile }
@@ -37,7 +37,7 @@ function fixture() {
getResolvedMcpServers: async () => [{ id: 'custom' }], getEnabledBuiltinMcpServerIds: async () => ['knowledge-base', 'obsidian', 'goodbuddy-config'],
getObsidianSettings: async () => ({}) },
executionSpaceResolver: { resolveProject: () => ({ kind: 'local', rootPath: '/workspace', cacheIdentity: '/workspace' }) },
terminalManager: { closeOwner: vi.fn() }, localEnvironment: { launchEnvironmentProvider: () => process.env },
terminalManager: { closeOwner: vi.fn() }, localEnvironment: { launchEnvironmentProvider: () => process.env, whenReady },
rootDirectory: '/clients', createGateway: () => gateway, openExternal
} as unknown as ConstructorParameters<typeof NativeClientCoordinator>[0])
coordinators.push(coordinator)
@@ -45,6 +45,16 @@ function fixture() {
}
describe('native client coordinator', () => {
it('waits for the startup tool environment before launching a native client', async () => {
let ready!: () => void
const { coordinator, start } = fixture(() => new Promise<void>(resolve => { ready = resolve }))
const opened = coordinator.open(1, 'conversation')
await new Promise(resolve => setTimeout(resolve, 10))
expect(start).not.toHaveBeenCalled()
ready()
await expect(opened).resolves.toEqual({ kind: 'browser', serviceId: 'service-0' })
expect(start).toHaveBeenCalledOnce()
})
it('retains a ready service and its gateway after browser failure, then reopens and stops it', async () => {
const { coordinator, gateway, start, stop, openExternal } = fixture()
openExternal.mockRejectedValueOnce(new Error('Browser unavailable'))
+4 -1
View File
@@ -25,7 +25,7 @@ type Options = {
capabilities: CapabilityService
executionSpaceResolver: ExecutionSpaceResolver
terminalManager: TerminalSessionManager
localEnvironment: LocalToolEnvironmentService
localEnvironment: Pick<LocalToolEnvironmentService, 'launchEnvironmentProvider' | 'whenReady'>
bundledRuntimePaths: BundledRuntimePaths
rootDirectory: string
managedNodeDirectory: string
@@ -122,6 +122,9 @@ export class NativeClientCoordinator {
}
async open(ownerId: number, conversationId: string): Promise<RuntimeNativeClientResult> {
this.assertOwner(ownerId)
// Native clients launch with the prepared tool PATH; startup builds it in the background.
await this.options.localEnvironment.whenReady()
this.assertOwner(ownerId)
const context = await this.resolve(conversationId)
this.assertOwner(ownerId)