Skip to content

Plan: scope the LSP workspace singleton per client so instances coexist - #714

Merged
jeduden merged 2 commits into
mainfrom
claude/mdsmith-lsp-multi-instance-2wwlo0
Jul 1, 2026
Merged

Plan: scope the LSP workspace singleton per client so instances coexist#714
jeduden merged 2 commits into
mainfrom
claude/mdsmith-lsp-multi-instance-2wwlo0

Conversation

@jeduden

@jeduden jeduden commented Jun 29, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a plan to let several mdsmith lsp processes serve one workspace at the same time — the VS Code extension's server, a Claude Code plugin server running in the same VS Code, and a second Claude in another terminal — while keeping the VS Code extension-upgrade hand-off (old server stops, new starts) intact.

This PR is plan only. No engine, server, or extension code changes yet.

Problem

The newest-wins workspace singleton (internal/lsp/singleton.go), enabled in production at cmd/mdsmith/lsp.go, keys its owner record on the workspace root alone:

func workspaceKey(root string) string {
    sum := sha256.Sum256([]byte(filepath.Clean(root)))
    return hex.EncodeToString(sum[:])
}

So every mdsmith lsp on a workspace contends for one slot, the newest wins, and the older ones send mdsmith/superseded and exit. The VS Code client suppresses restart on that signal (decideClose), so the VS Code server is torn down — and does not come back — the moment a Claude plugin server (or a second Claude terminal) initializes on the same repo.

The singleton must be preserved for the one case it was built for: a VS Code extension update/reload leaves a leaked extension host that holds the old server's stdin open (no EOF) and stays alive by PID (so the processId watchdog can't reap it). Newest-wins is what stops that orphan racing the freshly spawned server.

Proposed design

  • Make the singleton claim opt-in via a client-supplied initializationOptions.singletonScope token; key the owner record on (root, scope). No token → no claim, no supersede.
  • The VS Code extension sends vscode.env.sessionId — stable across an extension-host reload, unique per app session — so the orphan and the respawn share one slot and the upgrade hand-off still works.
  • Every other client (Claude Code plugin, Neovim, Helix, JetBrains) sends no token and coexists freely.

processId can't be used (orphan and respawn differ) and clientInfo.name can't either (two Claude terminals share claude-code). A client-supplied, reload-stable token is the only identity that both reaps the orphan and lets independent clients coexist.

Scope / non-goals

  • No cross-process cache sharing — each mdsmith lsp keeps its own in-process Session caches.
  • No coordination of fix-on-save writes across processes.
  • Singleton and processId watchdog both stay.

Plan

plan/2606292015_lsp-multi-instance-coexistence.md — status 🔳, with TDD tasks and acceptance criteria covering the Go server, the VS Code extension, and the docs (docs/reference/cli/lsp.md gains a multi-instance section; the VS Code guide's "Two mdsmith servers" note is updated).

Checks

  • mdsmith check . passes (520 files, 0 failures).

🤖 Generated with Claude Code


Generated by Claude Code

The newest-wins workspace singleton keys its owner record on the
workspace root alone, so every mdsmith lsp on a workspace contends
for one slot and the newest supersedes the rest. That tears down
the VS Code server the moment a Claude Code plugin server (or a
second Claude terminal) initializes on the same repo.

Plan: make the singleton claim opt-in via a client-supplied
`singletonScope` token and key the owner record on (root, scope).
VS Code sends `vscode.env.sessionId` (stable across an
extension-host reload), so the upgrade hand-off still reaps the
orphan; other clients send no token and coexist freely.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mb6qCg7oxErCU2p5JFH3eP
@codecov

codecov Bot commented Jun 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.63%. Comparing base (0314c10) to head (a68ec24).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
Components Coverage Δ
Go 98.62% <ø> (ø)
TypeScript 99.54% <ø> (ø)

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Two independent xhigh design-review passes converged on a
load-bearing flaw: vscode.env.sessionId is documented as changing
"each time the editor is started" and is injected per
extension-host process, so a leaked orphan host and its respawn
may hold different ids — which would make (root, sessionId)
keying never reap the orphan, strictly worse than today's
root-only key for the one case the singleton exists for.

Revisions:
- VS Code now sends a per-workspace UUID persisted in
  context.workspaceState (disk-backed, provably stable across a
  reload/update) instead of env.sessionId.
- Fold scope into the single workspaceKey seam (empty scope =>
  legacy root-only key) instead of a parallel singletonKey.
- Namespace the wire field as initializationOptions.mdsmith.singletonScope.
- One gate: EnableWorkspaceSingleton stays the process
  capability; scope is only the key input and claim gate.
- Add Backward compatibility (empty scope reproduces the legacy
  key; stale owner records are harmless), sharpen Rollout skew,
  and note multi-root re-keying is out of scope (unchanged today).
- Strengthen TDD tasks/criteria: key-derivation unit test,
  absent/null initializationOptions decode, empty-scope no-op
  driven red/green, no-token-never-claims, bun:test for the token.
- Two-window-same-folder limitation documented (not a regression).
- Fix status to not-started (the implementation has not begun).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mb6qCg7oxErCU2p5JFH3eP
@jeduden
jeduden marked this pull request as ready for review June 29, 2026 21:30
@jeduden jeduden added queue Add to a PR to enqueue it queue:active Applied automatically when a PR is in an active batch and removed queue Add to a PR to enqueue it labels Jul 1, 2026
@jeduden

jeduden commented Jul 1, 2026

Copy link
Copy Markdown
Owner Author

🟢 Merge Queue — picked up

This PR is in the queue and will be batched with other queue-labelled PRs.

Next: No action needed — you'll get another comment when CI starts on the batch. View merge queue run.

@jeduden

jeduden commented Jul 1, 2026

Copy link
Copy Markdown
Owner Author

🔵 Merge Queue — CI running

Merged into batch branch merge-queue/batch-714-1782925993. View CI run.

Next: No action needed — you'll be notified when CI completes.

@jeduden jeduden removed the queue:active Applied automatically when a PR is in an active batch label Jul 1, 2026
@jeduden
jeduden merged commit f012907 into main Jul 1, 2026
31 of 32 checks passed
@jeduden

jeduden commented Jul 1, 2026

Copy link
Copy Markdown
Owner Author

Merge Queue — merged

This PR landed on main via commit f012907. CI run that validated the merge.

Next: Done — nothing more to do here.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants