Skip to content

fix(lsp): stop respawn storm on failed server start + support TypeScript 7 native server - #1134

Open
cast-vytautas wants to merge 4 commits into
getkimchi:masterfrom
cast-vytautas:fix/lsp-failed-server-respawn
Open

fix(lsp): stop respawn storm on failed server start + support TypeScript 7 native server#1134
cast-vytautas wants to merge 4 commits into
getkimchi:masterfrom
cast-vytautas:fix/lsp-failed-server-respawn

Conversation

@cast-vytautas

@cast-vytautas cast-vytautas commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Linked issue

Closes #1133

What does this PR do?

Part 1 — quiet, session-scoped failure handling

When a language server fails to start (e.g. typescript-language-server with no resolvable tsserver.js — fresh worktrees without node_modules, or projects on TypeScript 7 which ships none), the session previously re-spawned the doomed server on every file operation and dumped Bun's bundled-source code frame into the TUI each time.

  • Respawn suppressionsrc/extensions/lsp.ts remembers failed server+root pairs for the session (failedClients); tool_result and the eager session_start register failures and skip re-spawn. Reset on session_start/session_shutdown, so a new session retries (the failure is often fixed between sessions, e.g. by running the package installer). Failures are recorded with a phase (start vs mid-session sync) so the status bar reports them accurately.
  • Clean error surface — one line (LSP file sync failed: <message>) instead of logging the Error object, whose Bun rendering produced the code-frame dump.
  • Status visibility — the status bar shows LSP: <server> failed to start, kept even when other servers sync fine.
  • Tool guard — all five lsp_* tools go through a startClient helper that fails fast with an actionable message for known-failed servers instead of re-spawning.

Reproduced with a fixture matrix (main repo vs worktree × with/without resolvable typescript, real typescript-language-server 6.0.0 + TS 5.9.3 / 7.0.2): red exactly in the no-resolvable-tsserver variants with the same error and stack frame as production, green elsewhere.

Part 2 — TypeScript 7 native server support

TS7 workspaces turned out to be exactly the environments that fail most often, and no harness-side fallback can fix them the old way: typescript@7 ships no lib/tsserver.js — its language service is a plain LSP server inside the native binary (tsc --lsp --stdio), so typescript-language-server can never start there. This branch now activates the native server directly:

  • Detection (src/extensions/lsp/servers.ts) — resolveTsNativeServerPath recognizes the native package signature (bin/tsc present, lib/tsserver.js absent). The workspace's own typescript package decides the flavor (a classic TS≤6 package wins even if the worktree's main repo is native); worktrees without their own install fall back to the main repo's bin. In a TS7 workspace the native server is preferred over an installed typescript-language-server, and it works with no global install at all.
  • Startup (client.ts) — skipProjectLoadWait skips the projectLoaded wait for servers that never emit $/progress startup cycles (otherwise a fixed 15-second stall on every spawn).
  • Diagnostics (client.ts, lsp.ts) — the native server has no publishDiagnostics push, so pull-model servers use the LSP 3.17 textDocument/diagnostic request in the tool_result sync path and in lsp_diagnostics. Push flow unchanged for classic servers.

Verified live against the real TS7 server in an actual TS7 workspace: initialize, didOpen, hover, definition, documentSymbol, and pull diagnostics all work; push diagnostics confirmed absent. Previously these sessions emitted the code-frame dump storm from Part 1; now they get working LSP, and if a server still can't start, the quiet one-line failure plus status entry applies.

Checklist

  • I have read CONTRIBUTING.md and agree to the CLA
  • This PR links to an open issue above
  • Tests pass locally (pnpm run test) — 140 LSP tests green including regression tests for the respawn/failure paths (Part 1) and the native-server matrix: signature resolution, flavor precedence, worktree fallback, detection preference order, missing-candidate exclusion, and pull-vs-push sync/tool paths (Part 2). Full suite green except pre-existing environment-dependent failures in pi-package-lookup (leaks local ~/.config/kimchi/harness/npm packages) and web-fetch integration (network) — both fail identically without this branch
  • Lint passes (pnpm run check)
  • Documentation updated if behavior changed — no docs describe LSP behavior; behavior notes are in the issue and this body

