Skip to content

[Bug] First write after an LSP server cold-start returns no diagnostics (silent false clean) #48787

Description

@Bearmancer

The first write that touches a freshly started LSP server returns no <diagnostics> block at all ΓÇö even for a file that definitely has errors. The same file, or a sibling written next, then reports normally. This is a silent false-negative: the tool result reads as "clean".

Mechanism (source)

packages/opencode/src/lsp/client.ts:

const DIAGNOSTICS_FRESHNESS_TIMEOUT_MS = 3_000;
// ...
async diagnostics(filePath, signal) {
  const absPath = this.resolveWorkspacePath(filePath);
  await this.openFile(absPath);
  const deadlineAt = Date.now() + this.diagnosticsFreshnessTimeoutMs;
  for (;;) {
    const snapshot = this.documents.captureDiagnosticSnapshot(absPath);
    if (!snapshot) return this.freshnessTimeout(absPath);   // <- returns a transientError, caller renders nothing
    const push = this.documents.resolvePushDiagnostics(snapshot);
    if (push.status === "ready") return { items: [...push.diagnostics] };
    ...

and the server is spawned lazily per (root, serverID) in lsp.ts (schedule() → server.spawn() → LSPClient.create() → initialize handshake with INITIALIZE_TIMEOUT_MS).

On a cold server the document is opened while the server is still initializing, so no push diagnostics ever arrive and the pull request yields nothing within the freshness window → the client returns a transient/freshness result, the write tool result carries no diagnostics section, and the user sees an apparently clean file. shouldSeedDiagnosticsOnFirstPush (client.ts) only special-cases serverID === "typescript", so every other server is exposed.

Repro (bare dir, no repo markers, "lsp": true)

  1. Create an empty directory.
  2. In a single batch, write one broken file per extension: probe.py (def f(:), probe.cs (int y = ;), probe.sh (echo "unclosed), and warm files probe.js / probe.ts.
  3. Read the write results.

Expected: every broken file reports its error.
Actual: only the extensions whose server happened to be warm report. Verbatim from the first batch:

LSP errors detected in other files:
<diagnostics file="...\probe.ts"> ERROR [1:19] Expected an expression ... </diagnostics>
<diagnostics file="...\probe.js"> ERROR [1:11] Expected an expression ... </diagnostics>
→ nothing for probe.py, probe.cs, probe.sh, probe.json, probe.jsonc

Second batch, new file names (same directory, servers now warm):

<diagnostics file="...\probe2.py">
ERROR [1:6] "(" was not closed
ERROR [1:7] Expected parameter name
...
</diagnostics>
<diagnostics file="...\probe2.cs"> ERROR [1:37] Invalid expression term ';' </diagnostics>

And a third observable: a file written earlier (__lsp_t.json) resurfaced later only as an "other files" entry ΓÇö i.e. the diagnostics existed, they just arrived after the tool result was rendered.

Counter-scenarios tried

  • Second write to the same server (warm) ΓåÆ diagnostics appear ΓçÆ not a parser problem.
  • Fresh file name in the same directory (no re-open of the same path) ΓåÆ appears once warm ΓçÆ not a path/caching artifact.
  • Different extension, identical bytes (.json vs .jsonc) ΓåÆ .json fires on a warm server, .jsonc never ΓçÆ confirms cold-start is orthogonal to the LANGUAGE_EXTENSIONS issue.
  • Independent tool path (plugin lsp_diagnostics) on the same cold file ΓåÆ returns the error immediately ΓçÆ the server can answer; only the write-time path misses it.

Expected vs actual

Expected: if diagnostics cannot be obtained within the freshness window, the tool result says so (or retries/reports freshness_timeout ΓÇö the error kind already exists in LspDiagnosticsDetails).
Actual: the write result is indistinguishable from a clean file.

Suggested fix

Surface the existing transient result on the write path (e.g. append a note like no diagnostics received within 3000 ms (server initializing)), or wait for the server's initialized notification before the first diagnostics request, or extend the per-server first-request window.


Environment (software)

item value
OS Microsoft Windows 10 Pro — 10.0.19045 (Build 19045), 64-bit
Shell PowerShell 7.6.6 (+ Windows Terminal)
opencode 1.18.30
plugins oh-my-openagent 4.19.4 · @cortexkit/opencode-antigravity-auth 2.2.1
runtimes Node v24.19.0 · npm 11.17.0 · Bun 1.4.2
LSP binaries biome 2.5.13 · basedpyright 1.40.1 · roslyn-language-server 5.12.0-1.26426.8 · bash-language-server 5.6.0 · shellcheck 0.11.0 · vscode-langservers-extracted 4.10.0 · PowerShellEditorServices 4.7.0 · PSScriptAnalyzer 1.25.0
logs ~/.local/share/opencode/log/opencode.log · ~/.omo/agent/OmO-debug.log · ~/.omo/agent/logs/config-reload.log

OS facts collected with:

Get-CimInstance Win32_OperatingSystem | Select-Object Caption,Version,BuildNumber,OSArchitecture

No hardware, hostname, username, or credentials included.

Activity

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions