From 9bd31275192dd90ad38f920ffbbc971f42a268d8 Mon Sep 17 00:00:00 2001 From: Michal Date: Wed, 25 Feb 2026 01:37:30 +0000 Subject: [PATCH] fix: warmup ACP subprocess eagerly to avoid 30s cold-start on status The pool refactor made ACP client creation lazy, causing the first /llm/health call to spawn + initialize + prompt Gemini in one request (30s+). Now warmup() eagerly starts the subprocess on mcplocal boot. Also fetch models in parallel with LLM health check. Co-Authored-By: Claude Opus 4.6 --- src/cli/src/commands/status.ts | 21 +++++++++++---------- src/mcplocal/src/llm-config.ts | 4 +++- src/mcplocal/src/providers/gemini-acp.ts | 12 ++++++++++++ 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/cli/src/commands/status.ts b/src/cli/src/commands/status.ts index 82a41be..b3ba569 100644 --- a/src/cli/src/commands/status.ts +++ b/src/cli/src/commands/status.ts @@ -47,7 +47,7 @@ function defaultCheckHealth(url: string): Promise { */ function defaultCheckLlm(mcplocalUrl: string): Promise { return new Promise((resolve) => { - const req = http.get(`${mcplocalUrl}/llm/health`, { timeout: 30000 }, (res) => { + const req = http.get(`${mcplocalUrl}/llm/health`, { timeout: 45000 }, (res) => { const chunks: Buffer[] = []; res.on('data', (chunk: Buffer) => chunks.push(chunk)); res.on('end', () => { @@ -167,8 +167,9 @@ export function createStatusCommand(deps?: Partial): Command return; } - // LLM check with spinner — queries mcplocal's /llm/health endpoint + // LLM check + models fetch in parallel — queries mcplocal endpoints const llmPromise = checkLlm(config.mcplocalUrl); + const modelsPromise = fetchModels(config.mcplocalUrl); if (isTTY) { let frame = 0; @@ -177,7 +178,7 @@ export function createStatusCommand(deps?: Partial): Command frame++; }, 80); - const llmStatus = await llmPromise; + const [llmStatus, models] = await Promise.all([llmPromise, modelsPromise]); clearInterval(interval); if (llmStatus === 'ok' || llmStatus === 'ok (key stored)') { @@ -185,20 +186,20 @@ export function createStatusCommand(deps?: Partial): Command } else { write(`${CLEAR_LINE}LLM: ${llmLabel} ${RED}✗ ${llmStatus}${RESET}\n`); } + if (models.length > 0) { + log(`${DIM} Available: ${models.join(', ')}${RESET}`); + } } else { // Non-TTY: no spinner, just wait and print - const llmStatus = await llmPromise; + const [llmStatus, models] = await Promise.all([llmPromise, modelsPromise]); if (llmStatus === 'ok' || llmStatus === 'ok (key stored)') { log(`LLM: ${llmLabel} ✓ ${llmStatus}`); } else { log(`LLM: ${llmLabel} ✗ ${llmStatus}`); } - } - - // Show available models (non-blocking, best effort) - const models = await fetchModels(config.mcplocalUrl); - if (models.length > 0) { - log(`${DIM} Available: ${models.join(', ')}${RESET}`); + if (models.length > 0) { + log(`${DIM} Available: ${models.join(', ')}${RESET}`); + } } }); } diff --git a/src/mcplocal/src/llm-config.ts b/src/mcplocal/src/llm-config.ts index 786fc19..aa5c7bd 100644 --- a/src/mcplocal/src/llm-config.ts +++ b/src/mcplocal/src/llm-config.ts @@ -28,7 +28,9 @@ export async function createProviderFromConfig( const cfg: GeminiAcpConfig = {}; if (config.binaryPath) cfg.binaryPath = config.binaryPath; if (config.model) cfg.defaultModel = config.model; - registry.register(new GeminiAcpProvider(cfg)); + const provider = new GeminiAcpProvider(cfg); + provider.warmup(); + registry.register(provider); break; } diff --git a/src/mcplocal/src/providers/gemini-acp.ts b/src/mcplocal/src/providers/gemini-acp.ts index c9a2816..82d2b14 100644 --- a/src/mcplocal/src/providers/gemini-acp.ts +++ b/src/mcplocal/src/providers/gemini-acp.ts @@ -83,6 +83,18 @@ export class GeminiAcpProvider implements LlmProvider { this.pool.clear(); } + /** + * Eagerly spawn the default model's ACP subprocess so it's ready + * for the first request (avoids 30s cold-start on health checks). + */ + warmup(): void { + const entry = this.getOrCreateEntry(this.defaultModel); + // Fire-and-forget: start the subprocess initialization in the background + entry.client.ensureReady().catch(() => { + // Ignore errors — next request will retry + }); + } + /** Number of active pool entries (for testing). */ get poolSize(): number { return this.pool.size;