BUG-HUNT: [error-handling] LspRuntime.get_diagnostics() and get_completions() skip textDocument/didClose on exception — LSP server accumulates permanently open documents #6581

Open
opened 2026-04-09 21:45:24 +00:00 by HAL9000 · 0 comments
Owner

Bug Report: [error-handling] — Missing try/finally leaves documents open after errors in get_diagnostics and get_completions

Severity Assessment

  • Impact: If the LSP server crashes or the request times out after did_open has been sent, did_close is never sent. The server continues tracking the document as open. On the next get_diagnostics / get_completions call for the same URI, did_open is sent again — many LSP servers reject or warn on duplicate didOpen for the same URI, leading to incorrect server-side state. Over time, documents accumulate in the server's open-document set, consuming memory and potentially causing the server to produce stale diagnostics for files the runtime no longer holds.
  • Likelihood: High — LSP server crashes and request timeouts are exactly the scenarios _get_healthy_client is designed to handle (it calls restart_server). When the server crashes mid-request, the exception path is the normal code path.
  • Priority: High

Location

  • File: src/cleveragents/lsp/runtime.py
  • Function: LspRuntime.get_diagnostics() (lines 148–164) and LspRuntime.get_completions() (lines 209–224)

Description

get_diagnostics and get_completions both follow this pattern:

  1. Call client.did_open(uri, ...) — notifies server the document is open.
  2. Perform the query operation (may raise).
  3. Call client.did_close(uri) — notifies server the document is closed.

If step 2 raises an exception (server crash, timeout, LspError), step 3 is never reached.

In contrast, get_hover() (lines 274–278) and get_definitions() (lines 328–332) correctly use try/finally:

# runtime.py lines 275-278 — CORRECT pattern
try:
    hover = client.get_hover(uri, line - 1, column - 1)
finally:
    client.did_close(uri)

This is a code consistency bug — the same pattern is applied inconsistently across four similar methods, and the two broken methods are the most commonly called ones (diagnostics and completions).

Evidence

# runtime.py lines 148-164 — get_diagnostics (MISSING try/finally)
client.did_open(uri, language_id, version=1, text=text)

# Give the server time to process and push diagnostics
diagnostics = client.get_diagnostics(uri)   # ← raises on server crash/timeout

# Close the document — NEVER REACHED if above raises
client.did_close(uri)
# runtime.py lines 209-224 — get_completions (MISSING try/finally)
client.did_open(uri, language_id, version=1, text=text)

# LSP uses 0-based line/character
completions = client.get_completions(uri, line - 1, column - 1)  # ← raises on crash

client.did_close(uri)   # NEVER REACHED if above raises
# runtime.py lines 275-278 — get_hover (CORRECT)
try:
    hover = client.get_hover(uri, line - 1, column - 1)
finally:
    client.did_close(uri)    # Always called
# runtime.py lines 329-332 — get_definitions (CORRECT)
try:
    definitions = client.get_definitions(uri, line - 1, column - 1)
finally:
    client.did_close(uri)    # Always called

Expected Behavior

did_close should always be called after did_open, regardless of whether the intervening operation raises an exception. This matches the LSP specification's expectation that every didOpen notification is paired with a didClose.

Actual Behavior

get_diagnostics and get_completions skip did_close when the query raises an exception, leaving the document in an "open" state on the server.

Suggested Fix

Wrap the query calls in try/finally blocks, mirroring the pattern already used in get_hover and get_definitions:

# get_diagnostics fix
client.did_open(uri, language_id, version=1, text=text)
try:
    diagnostics = client.get_diagnostics(uri)
finally:
    client.did_close(uri)

# get_completions fix
client.did_open(uri, language_id, version=1, text=text)
try:
    completions = client.get_completions(uri, line - 1, column - 1)
finally:
    client.did_close(uri)

Category

error-handling

TDD Note

After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD. The test will use tags: @tdd_issue, @tdd_issue_<this-issue-number>, and @tdd_expected_fail to prove the bug exists before fixing it.


Automated by CleverAgents Bot
Supervisor: Bug Hunting | Agent: bug-hunter

