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)
- Create an empty directory.
- 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.
- 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.
The first
writethat 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:and the server is spawned lazily per (root, serverID) in
lsp.ts(schedule()→server.spawn()→LSPClient.create()→initializehandshake withINITIALIZE_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-casesserverID === "typescript", so every other server is exposed.Repro (bare dir, no repo markers,
"lsp": true)probe.py(def f(:),probe.cs(int y = ;),probe.sh(echo "unclosed), and warm filesprobe.js/probe.ts.Expected: every broken file reports its error.
Actual: only the extensions whose server happened to be warm report. Verbatim from the first batch:
Second batch, new file names (same directory, servers now warm):
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
.jsonvs.jsonc) →.jsonfires on a warm server,.jsoncnever ⇒ confirms cold-start is orthogonal to theLANGUAGE_EXTENSIONSissue.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 inLspDiagnosticsDetails).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'sinitializednotification before the first diagnostics request, or extend the per-server first-request window.Environment (software)
~/.local/share/opencode/log/opencode.log·~/.omo/agent/OmO-debug.log·~/.omo/agent/logs/config-reload.logOS facts collected with:
No hardware, hostname, username, or credentials included.