Conversation
ibetitsmike
left a comment
There was a problem hiding this comment.
Mux working on behalf of Mike.
I found two blocking concurrency issues in the new LSP manager and transport paths. I left both inline below.
| const client = await this.clientFactory({ | ||
| descriptor, | ||
| runtime, | ||
| rootPath, | ||
| rootUri, | ||
| }); | ||
| workspaceEntry.clients.set(clientKey, client); |
There was a problem hiding this comment.
Mux working on behalf of Mike.
If two lsp_query calls hit the same workspace and root concurrently, both callers can observe no existing client here, both await clientFactory(), and the later one overwrites the earlier client in workspaceEntry.clients. That leaks an orphaned LSP process outside manager tracking. Please dedupe in-flight creation per (workspaceId, clientKey) or otherwise serialize this path, then add a concurrent query test that asserts the factory only runs once.
| const body = this.encoder.encode(JSON.stringify(message)); | ||
| const header = this.encoder.encode(`Content-Length: ${body.byteLength}\r\n\r\n`); | ||
| await this.stdinWriter.write(header); | ||
| await this.stdinWriter.write(body); |
There was a problem hiding this comment.
Mux working on behalf of Mike.
This method writes one LSP frame as two separate awaited writes, header then body. If two requests or notifications share this transport concurrently, the byte stream can interleave as headerA, headerB, bodyA, bodyB, which breaks Content-Length framing. Please serialize writes, or write each framed message in one critical section, and add a concurrent send test for it.
|
hey @terion-name chatted with the team and I have a good/bad news type of situation. in the meantime - you can definitely finish this implementation in your fork and play around with it. |
Add shared workspace LSP diagnostics schemas, backend list/subscribe plumbing, browser WorkspaceStore caching/hooks, and focused coverage for manager/store/server paths.
Reconnect active workspace diagnostics subscriptions after client swaps, retry diagnostics streams when they end unexpectedly, and settle pending LSP diagnostic waits during workspace disposal.
Add a Diagnostics sub-tab to the Stats sidebar and render grouped LSP diagnostics from the workspace store without subscribing while the tab is inactive. Add focused tests for the new diagnostics panel states and the Stats sub-tab list.
Separate LSP server metadata from resolved launch plans, add manager-side launch resolution/provisioning helpers, and cover the new seams with focused tests.
Update the policy-context cache-key test to prove trusted workspaces can still use the repo-local TypeScript server while untrusted workspaces fall back to an external PATH entry after workspace-local PATH entries are sanitized.
Poll tracked LSP client files so workspace diagnostics refresh after out-of-band saves. Add a tracked-file accessor to LspClient, start a manager-owned polling loop, and reuse the existing ensureFile/publishDiagnostics path so Stats → Diagnostics updates without UI or ORPC changes. Add focused coverage for tracked-file exposure, empty-cache polling refreshes, and failure tolerance when a tracked file disappears.
Keep poll-driven diagnostics refreshes from looking idle and reset per-workspace poll/serialization bookkeeping on disposal so recreated workspaces do not inherit stale in-flight state.
Search ancestor directories up to the workspace root for TypeScript and package-manager metadata, and provision a fallback TypeScript package when package-manager exec has no usable workspace tsserver.
Ensure untrusted PATH-based LSP resolution always probes and launches with an explicit sanitized PATH.
Separate persisted config reads from the effective runtime config so MUX_LSP_PROVISIONING_MODE stays read-time only, and copy the effective provisioning mode into mux run's ephemeral config.
Keep the sanitized PATH for untrusted pathCommand probing, but stop copying that synthetic env onto the final launch plan unless the launch policy explicitly requested env overrides. Add a regression test covering untrusted pathCommand launches without an explicit env block.
Use the full workspace path when sanitizing untrusted pathCommand PATH entries so nested package roots cannot inherit repo-level node_modules/.bin entries. Also unwrap MultiProjectRuntime to its primary runtime so host-local wrappers still sanitize while Docker/devcontainer-style wrappers keep their remote PATH untouched.
Handle workspace_symbols queries that target a directory like ./ by inferring the matching built-in server from language-specific root markers, skipping file opens when no concrete file is needed, and failing clearly when the directory matches multiple server types.
Prune package-only TypeScript descendants from directory workspace_symbols discovery, warm TypeScript roots with a representative source file before workspace/symbol requests, and summarize repeated root failures so mixed-language monorepos return more useful results.
Prefer exact symbol-name matches when selecting a TypeScript representative file for directory workspace_symbols warm-up, and rank exact path token matches ahead of loose substring matches. Add a repo-root regression test covering the Teleport ResourceService vs ResourcesService case.
Return one workspace_symbols success item per usable directory root, preserve per-root metadata, and surface clearer no-root/no-usable-root failures. Update tool schemas and focused tests to cover the new contract.
Add structured skipped root metadata, richer symbol fields, and disambiguation hints for directory workspace_symbols queries.
Postprocess Go workspace_symbols results so exact symbol-name matches win by default behind EXPERIMENT_LSP_GO_EXACT_MATCH_SYMBOLS, while preserving the original fuzzy output when the env var is explicitly set to false. Add focused coverage for both single-root and directory workspace_symbols queries.
When any directory workspace_symbols root returns an exact match, drop Go-only fuzzy groups unless that Go root also has an exact match. This keeps nested TypeScript exact hits from getting buried under unrelated gopls noise while preserving the existing env-var escape hatch.
@ibetitsmike I have pushed implementation to usable state. Tested and tuned on complex multi-language monorepo (like Teleport), fine-tuned behaviors and output shapes for proper agentic understanding. In general this stuff has a lot of caveats and needs a lot of case-testing. But if we need to wait architectural changes, at this point it doesn't make sense to continue. It would be great if you in your team also play around with it for some feedback. And when designing extension system to take into consideration this code. Ping me when new arch will be ready, I'll port this PS |
In short
This merge request adds a built-in LSP integration to mux. Implementation is heavily inspired by OpenCode LSP architecture. Currently it auto-provisions TypeScript, Python, Go and Rust lsp's. Feature is gated by experiment flag, auto-provisioning is settings-controlled. Multiple runtimes supported.
I did test what I was able to. It would be great if you also take som time to tackle around with it
In details
It introduces:
lsp_querytool for semantic code navigationThe result is that LSP becomes useful in three places at once:
lsp_queryWhat this adds
1. Built-in backend LSP subsystem
Mux now owns LSP clients in the backend instead of relying on editor/plugin integration.
2. Agent-facing semantic queries
Adds the experiment-gated
lsp_querytool with support for:hoverdefinitionreferencesimplementationdocument_symbolsworkspace_symbolsThis gives the model semantic code navigation instead of grep-only exploration.
3. Automatic post-edit diagnostics
After supported structured edits, mux now refreshes LSP state and returns fresh diagnostics in the tool result.
Currently wired into:
file_edit_replace_stringfile_edit_replace_linesfile_edit_inserttask_apply_git_patchThis creates a practical edit → diagnose → repair loop for the agent.
4. Workspace diagnostics snapshot + live UI
Adds workspace-scoped diagnostics APIs and a minimal UI surface:
workspace.lsp.listDiagnosticsworkspace.lsp.subscribeDiagnosticsThe panel shows:
5. Provisioning and trust policy
Adds policy-driven launch/provisioning for built-in servers with config:
lspProvisioningMode: "manual" | "auto"Built-in policies now cover:
Provisioning is trust-aware:
6. Diagnostics refresh after ordinary editor saves
Adds polling-based refresh for already tracked LSP files, so the diagnostics panel can update after out-of-band edits made in an editor or terminal.
This is intentionally scoped:
7. CLI parity
The CLI participates in the same LSP model:
lsp-queryexperiment wiringmux runArchitecture
Backend responsibilities
LspManagerOwns orchestration:
LspClientOwns protocol interaction:
initialize/shutdowndidOpen/didChange/didClosepublishDiagnosticsLspStdioTransportOwns framed stdio JSON-RPC transport and serialized writes.
Launch policy layer
Launch/provisioning is separated from protocol logic:
lspServerRegistry.tsdefines built-in server policieslspLaunchResolver.tschooses the final launch planlspLaunchProvisioning.tshandles provisioning/probing mechanicsThis keeps:
cleanly separated.
Integration points
Service graph / lifecycle
The LSP manager is wired into the backend service graph and workspace lifecycle, so clients are owned and cleaned up by mux.
Relevant files include:
src/node/services/coreServices.tssrc/node/services/serviceContainer.tssrc/node/services/aiService.tssrc/node/orpc/context.tssrc/node/orpc/router.tsTool system
lsp_queryis added to tool definitions and availability wiring:src/common/utils/tools/toolDefinitions.tssrc/common/utils/tools/tools.tssrc/common/types/tools.tssrc/node/services/tools/lsp_query.tsFile mutation hooks
Structured file-mutating tools now call a shared
onFilesMutated(...)hook, whichAIServicemaps toLspManager.collectPostMutationDiagnostics(...).Frontend store
Diagnostics snapshots flow through:
src/browser/stores/WorkspaceStore.tsThis store subscribes lazily and exposes diagnostics state to the UI.
Settings / config
Provisioning mode is wired through:
CLI
CLI support is wired through:
src/cli/run.tssrc/cli/runOptions.tssrc/cli/runTrust.tsHow it works end-to-end
Explicit agent query
lsp_queryPost-edit repair loop
onFilesMutated(...)AIServiceasksLspManagerfor fresh diagnosticspublishDiagnosticsUser diagnostics UI
publishDiagnosticsWorkspaceStoresubscribes lazilyBuilt-in server behavior
TypeScript
Supports:
typescript-language-servertypescript-language-servertsserver.pathtypescriptwhen neededPython
Supports:
pyright-langserverpyrightGo
Supports:
goplsgo installfallback into mux-managed tools dirRust
Supports:
rust-analyzerTrust and runtime handling
This MR is intentionally conservative about execution safety.
Trust
LSP launch behavior is driven by:
Runtime behavior
Launch behavior is runtime-aware:
pathCommandMultiProjectRuntimeis unwrapped so trust/path decisions follow its primary runtime behaviorPath mapping
LspPathMapperkeeps host/runtime paths aligned so LSP can run inside the runtime environment while mux still reports useful paths to agents and UI.Known limitations
This MR intentionally does not add:
The UI surface is intentionally small:
The polling refresh is also intentionally narrow: