-
Notifications
You must be signed in to change notification settings - Fork 10k
Description
Summary
The builtin ElixirLS LSP integration spawns a new ElixirLS process for every LSP request (symbols, goto_definition, find_references, etc.) instead of maintaining a persistent connection. Each new process must complete its full initialization cycle (installer, mix compile, dialyzer indexing), which takes far longer than the 60s DEFAULT_REQUEST_TIMEOUT_MSEC, so every request times out. Previous processes are never cleaned up, leading to orphaned beam.smp processes accumulating.
Environment
- OS: macOS (Apple Silicon)
- opencode: latest (installed via
~/.opencode/bin/opencode) - Elixir: 1.19.5
- OTP: 28
- ElixirLS: 0.30.0 (installed via Homebrew)
Steps to Reproduce
- Open an Elixir project (with
mix.exs) in opencode - Use any LSP tool on a
.exfile:lsp_symbols,lsp_goto_definition,lsp_find_references - Observe: request times out after 60 seconds
- Run
ps aux | grep beam.smp— multiple orphaned ElixirLS processes exist
Evidence
After making 2 LSP requests:
$ ps -ax -o pid,lstart,command | grep "launch.exs" | grep -v grep | grep -v phx.server
29215 Sat Feb 7 19:16:17 2026 .../beam.smp ... -extra .../elixir-ls/0.30.0/libexec/launch.exs
30594 Sat Feb 7 19:19:59 2026 .../beam.smp ... -extra .../elixir-ls/0.30.0/libexec/launch.exs
Each request spawned a new process (~3 minutes apart). Neither was cleaned up.
Manual Verification
ElixirLS itself works correctly. Sending a proper LSP initialize request via stdin/stdout directly:
printf 'Content-Length: 150\r\n\r\n{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{},"rootUri":"file:///path/to/project"}}' | elixir-lsReturns a full initialize response with all capabilities within seconds.
Root Cause Analysis
Issue 1: No persistent LSP connection
The LSP client appears to spawn a fresh ElixirLS for each tool call rather than maintaining a long-lived connection per workspace root. ElixirLS requires significant startup time (installer check → mix compile → dialyzer indexing), so a per-request spawn model will always timeout.
Issue 2: Server→Client request deadlock (secondary)
ElixirLS sends server→client JSON-RPC requests during initialization (workspace/configuration, window/workDoneProgress/create, client/registerCapability). If the LSP client doesn't respond to these, ElixirLS blocks waiting for replies, preventing it from processing any subsequent client→server requests. This causes a deadlock where the server is alive but unresponsive.
Issue 3: No process cleanup on timeout
When a request times out, the spawned ElixirLS process (beam.smp, ~380MB each) is not killed, leading to resource leaks.
Expected Behavior
- A single persistent ElixirLS connection per workspace root, reused across all LSP requests
- The LSP client should respond to server→client requests (at minimum:
workspace/configuration,window/workDoneProgress/create,client/registerCapability) - On connection failure/timeout, the process tree should be cleaned up
Relevant Code
The builtin config (from strings on the binary):
LSPServer.ElixirLS = {
id: "elixir-ls",
extensions: [".ex", ".exs"],
root: NearestRoot(["mix.exs", "mix.lock"]),
async spawn(root) {
let binary2 = Bun.which("elixir-ls");
return { process: spawn(binary2, { cwd: root }) };
}
};Note: ElixirLS's launch.sh uses exec to relaunch in the preferred shell (zsh), then exec again into the BEAM process. This may interact poorly with Bun.spawn's process tracking if it relies on the original PID.