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:
@@ -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'))
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user