@cast-vytautas cast-vytautas added the bug Something isn't working label Sep 3, 2026
@readme-ai-writer

readme-ai-writer Bot commented Sep 3, 2026

Copy link
Copy Markdown

Documentation Changes Added

Page Section Action Summary
coding-lspGuides📝 UpdatedDocument the status bar failure state and server start failure behavior added by the respawn storm fix.

🔗 View all changes in ReadMe


Actions

  • Merge documentation branch with PR merge
  • Delete documentation branch with PR close

If neither actions are selected, on PR close/merge the docs branch in ReadMe will remain open.

@kimchi-review

kimchi-review Bot commented Sep 3, 2026

Copy link
Copy Markdown

Kimchi Code Review

Property Value
Commit efed856
Author @cast-vytautas
Files changed 3
Review status Completed
Comments 2 (2 warning)
Duration 193s

Summary

📊 Review Score: 83/100 (overall code quality — 0 lowest, 100 highest)
⏱️ Estimated effort to review: 2/5 (1 = trivial, 5 = very complex)

🧪 Tests: yes — A focused regression test file src/extensions/lsp/lsp-file-sync.test.ts was added. It verifies that a server that fails to start is logged once as a single-line message, is not respawned on later file operations, is retried after a new session_start, and that LSP tools surface an actionable session-scoped error. The new getRegisteredTool mock helper is also tested indirectly.

📝 Found 2 issue(s). See inline comments for details.

What to expect

Kimchi will analyze the changes in this pull request and post:

  • A summary of the overall changes
  • Inline comments on specific lines with findings categorized by issue type

The review typically completes within a few minutes. This comment will be updated once the review is ready.

Interact with Kimchi
  • @getkimchi review — re-trigger a full review on the latest commit
  • @getkimchi summary — regenerate the PR summary
  • @getkimchi ignore — skip this PR (no review will be posted)
  • Reply to any inline comment to ask follow-up questions or request clarification
Configuration

Reviews are configured by your organization admin.
Review instructions, excluded directories, and severity thresholds can be adjusted per repository in the Kimchi dashboard.


Powered by Kimchi — AI-powered code review by CAST AI

@kimchi-review kimchi-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📊 Review Score: 83/100 (overall code quality — 0 lowest, 100 highest)
⏱️ Estimated effort to review: 2/5 (1 = trivial, 5 = very complex)

🧪 Tests: yes — A focused regression test file src/extensions/lsp/lsp-file-sync.test.ts was added. It verifies that a server that fails to start is logged once as a single-line message, is not respawned on later file operations, is retried after a new session_start, and that LSP tools surface an actionable session-scoped error. The new getRegisteredTool mock helper is also tested indirectly.

📝 Found 2 issue(s). See inline comments for details.

Comment thread src/extensions/lsp.ts Outdated
// same way, and re-spawning on each file op re-prints the same error each
// time. Reset on session_start — the failure is often fixed between
// sessions (e.g. by running the package installer), so a new session
// retries.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️🔀 Concurrency

failedClients is a module-level let that is reassigned in session_start and session_end, but the eager getOrCreateClient(...).catch((err) => noteClientFailure(...)) callbacks and noteClientFailure itself close over the variable binding, not the Set value. If a stale async rejection from a previous session fires after a new session has reset failedClients, the old failure is recorded in the new session's set and can incorrectly suppress that server for the rest of the new session.

💡 Suggestion: Capture the active failure set at session creation time and have noteClientFailure operate on that stable reference. For example, assign const sessionFailedClients = failedClients inside session_start and pass sessionFailedClients to the helpers, or keep the set in a session-scoped object instead of a top-level let.

Comment thread src/extensions/lsp.ts

/** Like getOrCreateClient, but skips server+root pairs that already failed
* this session instead of re-spawning a doomed server. Throws a
* human-readable error so tool handlers surface something actionable. */

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️🔧 Maintainability

startClient records a failure in failedClients but does not call noteClientFailure, so when an LSP tool is the first code path to trigger a start failure the status bar is not updated and no one-line console message is emitted. This makes the tool path inconsistent with the file-sync and eager-start paths and leaves the user without the status feedback the comment promises.

