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.
This commit is contained in:
Yigit Colakoglu
2026-01-31 20:12:57 +01:00
parent 12eeb63041
commit c52223ad5c
3 changed files with 17 additions and 2 deletions
+14 -1
View File
@@ -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);
}
+1 -1
View File
@@ -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 };
}),
);
+2
View File
@@ -358,6 +358,8 @@ export interface ServerConfig {
initOptions?: Record<string, unknown>;
settings?: Record<string, unknown>;
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;