import { LspClient } from "./client.js"; import type { LspServerSpec } from "./servers.js"; /** * Process-wide pool of language-server clients, shared by every `LspManager` * in the daemon and reclaimed when idle. * * Two separate costs made LSP the heaviest thing in the app, and each needs a * different mechanism: * * - **Duplication.** Every `AgentSession` built its own `LspManager`, so two * windows open on one repo ran two complete tsserver stacks over identical * files. Fixed by keying clients on (server, project root) for the whole * process and REFERENCE COUNTING the managers holding them, so one session * closing cannot pull a server out from under another. * - **Pinning.** A manager's map had no expiry, so any root ever touched kept * its server resident for the daemon's lifetime — measured at two roots with * no window open at all, holding ~330 MB between them. Fixed by the idle * sweep: a server unused for `idleTtlMs` is shut down even while sessions * still hold it, because agent edits arrive in bursts and the next burst can * afford a cold start (`firstBudgetMs` already assumes one). * * Holders are tracked as objects rather than counts so that `retain` is * idempotent: a manager calls it on every diagnostics pass and stays exactly * one reference, and an idle-evicted entry can be rebuilt without the old * bookkeeping leaking into the new one. */ /** Outcome of asking for a client: ready, or why it cannot be used. */ export type PooledClient = { status: "ready"; client: LspClient; } | { status: "unavailable" | "server_failed"; }; export interface LspClientPoolOptions { idleTtlMs?: number; sweepIntervalMs?: number; } export declare class LspClientPool { private readonly entries; /** * Build counter per key, kept OUTSIDE `entries` so it survives eviction — * that is the whole point: it tells a caller its warm server is gone. */ private readonly generations; private readonly idleTtlMs; private readonly sweepIntervalMs; private sweepTimer?; constructor(options?: LspClientPoolOptions); private keyFor; /** * Get-or-spawn the client for (spec, root) and record `holder` as retaining * it. Concurrent callers share one spawn: the entry and its pending promise * are registered synchronously before the first `await`. */ retain(spec: LspServerSpec, root: string, holder: object): Promise; /** * Identity of the server currently live for (spec, root), or 0 when none is. * * A caller records this alongside its own "I have warmed this server" state, * and a later mismatch means the server it warmed is gone. Reporting the LIVE * entry rather than the next build number is essential: callers ask before * `retain`, when a reclaimed server has not been rebuilt yet, and a stale * number there would look like a match and hand a cold server a warm budget. */ generationFor(spec: LspServerSpec, root: string): number; /** * Mark a diagnostics pass as in flight against (spec, root), returning the * function that ends it. A first-file pass can take seconds; without this the * sweep could shut the server down while it was still answering. */ beginCall(spec: LspServerSpec, root: string): () => void; /** * Record that this client died after starting, so later passes report * `server_failed` instead of talking to a corpse. * * The failure is CACHED rather than cleared, preserving the pre-pool * contract: a server that dies on a document is not respawned on every * following write. The dead process is reaped either way. */ markDead(spec: LspServerSpec, root: string): void; /** * Drop `holder`'s claim on every entry it retained. Servers still held by * another manager keep running; those left with no holders shut down now * rather than waiting for the idle sweep, because a disposed session is * proof the work is over. */ release(holder: object): void; /** Shut every pooled server down regardless of holders (process exit). */ shutdownAll(): void; /** Live pooled servers. Test/diagnostic use. */ get size(): number; /** Reference count for one (spec, root), or 0 when nothing is pooled. */ refCount(spec: LspServerSpec, root: string): number; /** * Retained stderr of a server `holder` is using. Diagnostic probe for tests * and failure reporting — a server's own last words are usually the only * explanation of why it went quiet. */ stderrTail(holder: object): Promise; /** Force one idle sweep. Exposed so tests need not wait on the interval. */ sweepNow(now?: number): void; private evict; /** * The sweep only runs while something is pooled, and is unref'd so it can * never hold the CLI (or a test worker) open on its own. */ private startSweep; private stopSweep; private spawn; } /** The daemon-wide pool. One per process, which is exactly the sharing scope. */ export declare const lspClientPool: LspClientPool; //# sourceMappingURL=pool.d.ts.map