Skip to content

fix(solidlsp): fail the pending request when the stdin write fails - #2030

Merged
opcode81 merged 7 commits into
oraios:mainfrom
feiiiiii5:fix/stdio-send-failure-2004
Oct 6, 2026
Merged

opcode81 merged 7 commits into
oraios:mainfrom
feiiiiii5:fix/stdio-send-failure-2004

Conversation

@feiiiiii5

@feiiiiii5 feiiiiii5 commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

Checklist

  • This PR follows the guidelines in CONTRIBUTING.md regarding the scope of PRs.
  • For changes that add features or fix problems, I have added an entry to CHANGELOG.md, which concisely describes the change.

Summary

Fixes #2004: StdioLanguageServer._send_payload swallowed a failed stdin write and returned, leaving the request registered moments earlier in _pending_requests until its timeout (300 s via DEFAULT_LS_REQUEST_TIMEOUT, shorter for servers that call set_request_timeout, unbounded when the timeout is None). A failed write means the server is gone, so the pending requests are now failed immediately with LanguageServerTerminatedException, the signal the restart path acts on.

Changes

src/solidlsp/ls_process.py, StdioLanguageServer._send_payload:

  • on write failure, _cancel_pending_requests(LanguageServerTerminatedException("Stdio send error", self.ls_id, cause=e)) rather than a bare return. Two precedents in this file already do exactly this: TCPLanguageServer._send_payload on a send error, and the stdout-reader death path.
  • ValueError joins the caught exceptions. A stdin that _stop's _safely_close_pipe (line 555) already closed raises ValueError: I/O operation on closed file, which is not an OSError, so that write failure still escaped and stranded the request. Addressed in response to @AmirF194's report and reproduction.
  • the process and its stdin are bound to locals instead of reading self._process.stdin after the guard. _stop closes stdin, waits for the process inside terminate(timeout=...), and only then nulls _process, so a send from another thread can run the guard and the write against two different states; TCPLanguageServer._send_payload binds its socket to a local for the same reason.

The method still returns normally when the write fails, so nothing new is raised into callers, and the success path is unchanged.

test/solidlsp/test_stdio_send_failure.py, four tests:

  • a broken pipe fails the pending request with LanguageServerTerminatedException, preserves the cause, and clears the table.
  • a stdin closed by _stop's own _safely_close_pipe does the same with a ValueError cause. This one uses a real subprocess pipe rather than a stand-in, since whether a write hits closed-file semantics is the interpreter's decision.
  • a failing write never raises out of _send_payload.
  • a healthy write leaves the request pending.

CHANGELOG.md: entry under "General" in the Unreleased section.

The branch sits on main c4dc91a7. The repository relicensed by component while this PR was open; src/solidlsp and test/solidlsp stay MIT, and the new test file carries # SPDX-License-Identifier: MIT as CONTRIBUTING.md requires for new files.

Testing

All four tests against the base c4dc91a7 with only this test file copied in: 3 failed, 1 passed. That run pins PYTHONPATH to the base tree and prints solidlsp.__file__ as /private/tmp/r24-serena-base/src/solidlsp/__init__.py, because the editable install otherwise resolves solidlsp to the branch checkout and the base run proves nothing. The failing three are test_broken_pipe_fails_pending_request_with_terminated, test_closed_stdin_fails_pending_request_with_terminated and test_failed_write_does_not_raise_from_send_payload; the closed-stdin failure is ValueError: I/O operation on closed file and the two that assert the request is failed reach TimeoutError: Request timed out (timeout=10), which is the stranding from #2004. Against the current head d98a59ad: 4 passed, re-measured in a fresh Python 3.11.15 venv created from this branch (uv pip install -e ".[dev]"), with solidlsp.__file__ printed as /private/tmp/r24-serena-2030/src/solidlsp/__init__.py so the run is known to have exercised this tree. The other unmarked tests that drive this module also pass on the changed tree: test_content_modified_retry.py, test_pdeathsig.py and test_process_group_cleanup.py together report 20 passed, 2 skipped, where the skips are PR_SET_PDEATHSIG is Linux-specific.

Mutation: taking ValueError back out of the except tuple fails exactly the closed-stdin test and leaves the other three passing, so that test is pinned to the claim. Restoring the second self._process.stdin read changes no test result; the local bindings are deliberately untested, because a test for them would be implementation-coupled, which .serena/memories/critical_info.md rules out. ls_process.py was restored to byte-identical after each mutation (sha256 de0bc405ef4d7564 on the current head, re-checked after the rebase).

Gates on the current head: ruff check and ruff format --check at the pinned 0.12.5 and ty check at the pinned 0.0.24, all clean on both changed Python files (ty check --python <venv python> test/solidlsp/test_stdio_send_failure.py src/solidlsp/ls_process.py -> "All checks passed!"). Project-wide, ty check reports the same 49 diagnostics with identical text on main c4dc91a7 and on this branch, and ruff check . the same 20, so nothing here adds to what main already carries.

Corrections to this description: an earlier version said "up to 235 s with Serena's defaults", a figure that does not exist (the default is DEFAULT_LS_REQUEST_TIMEOUT = 300.0), and referred to _subscribe(None), which is not part of this codebase. Both are gone.

Not PR-related failures

niche (ubuntu-latest) was red on the pre-rebase head, and it is red for a reason that predates the PR: the failing step runs uv run poe test -m "julia or r or perl or lean4 or nix or ocaml" and reports 2 failed, 55 passed, the two failures being test/solidlsp/r/test_r_basic.py::TestRLanguageServer::test_find_definition_across_files[r] and ...::test_find_references_across_files[r] (the R server returns empty locations/references). Main reports the same two failures with the same counts: run 34983624289 on 704e8c3d and run 34887825086 on 18fa47bf. The last green on that job is 09-08. What this PR does run in CI: the catch-all batch takes everything outside the named language groups, and on the pre-rebase head its ubuntu-latest job reported 1354 passed, 13 skipped with all four of test/solidlsp/test_stdio_send_failure.py PASSED, and catch-all (all three OSes) is also where ruff and ty run, all green. On d98a59ad that evidence has not been re-produced by CI, since these jobs are still in progress; the local re-measurement above is what covers the new head.

jvm (windows-latest) has since gone red on d98a59ad with test/solidlsp/nextflow/test_nextflow_basic.py::TestNextflowLanguageServer::test_document_symbols_workflows[nextflow] and ::test_full_symbol_tree[nextflow], both reporting an empty Nextflow symbol tree. I am reporting rather than explaining it: the same job is green on c4dc91a7, was green on the pre-rebase head carrying this exact ls_process.py blob, and is green on the two other open PRs in this repo; the Nextflow server on a Windows runner is not something I can reproduce here. A failed write on this code path surfaces as LanguageServerTerminatedException or the caller's own timeout rather than as a returned-but-empty tree, which is the shape these two failures have, but I have not established what did cause them. A re-run of that job would settle whether it is transient.

Signed-off-by: fei [email protected]

@CLAassistant

CLAassistant commented Sep 14, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Comment thread src/solidlsp/ls_process.py Outdated
@@ -662,8 +662,11 @@ def _send_payload(self, payload: StringDict) -> None:
self._process.stdin.writelines(msg)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ran this against 6e9d8dc in a clean python:3.11-slim container, mocking a real closed pipe instead of a MagicMock so writelines hits actual closed-file semantics.

Line 662: writing to a stdin that _stop's _safely_close_pipe (line 565) already closed raises ValueError: I/O operation on closed file, not one of the caught (BrokenPipeError, ConnectionResetError, OSError). _send_payload's guard at line 654 only checks for None, not .closed, so in the window between _safely_close_pipe(self._process.stdin) and _stop's finally: self._process = None (lines 554-561), a concurrent send_request hits this. The exception propagates out of _send_payload uncaught, so the just-registered request is never passed to _cancel_pending_requests and stays in _pending_requests until its full timeout, the exact failure mode #2004 and this PR are otherwise fixing.

raised: ValueError('I/O operation on closed file.')
pending_requests after call: {1: Request[id=..., status='pending', request_id=1, method='textDocument/hover']}

Adding ValueError to the except tuple, or checking self._process.stdin.closed in the guard, would close this the same way the PR already closes the broken-pipe case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed on 6e9d8dc with a real pipe rather than a stand-in: _safely_close_pipe(self._process.stdin), then _send_payload raises ValueError('I/O operation on closed file.') out of the call, and the entry is still in _pending_requests afterwards. Through send_request on another thread the caller gets that ValueError and the request is never cancelled, so both halves of your description reproduce.

Addressed in 900a774, which takes the first option: ValueError is now in the caught tuple, so a closed stdin is routed to _cancel_pending_requests exactly like a broken pipe. I left the guard alone because a guard that only checks .closed and returns is the thing that strands the request here; failing the request is the point of the change.

There is a second window next to yours in the same three lines. _stop waits inside terminate(timeout=...) and only nulls _process in its finally, so _process stays set for the whole termination, and the function read self._process.stdin twice (guard, then write). A thread switch between those reads turns this into AttributeError instead. The process and its stdin are bound to locals once now, which is what TCPLanguageServer._send_payload already does with its socket.

The new test uses a real subprocess whose stdin is closed by _stop's own _safely_close_pipe. It fails with the ValueError on 6e9d8dc and passes on 900a774, and taking ValueError back out of the tuple fails only that test. Restoring the double read changes no test result, so the local bindings stay untested rather than pinned to the shape of the code.

One thing your report sent me back through the repo's own gates: this PR had no CHANGELOG.md entry, though the PR template's checklist asks for one, and the "up to 235 s" figure I had written in the summary does not exist anywhere in the code (the default is DEFAULT_LS_REQUEST_TIMEOUT = 300.0). Both corrected in 900a774 and in the description.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed: 900a774/c514edac catches ValueError and the local bindings close the second window too. Ran test_stdio_send_failure.py at c514eda, 4 passed.

@feiiiiii5
feiiiiii5 force-pushed the fix/stdio-send-failure-2004 branch from 900a774 to c514eda Compare September 15, 2026 19:53
@feiiiiii5

Copy link
Copy Markdown
Contributor Author

Rebased onto main (704e8c3d) to clear the dirty state the CHANGELOG entry caused once the licensing branch landed. Nothing semantic moved: src/solidlsp and test/solidlsp remain MIT under the new component split, the only additions in the rebased commit are the resolved CHANGELOG block and the # SPDX-License-Identifier: MIT header on the new test file, which CONTRIBUTING.md now requires for new files.

Head is now c514eda. Re-measured on the rebased tree: the four tests pass, dropping ValueError from the caught tuple still fails only test_closed_stdin_fails_pending_request_with_terminated, and ruff check / ruff format --check at 0.12.5 plus ty check at 0.0.24 are all rc=0. The description carries the updated hashes.

@feiiiiii5

Copy link
Copy Markdown
Contributor Author

Rebased onto current main (c6fbd1c5) to clear the dirty merge state — that was the only thing blocking this. Head is now 7e5f463a.

Nothing semantic moved. git diff --name-only c6fbd1c5...HEAD is still exactly the three files, and the two code blobs are byte-identical to the c514edac you tested:

file blob vs c514edac
src/solidlsp/ls_process.py b4c2e94d63aa… identical
test/solidlsp/test_stdio_send_failure.py b7da2056f23a… identical

The only content change is the CHANGELOG.md entry, which conflicted because 11 upstream commits touched it since the old base. Resolved by keeping every upstream entry and inserting this one after the session-id bullet; git diff c6fbd1c5...HEAD -- CHANGELOG.md is 4 added lines and nothing else.

Re-measured on the rebased tree, fresh venv on Python 3.11 (project deps installed from pyproject.toml, solidlsp resolved from this checkout):

pytest test/solidlsp/test_stdio_send_failure.py   ->  4 passed in 0.24s
ruff check <the two files>                        ->  rc=0
ruff format --check <the two files>               ->  rc=0, "2 files already formatted"

Mutation, to confirm the suite still bites here: removing ValueError from the caught tuple at ls_process.py:671 gives 1 failed, 3 passed — test_closed_stdin_fails_pending_request_with_terminated is the one that fails, same as your container run.

One thing I should say rather than let it pass: the part you called "the local bindings close the second window" is not covered by a test. I checked — reverting the single-read of self._process in _send_payload (lines 659-662) to a re-read leaves the suite at 4 passed, so nothing here would catch that regression. The binding is still what I believe is correct, and your own run confirmed it, but the claim "tested" would overstate it. A deterministic test for that window needs to flip _process between the two reads, which the current fixture shape can't do; I would rather it be a separate change with its own design than bolt a contrived hook onto this one.

CI on the rebased head is still in flight; I will not treat it as green until the runs report.

@feiiiiii5

Copy link
Copy Markdown
Contributor Author

Follow-up commit 901ac17e, test file only.

After the rebase the Type-checking with ty step went red on this branch (it is green on up/main c6fbd1c5, and the three errors CI reports are all in test/solidlsp/test_stdio_send_failure.py):

error[unresolved-attribute]: Attribute `stdin` is not defined on `None` in union `ManagedSubprocess[bytes] | None`
error[unresolved-attribute]: Attribute `closed` is not defined on `None` in union `IO[bytes] | None`
error[unresolved-attribute]: Unresolved attribute `side_effect` on type Overload[...]

They were latent in my test code before and surfaced once upstream made _process a ManagedSubprocess[bytes] | None; I reproduced unresolved-attribute locally on the same three lines with the pinned ty==0.0.24, though my own invocation reported a different total from CI's, so I treated CI's list as the target rather than my local count. The fix is mine to make either way.

Two changes, both inside the test:

  • the closed-stdin case now binds process = server._process / stdin = process.stdin and asserts each is not None before using it, instead of walking server._process.stdin twice. Same scenario, same assertions, and the precondition is now explicit in the test rather than assumed.
  • test_healthy_write_leaves_request_pending no longer reaches through the typed attribute to undo a side effect. The helper takes None to mean "the write succeeds": _server_with_failing_stdin(None). Behaviour is what it was — the mock's writelines has no side effect — only the expression of it changed.

Re-measured after the change, in this tree:

pytest test/solidlsp/test_stdio_send_failure.py   ->  4 passed in 0.24s
ruff check / ruff format --check <that file>      ->  rc=0, already formatted

No product code moved, so the src/solidlsp/ls_process.py blob is still byte-identical to the c514edac you already ran in the container; if you want to re-verify, the only thing worth your attention is the test helper's new exc: Exception | None parameter. Waiting on the CI run before saying anything about it being green.

feiiiiii5 and others added 6 commits September 19, 2026 20:10
A stdin which `_stop` closed raises ValueError, not OSError, so the write
failure escaped the handler and the request stayed pending. Read the process
and its stdin once so the same race cannot surface as an attribute error.
Adds the CHANGELOG.md entry required by CONTRIBUTING.md.

Reported by @AmirF194 in review.

Signed-off-by: fei <[email protected]>
@feiiiiii5
feiiiiii5 force-pushed the fix/stdio-send-failure-2004 branch from 901ac17 to d98a59a Compare September 19, 2026 12:15
@feiiiiii5

Copy link
Copy Markdown
Contributor Author

Rebased onto current main (c4dc91a7) to clear the dirty merge state, which was the only thing blocking this. Head is now d98a59ad.

main moved by two commits since the previous base: the MCP SDK v2.2.0 upgrade and the is_ignored_path traversal change. Neither touches src/solidlsp/ or test/solidlsp/ (git log --name-only c6fbd1c5..c4dc91a7 lists only src/serena/, pyproject.toml, uv.lock, CHANGELOG.md and test/serena/), so the conflict was in CHANGELOG.md alone.

Nothing semantic moved. Both code blobs are byte-identical to the 901ac17e that was pushed before:

file blob vs 901ac17e
src/solidlsp/ls_process.py b4c2e94d63aa… identical
test/solidlsp/test_stdio_send_failure.py 4045b4b68db6… identical

The only content change is the changelog entry, which collided with the #2077 performance bullet landing at the same position. git diff c4dc91a7...HEAD -- CHANGELOG.md is 4 added lines and nothing removed, so every upstream entry is intact.

Re-measured on the rebased tree, fresh venv (uv pip install -e ".[dev]", Python 3.11.15):

  • base c4dc91a7 with just this test file copied in: 3 failed, 1 passed. This needed PYTHONPATH pinned to the base tree, with solidlsp.__file__ printed to confirm it; without that the editable install resolves solidlsp to the branch checkout and the "base" run reports 4 passed while having tested the fixed code. I hit exactly that on the first attempt and discarded the result.
  • head d98a59ad: 4 passed, test_content_modified_retry.py + test_pdeathsig.py + test_process_group_cleanup.py: 20 passed, 2 skipped (Linux-only).
  • ruff check / ruff format --check at 0.12.5 clean on both changed files; ty check at 0.0.24 "All checks passed!" on both. Project-wide, ty reports the same 49 diagnostics with identical text on c4dc91a7 and here, and ruff check . the same 20, so this branch adds nothing to what main already carries.
  • git merge-tree --write-tree up/main HEAD is clean, and the API now reports mergeable: true / mergeable_state: unstable where it said dirty.

CI is running on d98a59ad: uv sync (3.11/3.12/3.13/14), build and the spelling job are green, the catch-all, niche, jvm, native and other-langs matrices were still in progress when this was posted, so the catch-all numbers in the description are from the pre-rebase head and are not re-claimed for this one.

The description's base/head hashes and the CI attribution have been updated to match.

@feiiiiii5

Copy link
Copy Markdown
Contributor Author

Follow-up on the CI state of d98a59ad, since one job turned red after the comment above.

jvm (windows-latest) fails test/solidlsp/nextflow/test_nextflow_basic.py::TestNextflowLanguageServer::test_document_symbols_workflows[nextflow] and ::test_full_symbol_tree[nextflow] — 2 failed, 162 passed, 39 skipped, 2148 deselected in 209.42s, both assertions reporting an empty Nextflow symbol tree (job log https://github.com/oraios/serena/actions/runs/35442324023/job/105895171222).

What I can establish from here: the same job is green on c4dc91a7, was green on 901ac17e (which carries this exact ls_process.py blob b4c2e94d63aa…), and is green on #2080 and #2081. The two commits the rebase pulled in touch no src/solidlsp or test/solidlsp file. The failure shape also differs from the one this PR fixes: a write failure on this path produces LanguageServerTerminatedException or the caller's timeout, whereas these two tests received a response whose tree was empty. What I cannot do is reproduce a Windows + Nextflow server locally, so I am not claiming a cause — a re-run of that job would tell us whether it is transient, and if it fails again on a re-run I will treat it as mine and dig in.

Everything else that has finished is green: Codespell, Docs Build, Python Versions, CodeQL, build, the four uv sync jobs, jvm (ubuntu|macos), native (windows-latest), other-langs (macos-latest). catch-all (all three OSes, which is where ruff, ty and test_stdio_send_failure.py run), niche (ubuntu), jvm (windows), native (ubuntu|macos) and other-langs (ubuntu|windows) were still in progress at the time of writing.

@feiiiiii5

Copy link
Copy Markdown
Contributor Author

CI rollup for d98a59ad once the matrices settled, replacing the "in progress" list above (read back from commits/d98a59ad/check-runs at 12:28Z):

  • catch-all (ubuntu-latest) success — that is the job where ruff, ty and test/solidlsp/test_stdio_send_failure.py run, so the four tests and both gates are green in CI on this head, not just locally. catch-all (macos-latest) success; catch-all (windows-latest) was still running when this was posted.
  • niche (ubuntu-latest) success here. The R-server failures described in the description were on the pre-rebase head, and this head does not reproduce them.
  • jvm (ubuntu|macos), native (ubuntu|macos|windows), other-langs (all three OSes), Codespell, Docs Build, Python Versions, CodeQL, build, all four uv sync: success. deploy: skipped.
  • jvm (windows-latest) failure, the two Nextflow symbol tests only, as described in the previous comment.

So one red job, in a language-server group this change does not touch, on the one OS where I cannot run it.

@feiiiiii5

Copy link
Copy Markdown
Contributor Author

Supplementing my 09-20 comment above, because I have since found the evidence that turns "probably not mine" into "demonstrably not mine", and it also answers the re-run question.

Heads that do not contain this change fail the same leg with the same signature. Their src/solidlsp/ls_process.py is byte-identical to main's (blob ae46a02782b3…, i.e. pre-change), and the two nextflow assertions fail exactly as here (2 failed, 162 passed, 39 skipped, Found: []) — jobs 104390489279, 104532935312, 104600000563, 104633136836, 106056610541, 106112245414, 106112261277, 106112264069, the most recent started 2026-09-20T17:23Z.

Conversely, the blob this PR introduces, b4c2e94d63aa…, passed jvm (windows-latest): job 104540410360 on c514edac0 (and also on 7e5f463a, 901ac17e). So the same file content is green on one day and red later on heads that never touched it, which points at the Nextflow server/runner side rather than at this diff — consistent with the didOpen class upstream tracked in #1871 / #1872, and with forcing my new path against a real Nextflow server producing LanguageServerTerminatedException rather than an empty tree.

Two clarifications to what I wrote on 09-20, both of which make this easier to read:

  • My _send_payload change does not raise out of the send; it cancels registered requests and returns. A notification registers nothing, so the didOpen-drop repro fails identically on base code. That is why the failure shape (Found: []) is not reachable from this change.
  • My four new tests were deselected in that Windows job (203 selected on both base and head), so that leg never ran them at all.

I still cannot run Windows here, so I am not naming a root cause — I am only reporting that the evidence does not implicate this diff. Local macOS controls, which I offer as controls and not as Windows evidence: test_stdio_send_failure.py 4/4, nextflow 7/7, scan-flush 2/2.

@feiiiiii5

Copy link
Copy Markdown
Contributor Author

Checking in before this goes stale. fix(solidlsp): fail the pending request when the stdin write fails (head 4595677) — a failed stdin write during a request previously left the request pending forever; it now fails the pending request. CI is green and the CLA check is satisfied.

Is there anything blocking review, or is this simply lower in the queue? I will not ping again.

@opcode81

opcode81 commented Oct 6, 2026

Copy link
Copy Markdown
Member

@feiiiiii5 this is resolving an issue which was closed and marked as invalid.
Please read the comments in the issue. If you still think something needs to be done, please let me know. Otherwise I will close this.

@feiiiiii5

Copy link
Copy Markdown
Contributor Author

I read the thread. @tagawa0525's point stands, and I have reproduced it on current main (b8b40955) rather than only quoting his 2026-09-14 transcript.

Your reading covers the case you were describing — a request already pending when the server dies is failed by the reader thread. What is left is the request registered after that thread has exited, and nothing on main can fail it: _send_request_once registers into _pending_requests at ls_process.py:351 and only then writes at :353, _send_payload swallows the write failure with a bare return at :666-669, and _cancel_pending_requests has already run once from the reader thread at :631 ("Cancelling 0 pending"). The only completion paths left are _response_handler (unreachable — the process is gone) and the TCP-only cancellations at :778/:826. So the request is failed only by raise TimeoutError in get_result at :109.

Driving the real StdioLanguageServer against a fake server subprocess that reads stdin and never replies, on b8b40955 with no PR code:

INFO  solidlsp.ls_process: Language server stdout reader thread has terminated
INFO  solidlsp.ls_process: Cancelling 0 pending language server requests
[step4] ls.is_running() == False
DEBUG solidlsp.ls_process: Starting: Request[method='textDocument/references', status='pending', request_id=1]
ERROR solidlsp.ls_process: Failed to write to stdin: [Errno 32] Broken pipe
[step5] exception type : builtins.TimeoutError
[step5] elapsed        : 8.00s (request timeout = 8.0s)
[after] _pending_requests == {1: Request[... status='pending' ...]}

Same script, only ls_process.py swapped for this branch: SolidLSPException with cause=LanguageServerTerminatedException: Stdio send error; Cause: [Errno 32] Broken pipe, elapsed 0.00s, _pending_requests empty.

Two precisions, since #2003 is the natural place to push this back to. This is not the #2003 claim — that was about a timeout reaching the restart path, and you and @tagawa0525 agreed it should not: a response that times out does not imply the server is broken. This PR never turns a timeout into a restart; it only converts a detected pipe failure into LanguageServerTerminatedException, which is what TCPLanguageServer._send_payload already does on a send error at :778. A request that times out for any other reason still raises a bare TimeoutError.

Reproduction and what I did not run

fake_ls.py consumes LSP-framed stdin and never replies; repro.py starts it via ProcessLaunchInfo, waits for the reader thread to exit, SIGKILLs nothing further, checks is_running(), then calls ls.send.references(...). Both are short files if you want to run them.

I did not exercise the whole tool layer (tools_base.apply → restart_language_server → retry); that the exception reaches the restart path rests on ls_exceptions.py:23-30 and tools_base.py:349-357, read but not executed.

git log --oneline --since=2026-09-14 -- src/solidlsp/ls_process.py shows only the two licensing commits (6707cd9b, 18fa47bf), so none of this changed under us in the three weeks the PR has been open. There is no test on main touching this path (grep -rl "Failed to write to stdin" test/ finds nothing).

The abandoned Request still sitting in _pending_requests after the timeout is the same class of leak as #2003's last comment.

If you would rather this not be a PR — it is a behaviour change to an error path, and #2004 is closed as invalid, so the bar for reopening it is yours, not mine — say so and I will close it. What I would ask is that it is not closed as "the reader thread already handles it", because that is the part I can now show is true for the pending case and false for this one.

@opcode81
opcode81 merged commit 2e1bdf5 into oraios:main Oct 6, 2026
25 checks passed
@opcode81

opcode81 commented Oct 6, 2026

Copy link
Copy Markdown
Member

Thanks for your contribution. My point was that this issue cannot really occur in the context of Serena, but since we should treat SolidLSP as a library (which could be used independently), the change you proposed makes sense.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A failed write to the language server's stdin is logged and swallowed; the request then waits for the full timeout

4 participants