[BUG] Interactive LSP client never sends didChange after Edit-tool writes — server buffers stay frozen at first-query content (headless `-p` syncs correctly)

Status Open
Reported on v2.1.215
Maintainer reply None cached
Activity 0 comments · opened Jul 21, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

In an interactive session, the LSP client (plugin-registered server via
.lsp.json) sends textDocument/didOpen the first time a file is queried and
then never notifies the server of any write again — no didChange, no
didSave — not even for changes made by Claude's own Edit tool. The server's
buffer stays frozen at first-query content for the entire session, so every
LSP answer after the first write silently describes a past version of the
file: findReferences misses new call sites, hover reports old signatures,
documentSymbol lists renamed symbols under their old names. In one measured
session the buffer stayed frozen for 40+ minutes; a hover past the frozen
buffer's last line errored with No line in <file> at position … while the
file on disk had more lines.

The headless -p client handles the same scenario correctly: same binary,
same plugin, same edit — it sends a rangeless full-text didChange with a
properly incremented version, then didSave, and answers are fresh.

There is also no recovery path: killing the server process leaves the client
with a zombie connection refusing every subsequent call (Cannot send request
… server is running
), and restartOnCrash did not respawn it. Only a new
session gets a fresh server.

Languages with order-sensitive compilation (F#) are hit hardest — a stale
buffer for one file also poisons answers about every file depending on it —
but the defect is language-independent and likely explains stale-diagnostics
reports in other languages (e.g. #64239 for TypeScript).

Distinct from #30622 (constant version in didChange): here no didChange is
sent at all in interactive mode — and at least fsautocomplete applies a
rangeless full-text didChange regardless of version, so the missing
notification, not the version, is what freezes it. Note #17063 claims -p
sends no lifecycle notifications; on 2.1.215 we measured the opposite (-p
sends didOpen/didChange/didSave correctly), so the asymmetry has flipped.

What Should Happen?

The interactive client should notify the server of writes the same way the
headless -p client already does: a didChange (full-text is fine) with an
incrementing version after each tool edit, so LSP answers describe the file
as it currently is.

Error Messages/Logs

Client→server traffic captured by a transparent logging wrapper registered as
the `.lsp.json` command (script in Steps to Reproduce). Noise trimmed.

Interactive session — query, Edit-tool change, query, out-of-band change (sed), query:

    req initialize
    ntf initialized
    ntf workspace/didChangeConfiguration
    ntf textDocument/didOpen              # Book.fs, full text
    req textDocument/documentSymbol       # answer correct
                                          # <- Edit tool rewrites Book.fs here
    req textDocument/documentSymbol       # NO didChange was sent; stale answer
                                          # <- sed rewrites Book.fs here
    req textDocument/documentSymbol       # still nothing; stale answer

Headless `-p` session — identical scenario, same machine, same plugin:

    req initialize
    ntf initialized
    ntf workspace/didChangeConfiguration
    ntf textDocument/didOpen              # Book.fs, full text
    req textDocument/documentSymbol       # answer correct
    ntf textDocument/didChange            # version=2, one rangeless full-text change
    ntf textDocument/didSave
    req textDocument/documentSymbol       # fresh answer
    req shutdown

Steps to Reproduce

  1. Save this pass-through logger as /abs/path/log_lsp.py (any LSP server,

any language):

``python
#!/usr/bin/env python3
# log_lsp.py SERVER_CMD... — pass-through that logs client->server methods
import json, os, subprocess, sys, threading
srv = subprocess.Popen(sys.argv[1:], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
threading.Thread(target=lambda: [sys.stdout.buffer.write(c) or sys.stdout.buffer.flush()
for c in iter(lambda: srv.stdout.read1(65536), b"")], daemon=True).start()
log = open(os.environ.get("LSP_LOG", "/tmp/lsp-wire.log"), "a")
stdin = sys.stdin.buffer
while True:
length, line = None, stdin.readline()
if not line: break
raw = line
while line.strip():
if line.lower().startswith(b"content-length:"): length = int(line.split(b":",1)[1])
line = stdin.readline(); raw += line
body = stdin.read(length); raw += body
try:
m = json.loads(body)
kind = "req" if "id" in m and "method" in m else "ntf" if "method" in m else "res"
v = m.get("params", {}).get("textDocument", {}).get("version")
log.write(f"{kind} {m.get('method')}" + (f" v={v}" if v else "") + "\n"); log.flush()
except Exception: pass
srv.stdin.write(raw); srv.stdin.flush()
``

  1. In a plugin's .lsp.json, wrap the registered server with it:

``json
{ "yourlang": { "command": "python3",
"args": ["/abs/path/log_lsp.py", "your-language-server"], ... } }
``

(Reproduced with fsautocomplete 0.83.0 registered for .fs.)

  1. Open an interactive session in a project of that language.
  2. Ask for any LSP query on a file (documentSymbol or hover), so the client

opens it.

  1. Let Claude edit that file with the Edit tool (e.g. rename a function).
  2. Ask the same LSP query again.
  3. Observe in /tmp/lsp-wire.log: no didChange after the edit, and the

second answer describes the pre-edit file.

  1. Repeat the same steps with claude -p: the log shows

ntf textDocument/didChange v=2 and the second answer is fresh.

Claude Model

Not sure / Multiple models

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

2.1.216 (Claude Code)

Platform

Anthropic API

Operating System

Ubuntu/Debian Linux

Terminal/Shell

WSL (Windows Subsystem for Linux)

Additional Information

Workaround we ship for F#: a disk-syncing stdio proxy between the client and
fsautocomplete that re-reads changed files before each request —
https://github.com/diegopego/claude-code-fsharp-lsp/blob/master/tools/fsac_sync_proxy.py
It fixes one language; every other .lsp.json-served language still has the
frozen-buffer behavior. Fixing the interactive client to send the same
didChange the -p client already sends would make it unnecessary.

View original on GitHub ↗