fix(lsp): make transport recovery ownership-safe

This commit is contained in:
Tianyi Cui
2026-07-22 15:56:29 +08:00
parent 2a8e7c661d
commit 769710cfb9
10 changed files with 94 additions and 35 deletions

View File

@@ -2,8 +2,8 @@
* Generic stdio language-server backend for `ctx.lsp`. One plugin instance configures a named table
* of server commands and registers one isolated provider for each entry. Every provider lazily
* single-flights one server process per canonical workspace realpath, serves transient-open queries
* through it, and replaces a transport that dies between a pool liveness check and the next
* read-only query. Providers read sources through Node APIs in the host namespace (not `ctx.fs`)
* through it, and replaces a selected transport that fails before or during the next read-only
* query. Providers read sources through Node APIs in the host namespace (not `ctx.fs`)
* and trust their configured servers — no sandbox confinement.
*
* Namespace plugin (named exports, no default export). Lifecycle is effect-scoped: disposal
@@ -224,16 +224,20 @@ class LocalLspProvider implements LspProvider {
try {
return await instance.query(request, source, signal)
} catch (error) {
// A child can die after the pre-query liveness check but before or during the next write.
// Queries are read-only, so replace a newly failed transport once and retry transparently.
if (!instance.dead) throw error
// A selected child can have died while idle or fail during the next write. Queries are
// read-only, so replace that transport once and retry transparently.
if (!instance.isTransportFailure(error)) throw error
await instance.dispose()
this.evictIfCurrent(workspace, instance)
this.assertActive(signal)
instance = this.instanceFor(workspace)
return await instance.query(request, source, signal)
} finally {
// Drop a crashed slot only when it still owns this instance; a replacement must survive.
if (instance.dead) this.evictIfCurrent(workspace, instance)
// Reach quiescence before dropping a dead slot; a replacement must survive this ownership check.
if (instance.dead) {
await instance.dispose()
this.evictIfCurrent(workspace, instance)
}
}
})
}