From c52223ad5ce6582fec6a0ec13fd4d5f71d4faacc Mon Sep 17 00:00:00 2001 From: Yigit Colakoglu Date: Sat, 31 Jan 2026 20:12:57 +0100 Subject: [PATCH] fix(coding-agent/lsp): surface server exit errors and add per-server warmup timeout Previously, any LSP server failure during initialize (process crash, missing binary, rustup proxy error) was reported as a generic "timed out" message. The process exit and its stderr were ignored; only the per-request timeout fired, hiding the actual cause. proc.exited now rejects all pending requests with the server's stderr output. ptree already captures stderr and awaits its completion for non-zero exits, so peekStderr() is reliable at this point. The first rejection wins the race against the timeout; the other finds the request already removed and no-ops. Also added warmupTimeoutMs to ServerConfig, allowing per-server override of the global 5s warmup cap via lsp.json/lsp.yaml. Servers without it fall back to WARMUP_TIMEOUT_MS as before. Timeout error messages now include the configured duration for clarity when they do legitimately fire. --- packages/coding-agent/src/lsp/client.ts | 15 ++++++++++++++- packages/coding-agent/src/lsp/index.ts | 2 +- packages/coding-agent/src/lsp/types.ts | 2 ++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/src/lsp/client.ts b/packages/coding-agent/src/lsp/client.ts index 62d3ea849..0f22ee3f2 100644 --- a/packages/coding-agent/src/lsp/client.ts +++ b/packages/coding-agent/src/lsp/client.ts @@ -436,6 +436,19 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT proc.exited.then(() => { clients.delete(key); clientLocks.delete(key); + + // Reject any pending requests — the server is gone, they will never complete. + if (client.pendingRequests.size > 0) { + const stderr = proc.peekStderr().trim(); + const code = proc.exitCode; + const err = new Error( + stderr ? `LSP server exited (code ${code}): ${stderr}` : `LSP server exited unexpectedly (code ${code})`, + ); + for (const pending of client.pendingRequests.values()) { + pending.reject(err); + } + client.pendingRequests.clear(); + } }); // Start background message reader @@ -743,7 +756,7 @@ export async function sendRequest( timeout = setTimeout(() => { if (client.pendingRequests.has(id)) { client.pendingRequests.delete(id); - const err = new Error(`LSP request ${method} timed out`); + const err = new Error(`LSP request ${method} timed out after ${timeoutMs}ms`); cleanup(); reject(err); } diff --git a/packages/coding-agent/src/lsp/index.ts b/packages/coding-agent/src/lsp/index.ts index 721899588..d724c8e81 100644 --- a/packages/coding-agent/src/lsp/index.ts +++ b/packages/coding-agent/src/lsp/index.ts @@ -102,7 +102,7 @@ export async function warmupLspServers(cwd: string, options?: LspWarmupOptions): // Servers that don't respond quickly will be initialized lazily on first use const results = await Promise.allSettled( lspServers.map(async ([name, serverConfig]) => { - const client = await getOrCreateClient(serverConfig, cwd, WARMUP_TIMEOUT_MS); + const client = await getOrCreateClient(serverConfig, cwd, serverConfig.warmupTimeoutMs ?? WARMUP_TIMEOUT_MS); return { name, client, fileTypes: serverConfig.fileTypes }; }), ); diff --git a/packages/coding-agent/src/lsp/types.ts b/packages/coding-agent/src/lsp/types.ts index cd8237f25..8dc3dbc2c 100644 --- a/packages/coding-agent/src/lsp/types.ts +++ b/packages/coding-agent/src/lsp/types.ts @@ -358,6 +358,8 @@ export interface ServerConfig { initOptions?: Record; settings?: Record; disabled?: boolean; + /** Per-server warmup timeout in milliseconds. Overrides the global WARMUP_TIMEOUT_MS for this server during startup. */ + warmupTimeoutMs?: number; capabilities?: ServerCapabilities; /** If true, this is a linter/formatter server (e.g., Biome) - used only for diagnostics/actions, not type intelligence */ isLinter?: boolean;