## Bug Report: [error-handling] — Missing `try/finally` leaves documents open after errors in `get_diagnostics` and `get_completions` ### Severity Assessment - **Impact**: If the LSP server crashes or the request times out after `did_open` has been sent, `did_close` is never sent. The server continues tracking the document as open. On the next `get_diagnostics` / `get_completions` call for the same URI, `did_open` is sent again — many LSP servers reject or warn on duplicate `didOpen` for the same URI, leading to incorrect server-side state. Over time, documents accumulate in the server's open-document set, consuming memory and potentially causing the server to produce stale diagnostics for files the runtime no longer holds. - **Likelihood**: High — LSP server crashes and request timeouts are exactly the scenarios `_get_healthy_client` is designed to handle (it calls `restart_server`). When the server crashes mid-request, the exception path is the normal code path. - **Priority**: High ### Location - **File**: `src/cleveragents/lsp/runtime.py` - **Function**: `LspRuntime.get_diagnostics()` (lines 148–164) and `LspRuntime.get_completions()` (lines 209–224) ### Description `get_diagnostics` and `get_completions` both follow this pattern: 1. Call `client.did_open(uri, ...)` — notifies server the document is open. 2. Perform the query operation (may raise). 3. Call `client.did_close(uri)` — notifies server the document is closed. If step 2 raises an exception (server crash, timeout, `LspError`), step 3 is never reached. In contrast, `get_hover()` (lines 274–278) and `get_definitions()` (lines 328–332) correctly use `try/finally`: ```python # runtime.py lines 275-278 — CORRECT pattern try: hover = client.get_hover(uri, line - 1, column - 1) finally: client.did_close(uri) ``` This is a **code consistency bug** — the same pattern is applied inconsistently across four similar methods, and the two broken methods are the most commonly called ones (`diagnostics` and `completions`). ### Evidence ```python # runtime.py lines 148-164 — get_diagnostics (MISSING try/finally) client.did_open(uri, language_id, version=1, text=text) # Give the server time to process and push diagnostics diagnostics = client.get_diagnostics(uri) # ← raises on server crash/timeout # Close the document — NEVER REACHED if above raises client.did_close(uri) ``` ```python # runtime.py lines 209-224 — get_completions (MISSING try/finally) client.did_open(uri, language_id, version=1, text=text) # LSP uses 0-based line/character completions = client.get_completions(uri, line - 1, column - 1) # ← raises on crash client.did_close(uri) # NEVER REACHED if above raises ``` ```python # runtime.py lines 275-278 — get_hover (CORRECT) try: hover = client.get_hover(uri, line - 1, column - 1) finally: client.did_close(uri) # Always called ``` ```python # runtime.py lines 329-332 — get_definitions (CORRECT) try: definitions = client.get_definitions(uri, line - 1, column - 1) finally: client.did_close(uri) # Always called ``` ### Expected Behavior `did_close` should always be called after `did_open`, regardless of whether the intervening operation raises an exception. This matches the LSP specification's expectation that every `didOpen` notification is paired with a `didClose`. ### Actual Behavior `get_diagnostics` and `get_completions` skip `did_close` when the query raises an exception, leaving the document in an "open" state on the server. ### Suggested Fix Wrap the query calls in `try/finally` blocks, mirroring the pattern already used in `get_hover` and `get_definitions`: ```python # get_diagnostics fix client.did_open(uri, language_id, version=1, text=text) try: diagnostics = client.get_diagnostics(uri) finally: client.did_close(uri) # get_completions fix client.did_open(uri, language_id, version=1, text=text) try: completions = client.get_completions(uri, line - 1, column - 1) finally: client.did_close(uri) ``` ### Category error-handling ### TDD Note After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD. The test will use tags: `@tdd_issue`, `@tdd_issue_<this-issue-number>`, and `@tdd_expected_fail` to prove the bug exists before fixing it. --- **Automated by CleverAgents Bot** Supervisor: Bug Hunting | Agent: bug-hunter
HAL9000 added this to the v3.2.0 milestone 2026-04-09 21:52:44 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
cleveragents/cleveragents-core#6581
No description provided.