Restart a crashed language server per --restart never|always|N

The process is supervised: on exit after a successful initialize it is
spawned again with exponential backoff (500ms to 30s), open documents are
re-sent as didOpen before the new session is announced, and requests caught
by the crash wait for it and retry. A crash more than a minute after the
previous one starts a fresh streak; reaching the streak limit (default 3)
ends restarting for good, after which every request fails with an LspError
saying so. Restarts and give-ups are logged and forwarded to the MCP client.

Writes to the child's stdin go through a PassThrough so a broken pipe ends
the session instead of surfacing as an unhandled rejection.
This commit is contained in:
EFLKumo committed 2026-09-20 15:36:30 +08:00
1 parent a89f862f73
commit 1463527870
6 files changed
+441 -146

No files matched your search

+3 -1
View File
@@ -2,7 +2,8 @@
* A miniature language server used by the tests. Words are symbols: hover
* describes the word under the cursor, definition is its first occurrence,
* references and rename cover every occurrence, and each line containing
* "TODO" yields a warning. `fixture/state` reports open/close counts.
* "TODO" yields a warning. `fixture/state` reports open/close counts and
* `fixture/crash` kills the process.
*/
import {
createConnection,
@@ -136,6 +137,7 @@ connection.onDocumentSymbol(({ textDocument }) => {
);
});
connection.onNotification("fixture/crash", () => process.exit(1));
connection.onRequest("fixture/state", () => ({
open: documents.all().length,
closes,
+6 -2
View File
@@ -1,11 +1,14 @@
import { fileURLToPath } from "node:url";
import { Effect, FileSystem, Layer, Path } from "effect";
import { LanguageServer } from "../../src/lsp/LanguageServer.ts";
import { LanguageServer, type Options } from "../../src/lsp/LanguageServer.ts";
const server = fileURLToPath(new URL("./server.ts", import.meta.url));
/** A temporary workspace with the given files, served by the fixture language server. */
export const workspace = (files: Record<string, string>) =>
export const workspace = (
files: Record<string, string>,
options?: Pick<Options, "restart">,
) =>
Layer.unwrap(
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
@@ -18,6 +21,7 @@ export const workspace = (files: Record<string, string>) =>
command: process.execPath,
args: [server],
root,
...options,
});
}),
);