💡 Suggestion: Make startClient reuse noteClientFailure by passing the UI context into it, e.g. change the catch block to noteClientFailure(server, root, err, ui) instead of manually calling failedClients.add(clientKey(server, root)).

cast-vytautas and others added 2 commits September 4, 2026 07:40
When an LSP server binary is on PATH but the session cwd has no resolvable TypeScript installation — fresh worktrees without node_modules, or projects on TypeScript 7 which ships no tsserver.js — typescript-language-server rejects initialize and exits. Each file operation re-spawned the server, and logging the rejected Error object dumped Bun's bundled-source code frame into the TUI every time.

- Cache failed server+root pairs per session and skip re-spawn; reset on session start/shutdown so a new session retries.
- Log one line (LSP file sync failed: <message>) instead of the Error object.
- Surface the failure in the status bar: LSP: <server> failed to start.
- Route the five lsp_* tools through a startClient guard that errors fast with an actionable message for known-failed servers.

Adds regression tests for the respawn storm, eager-start failure, session-restart retry, and the tool-side guard.

Closes getkimchi#1133

Co-Authored-By: Kimchi <noreply@kimchi.dev>
- Unify failure recording: startClient routes through noteClientFailure
  so tool-path failures get the status bar update and one-line log.
- Track failure phase (start vs mid-session sync) so a crashed server
  reports "failed" instead of the misleading "failed to start".
- Keep failure indicators when a healthy server syncs afterwards —
  mixed-repo status no longer erases a failed server.
- Phase-aware tool guard message; tests use mock getHandler generics.

Co-Authored-By: Kimchi <noreply@kimchi.dev>
@cast-vytautas
cast-vytautas force-pushed the fix/lsp-failed-server-respawn branch from 19df98d to 3022a56 Compare September 4, 2026 04:40
TypeScript 7 ships no tsserver.js — its language service is a plain LSP server inside the native binary (tsc --lsp --stdio), so typescript-language-server can never serve a TS7 workspace; it failed initialize with the classic Could not find a valid TypeScript installation error in every session.

- servers.ts: recognize the native package signature (bin/tsc present, lib/tsserver.js absent) via resolveTsNativeServerPath, with workspace-flavor precedence — the workspace's own typescript package decides (classic wins even if the main repo is native), worktrees fall back to the main repo's bin. In a TS7 workspace the native server is preferred over an installed typescript-language-server and also works with no global install.
- client.ts: skip the projectLoaded wait for servers that never emit $/progress startup cycles (previously a fixed 15s stall on every spawn), and add pullDiagnostics for the LSP 3.17 textDocument/diagnostic pull model since the native server has no publishDiagnostics push.
- lsp.ts: tool_result sync and lsp_diagnostics pull diagnostics for pull-model servers; push flow unchanged for classic servers.

Verified live against the real TS7 server: initialize, didOpen, hover, definition, documentSymbol, and pull diagnostics all work; push diagnostics confirmed absent.

Co-Authored-By: Kimchi <noreply@kimchi.dev>
@cast-vytautas cast-vytautas added the new feature Introduces a new feature label Sep 4, 2026
@cast-vytautas cast-vytautas changed the title fix(lsp): stop respawn storm and code-frame dump on failed server start fix(lsp): stop respawn storm on failed server start + support TypeScript 7 native server Sep 4, 2026
- tool_result sync resolves the project root via findRoot() like the lsp_* tools do, so the failure cache and the spawned client are keyed by the same root whichever path sees the file first. Spawn suppression stays per server+root (distinct monorepo roots are distinct server processes), but the console line is now reported once per server per session (reportedFailures) — the issue's "one human-readable line per session" even in monorepos.
- The failure log label reflects the phase: "LSP: <server> failed to start:" for spawn/initialize failures (including tool-initiated starts, previously mislabeled as file sync failures) and the original "LSP file sync failed:" line for mid-session sync breaks.
- servers.ts: hoist the typescript-language-server entry to a named constant referenced by identity — removes the non-null `as` cast on SERVERS.find() and the repeated name string across detection branches.

Co-Authored-By: Kimchi <noreply@kimchi.dev>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working new feature Introduces a new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LSP: failed server start dumps Bun code frame and re-spawns on every file op (worktrees without node_modules / TypeScript 7)

1 participant