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

+37 -1
View File
@@ -1,7 +1,7 @@
import { NodeServices } from "@effect/platform-node";
import { assert, describe, it } from "@effect/vitest";
import { Effect, FileSystem, Layer } from "effect";
import { RequestType } from "vscode-languageserver-protocol";
import { NotificationType, RequestType } from "vscode-languageserver-protocol";
import { Diagnostics } from "../src/lsp/Diagnostics.ts";
import { Documents } from "../src/lsp/Documents.ts";
import * as Editor from "../src/lsp/Editor.ts";
@@ -12,6 +12,7 @@ const State = new RequestType<null, { open: number; closes: number }, void>(
"fixture/state",
);
const Progress = new RequestType<number, string, void>("fixture/progress");
const Crash = new NotificationType<null>("fixture/crash");
const TestLayer = Layer.mergeAll(
Documents.layer({ capacity: 2, languages: {} }),
@@ -168,4 +169,39 @@ describe("Editor", () => {
);
},
);
it.layer(
Layer.mergeAll(
Documents.layer({ capacity: 2, languages: {} }),
Diagnostics.layer,
).pipe(
Layer.provideMerge(
workspace({ "a.ts": "const alpha = 1\n" }, { restart: 1 }),
),
Layer.provideMerge(NodeServices.layer),
),
{ excludeTestServices: true },
)("when the server crashes", (it) => {
it.effect(
"starts it again with the same documents open, until the policy gives up",
() =>
Effect.gen(function* () {
const lsp = yield* LanguageServer;
const hover = Editor.hover({
file: "a.ts",
line: 1,
symbol: "alpha",
});
yield* hover;
yield* lsp.notify(Crash, null);
assert.strictEqual((yield* hover)?.contents, "word **alpha**");
assert.strictEqual((yield* lsp.request(State, null)).open, 1);
yield* lsp.notify(Crash, null);
const error = yield* Effect.flip(hover);
assert.match(error.message, /1 crash in a row/);
}),
);
});
});