fix(lsp): read1() on the session pipe so short frames are not held in the buffer - #118
Merged
josephsenior merged 1 commit intoAug 2, 2026
Conversation
… the buffer `LspSession._read_loop` calls `proc.stdout.read(4096)`. `Popen` hands back a `BufferedReader`, and `BufferedReader.read(size)` keeps blocking until it can fill the whole requested buffer or the stream reaches EOF. A language server is a long-lived process, so it never closes stdout. When the server's reply is shorter than 4096 bytes the reader blocks with a complete JSON-RPC frame already sitting in the pipe, while the server waits for the `initialized` notification that only arrives after the client parses the initialize response. Both sides wait for each other until the init timeout fires. Measured against typescript-language-server: 234 bytes of `window/logMessage` plus a 1,814-byte initialize response = 2,048 bytes total, well short of 4096. Every `lsp_query` then reported `available=false` with `latency_ms` pinned to the 20s init timeout and fell back to grep, even though the server had answered in ~0.1s. `read1()` returns after a single underlying pipe read, so a complete frame is delivered as soon as it arrives. `getattr(..., 'read1', ...read)` keeps a fallback for file-like test doubles and unusual Popen configurations. Adds a regression test that spawns a helper process which writes one short initialize response and then sleeps, reproducing the deadlock. It fails on the unpatched reader (init times out) and passes with `read1()`. Verified: backend/tests/unit/utils passes (54 LSP-related tests). The unrelated failures in validation/orchestration/tui reproduce identically on a clean `main` in this environment and are missing pytest plugins, not regressions from this change.
Reviewer's GuideAdjusts the LSP session stdout reader to use a non-blocking, pipe-friendly read method so short JSON-RPC frames are delivered promptly, and adds a regression test to ensure short initialize responses don’t deadlock initialization. Sequence diagram for updated LspSession stdout read loopsequenceDiagram
actor Client
participant LspSession
participant LanguageServer
participant BufferedReader
Client->>LspSession: start_session()
LspSession->>LanguageServer: send_initialize()
LanguageServer->>BufferedReader: write initialize_response (<= 2048 bytes)
LspSession->>LspSession: _read_stdout()
LspSession->>BufferedReader: getattr(stdout, read1, read)
alt before_fix
LspSession->>BufferedReader: read(4096)
BufferedReader-->>LspSession: [blocks until buffer full or EOF]
Client-->>Client: init timeout fires
else after_fix
LspSession->>BufferedReader: read1(4096)
BufferedReader-->>LspSession: chunk (complete JSON-RPC frame)
LspSession->>LspSession: parse_frame()
LspSession->>LanguageServer: send_initialized()
Client-->>Client: LSP query completed
end
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
LspSession._read_loopreads the language server's stdout withproc.stdout.read(4096).Popenreturns aBufferedReader, andBufferedReader.read(size)blocks until it can fill the whole requested buffer or the stream hits EOF. A language server is a long-lived process, so it never closes stdout.When the server's reply is shorter than 4096 bytes, the reader blocks with a complete JSON-RPC frame already sitting in the pipe, while the server waits for the
initializednotification that the client only sends after parsing the initialize response. Both sides wait on each other until the init timeout fires.Repro
Configure any stdio language server and run an
lsptool call:Measured against
typescript-language-serveron a small JS workspace:window/logMessageid=1)initialized2,048 < 4096, so
read()never returns.latency_mslands exactly on the 20s_DEFAULT_INIT_TIMEOUT_SEC.Meanwhile the server itself is healthy — driving the same binary directly over JSON-RPC from the same cwd answers
initializein ~0.1s.Fix
read1()returns after a single underlying pipe read, so a complete frame is delivered as soon as it arrives. Thegetattrkeeps aread()fallback for file-like test doubles and unusualPopenconfigurations.Test plan
backend/tests/unit/utils/test_lsp_session_reader.py— spawns a helper process that writes one short initialize response then sleeps, reproducing the deadlock.LSP initialize timed out) and passes withread1()— verified by reverting the one-line change and re-running.pytest backend/tests/unit/utilspasses (54 LSP-related tests).lsptool calls returnLSP query completed.andfind_referencesyields correct line numbers instead of falling back to grep.Summary by Sourcery
Prevent LSP session stdout reader from blocking indefinitely on short JSON-RPC frames by using a non-buffer-filling read method.
Enhancements:
Tests: