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