lsp implementation
The five file-targeted tools use file_path: LspDiagnostics, LspGoToDefinition, LspFindReferences, LspHover and LspDocumentSymbol. LspWorkspaceSymbol searches by query and has no file target. The file argument passes through lspPath; host paths use host authorization, managed paths use guest POSIX rules, and computer paths use the selected target's OS rules. Protocol URIs use that same OS, including Windows drive and UNC paths. Document identifiers normalize drive-letter case and equivalent percent encoding before matching diagnostic publications. The tool schema baseline pins the new names without old-name aliases.
Declared HTTP patterns
Core 0.30.4 and shared API 14 match GET /brain/lsp, POST /brain/lsp/install and POST /brain/lsp/uninstall as complete patterns. An undeclared suffix returns dispatcher 404; handlers receive an empty req.path and need no exact-path wrapper. Core retains user access for status and administrator access for package mutations. LSP retains manager availability and command validation.
Module ownership
Path resolution, allowed roots, access scope, administrator admission and manager selection await the live host authority. Tools and after-edit diagnostics finish these checks before acquiring a language server; asynchronous denials never become a host-path fallback.
Conversation settlement fences acquisitions in the existing slot map. When no slot exists yet because a path guard is still resolving, settlement records a null slot; the late acquisition consumes that fence and returns without constructing a manager. Existing builds retain their slot-identity fence. Service stop clears the map after cleanup, including settlement fences for conversations that never acquire.
The plugin keeps project-root discovery in src/paths.ts, alongside canonical path and containment checks. LspManager calls projectRootForFile(path, boundary) before acquiring a server; discovery chooses the nearest marker within the permitted boundary. If no marker exists, it uses that boundary or the file's own directory. It never replaces an absent boundary with the daemon's working directory.
src/manager.ts owns acquisition, diagnostics queues, cancellation, crash budgets, idle eviction and unconfirmed closing attempts. Its diagnostic and intelligence outcomes share one failure-reason type, with no-response reserved for diagnostic checks. src/format.ts renders those outcomes and the raw location, hover and symbol results for src/tools.ts and src/afterEdit.ts. Diagnostic and intelligence explanations retain their distinct wording; diagnostics show up to 20 entries, locations and workspace symbols up to 30, and document symbols up to 50 per level.
src/client.ts owns the protocol, document versions and the document LRU cache. Both diagnostic checks and intelligence requests use its private document preparation operation, which creates the next version and invalidates the previous confirmed verdict without pushing that document. Diagnostics register their waiter before sending didOpen or didChange; intelligence requests send the prepared document before their request. Unchanged confirmed diagnostics and unchanged intelligence documents retain their separate cache checks. New documents evict the oldest idle entry and send didClose; active diagnostic documents remain protected until their checks settle.
All six tools in src/tools.ts wrap their final text with textResult(text) from elowen-plugin-shared/toolResult, producing one text block and empty details. Formatting, position validation, path refusals and stopped-manager wording stay in the plugin. The shared helper performs no I/O or error classification; existing caught path errors remain text and uncaught manager or transport failures still propagate.
Managed file reads
ManagedLspManager reads source with readGuestFile(files, guestLspPath(path), { maxBytes: GUEST_FILE_CHUNK_BYTES }) from elowen-plugin-shared/guestFiles. The 512 KiB language-server read limit remains unchanged. The client owns bounded paging, one content version and final multi-page validation. Its callback resolves and checks the live Sandbox authority on every operation, preserving the selected Project, acting account and expected runtime generation. Missing Sandbox or revoked access fails the request; the plugin constructs no guest operation literals. Process preparation, lease cleanup and protocol handling retain their existing owners. Requires core 0.30.4 and shared API 14. Editor and OneDrive consume the same shared client.
Local execution boundary
ManagedLspManager owns both managed and paired-computer streams. A local conversation selects it with { kind: 'local', projectId } and the authenticated account: the ref names the paired computer's Project, and core resolves that Project to the device. Its slot is isolated by conversation, account, Project and working directory. Automatic checks and all six tools resolve the same slot. Pending diagnostics carry that scope too, so switching account, computer or directory cannot deliver another scope's verdict.
The manager resolves Sandbox live before file reads, inventory, process preparation and warm workspace queries. localLspScope awaits the host's access descriptor, account, target metadata and working directory. The slot and API probe pass this resolved scope into the manager constructor; construction never reads asynchronous host authority synchronously. resolveLocalTarget supplies OS, connection and device metadata; its projectId must be the ref's Project, so a provider answering for another computer is refused before the first read or launch. localTargetPath from elowen-plugin-shared/localExecution supplies path semantics. The conversation's computer working directory is the workspace root; a file outside it uses its own directory. No computer path passes through daemon filesystem canonicalization or host marker discovery. The target authorizes each operation; lexical containment only selects the language-server workspace.
Source reads bind readGuestFile to Sandbox.localFiles({ target, accountUserId, sessionId, operation }) with the same 512 KiB limit as managed reads. Processes use Sandbox.prepareExecution({ projectRef, command: { type: 'argv', file, args }, cwd, leaseKind: 'lsp' }). Only a mode: 'local' stream for that Project and account is accepted. LSP reuses the managed session's framed stdin/stdout, heartbeat and confirmed cleanup. The server receives processId: null: the daemon PID cannot supervise a process on another computer. No alternative host launch is attempted.
Inventory executes on the computer through Sandbox.runCommand. Windows queries PowerShell's application and script lookup and parses a JSON array; POSIX computers use sh and command -v with a bounded, allowlisted line list. Managed inventory retains its Python probe. Missing binaries remain an explicit not-installed result. The local executor owns Windows argv handling, including npm command shims; the LSP plugin does not wrap stdio in a second shell transport.
Socket loss ends the admitted stream, rejects its pending requests and releases its lease. A later explicit call may create a new server; no lost request or package mutation is replayed. An install/uninstall failure with an unknown outcome propagates unchanged. Conversation settlement, abort, idle expiry, target replacement and plugin shutdown retain the existing teardown path. There is no persistent local server record to restore after daemon restart. Without Sandbox, an account, a conversation binding or a connected target, local LSP is unavailable and never uses the daemon's files or binaries.
tests/lspLocalTarget.test.ts uses fake local process and file transports for tools, diagnostics, Windows URIs, scope replacement, authorization changes, cancellation, disconnect and unknown package outcomes. Core must admit LSP leases and keep the original process stream alive between turns. Release validation must also exercise npm shims and byte-preserving UTF-8 stdio on an actual Windows client.
Process integration verification
Managed inventory fixtures use tests/helpers/managedSession.ts::childSession. An EventEmitter child closes its PassThrough stdout and stderr when it emits close, supplying the EOF that a real ChildProcess owns. The host command runner waits for both output streams to drain as well as the exit verdict before releasing its lease; long-lived language-server fixtures retain open streams until their own close event. Real child pipes are never ended by the helper. The scripted language server discards a queued reply when cancellation has already ended its stdout, matching a terminated process rather than writing after EOF.
The real TypeScript runner-death integration test requires an existing installation selected with ELOWEN_LSP_DIR. Run tests/lspProcessLifetime.test.ts with that variable set after building this plugin; it checks semantic diagnostic 2322 and the exit of every captured descendant. Without an explicitly selected installation, that integration test is not run. The dependency-free process-group test always runs.