From 9501b2299bc7f2c6e28aaa99878e5f14ca40e059 Mon Sep 17 00:00:00 2001 From: Muhammad Mustaqeem <265153888+Mustaqeem66@users.noreply.github.com> Date: Fri, 14 Aug 2026 11:32:53 +0500 Subject: [PATCH] fix(lsp): stopped reaping clients with in-flight requests (#8390) --- packages/coding-agent/src/lsp/client.ts | 30 +++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/src/lsp/client.ts b/packages/coding-agent/src/lsp/client.ts index 9a5950083..49f1e7fe6 100644 --- a/packages/coding-agent/src/lsp/client.ts +++ b/packages/coding-agent/src/lsp/client.ts @@ -62,13 +62,32 @@ export function setIdleTimeout(ms: number | null | undefined): void { } } +/** + * Whether a client may be reaped by the idle checker. + * + * A client with in-flight requests is *busy*, never idle. `lastActivity` is + * stamped when a request is written, not while it is outstanding, so a single + * request that runs longer than the idle timeout used to look like silence: + * the checker tore the client down mid-flight and `shutdownClientInstance` + * rejected the caller's still-pending promise with "LSP client shutdown" + * (issue #8390). Requests that settle refresh `lastActivity`, so a client + * becomes eligible again only after the final one lands and the full idle + * window then elapses. + * + * Exported for tests; the idle checker is the only production caller. + */ +export function isIdleClient(client: LspClient, now: number, timeoutMs: number): boolean { + if (client.pendingRequests.size > 0) return false; + return now - client.lastActivity > timeoutMs; +} + function startIdleChecker(): void { if (idleCheckInterval) return; idleCheckInterval = setInterval(() => { if (!idleTimeoutMs) return; const now = Date.now(); for (const [key, client] of Array.from(clients.entries())) { - if (now - client.lastActivity > idleTimeoutMs) { + if (isIdleClient(client, now, idleTimeoutMs)) { void shutdownClient(key); } } @@ -1462,15 +1481,22 @@ export async function sendRequest( } } - // Register pending request with timeout wrapper + // Register pending request with timeout wrapper. + // Settling stamps `lastActivity`: the idle window must be measured from when + // the exchange finished, not from when it started. Without this a request + // that outlives the timeout would leave the client instantly reapable the + // moment it lands, so the next idle sweep would kill a server that had just + // answered (issue #8390). client.pendingRequests.set(id, { resolve: result => { if (timeout) clearTimeout(timeout); + client.lastActivity = Date.now(); cleanup(); resolve(result); }, reject: err => { if (timeout) clearTimeout(timeout); + client.lastActivity = Date.now(); cleanup(); reject(err); },