Final review pass on the local provider:
- Tear the instance down when `initialize` REJECTS (utf-8 negotiation, malformed
result), not only on abort, so a permanently-rejecting `ready` is never pooled.
- Use the group-aware SIGKILL on a framing failure so helpers are reached.
- Validate maxMessageBytes and maxDocumentBytes positive at load alongside the
other byte caps.
- Fix the location renderer's outside-workspace check to match a `..` segment
exactly, so an in-workspace path like `..generated/a.ts` stays relative.
- Document the accepted ancestor-directory symlink-swap TOCTOU under the
trusted-host model (O_NOFOLLOW guards only the final component).
Further lifecycle/safety hardening of the local provider:
- Tear the instance down when the initialize handshake is aborted, so a
poisoned pending `ready` can't make later queries for that workspace re-wait.
- Make the serialized-queue wait itself abortable, so a query blocked behind
hung earlier work can still observe its own timeout.
- Spawn the server detached and signal the whole process group on teardown, so
helper processes (e.g. tsserver) can't outlive dispose().
- Open the source with O_NOFOLLOW and cap the read at maxDocumentBytes+1, closing
the symlink-swap and concurrent-grow windows the fd-based read left open.
- Honor an already-aborted signal before any host I/O or startup.
- Validate maxStderrBytes positive at load; surface the retained stderr tail in
the "language server exited" error so a fatal startup diagnostic is visible.
Lifecycle and safety fixes from the external review:
- Observe abort while awaiting the initialize handshake, so a server that never
replies can't defeat the tool-timeout signal.
- On an aborted request the server won't cancel, tear the instance down after a
bounded grace instead of releasing the serialized queue with work still live
(prevents overlapping document lifecycles).
- Re-check provider disposal after the canonicalize/read awaits so a query can't
spawn an unowned server after disposeAll().
- Read the source through one open handle (stat + read on the same fd) to close
the realpath-vs-read TOCTOU; decode with a fatal UTF-8 decoder so a legitimate
U+FFFD is not misclassified as invalid.
- Validate and read the source BEFORE spawning a server (pre-start rejection).
- Require an explicit openClose for option-form textDocumentSync.
- Reject nonpositive teardown budgets and non-executable absolute commands at
load; surface unsupported operations as structured LSP_UNSUPPORTED_OPERATION.
- Retain the stderr tail (fatal diagnostics land at exit), not the prefix.
- Catalog the seam vocabulary in docs/core-data-structures/lsp.md.
Implements the LSP capability seam RFC as three packages: dsh-lsp (the
ctx.lsp interface — provider registry by branded id + exclusive extension
mapping, per-query order-independent selection, closed request/result
vocabulary, LspError taxonomy), dsh-lsp-local (a generic stdio language-server
provider — Content-Length JSON-RPC framing, per-(provider, workspace) process
single-flight, transient didOpen/query/didClose, an abortable per-instance
queue, UTF-16 negotiation, host-namespace source reads outside ctx.fs, and
bounded shutdown/kill teardown), and dsh-tool-lsp (the model-facing lsp tool —
four operations, one-based UTF-16 cursor conversion, workspace-grouped location
rendering, hover capping, a required session workspace, and a timeout budget).
Why: an agent had text search and file reads but no way to identify a program
symbol — follow an alias, connect an interface to implementations, or read an
inferred type — before changing code. Splitting model contract, seam, and local
subprocess behavior keeps the four semantic queries stable across future remote
or sandbox-native providers without leaking a JSON-RPC escape hatch.