Skip to content

feat(lsp): Phase 1 — bounded ingress, cancellation ownership, client gating#3976

Open
rossirpaulo wants to merge 2 commits into
paulo/lsp-latency-mutex-fixfrom
paulo/lsp-ingress-phase1
Open

feat(lsp): Phase 1 — bounded ingress, cancellation ownership, client gating#3976
rossirpaulo wants to merge 2 commits into
paulo/lsp-latency-mutex-fixfrom
paulo/lsp-ingress-phase1

Conversation

@rossirpaulo

Copy link
Copy Markdown
Contributor

Stacked on #3969 (Phase 0). Do not merge before it; GitHub will retarget this to canary when #3969 lands. Together the two PRs deliver the LSP latency/mutex design (docs/design/lsp-latency-and-mutex-fix.md) through Phase 1; Phase 2 (salsa snapshot reads) remains behind the design doc's spike gate.

Summary

Rust — bounded ingress and cancellation (Track B2/D2/D3)

  • IngressScheduler (from the alternate implementation in Fix LSP latency, mutex contention, and session correctness #3961, relocated into baml_lsp_server where transport scheduling belongs): transport-neutral admission classes, lifecycle barriers, per-class byte/item budgets, response-ownership tokens, didChange tail-coalescing, browser takeover. Ported with fixes for every defect verified in review:
    • forced NonCancellableWhenRunning on initialize/shutdown/executeCommand can no longer be downgraded by a caller policy (stronger_cancellation);
    • a permanently oversized message is contained per-message — requests answer -32803, notifications drop with a warn; the session and process stay alive;
    • an oversized $/cancelRequest no longer livelocks the control lane;
    • pipelined initialized during InitializeResponding is admitted FIFO and accepted at dispatch (found via the latency harness; the reference dropped it, breaking pipelining clients).
  • LspRuntime: stdio and browser /api/lsp sessions dispatch through one scheduler behind a process-owned mutex; $/cancelRequest is drained on the transport thread so it can claim a response while a handler runs — queued requests answer -32800 immediately, running cancellable requests observe a dispatch-scoped token (checked at handler entry and immediately after the source guard; never connected to salsa unwinding). Browser takeover atomically revokes the old session's sink (tombstone), purges its pending responses, and replays document overlays.
  • Bounded output: serialize-once Arc<[u8]> frames with a shared budget charge (accounting equals actual memory); per-session outbound budgets; Lagged/saturation close only the affected session; responses exceeding the reservation are replaced with a per-request -32803, never a session kill.
  • Bounded stdio framing: 16 KiB header cap; >8 MiB bodies discarded in bounded chunks with a recoverable null-id error; abnormal exit paths return nonzero.
  • bex_project kept minimal: LspError::RequestCanceled-32800 at the single serialization boundary (I7); connection-scoped LSP sessions (fresh position-encoding negotiation and workspace roots per connection, shared project registry) — fixes the global once-only encoding negotiation flagged in review.

TypeScript — client gating (D6, D2 subset, Track A)

  • Playground run-gating: when is_bex_current is false (or a run/preview was refused with projectNotReady), Run/test/preview actions disable with a "Preparing current build…" status and re-enable on the next current ProjectUpdate. No protocol version bump; closes the fail-closed D7 UX window refactor(lsp): Phase 0 latency and mutex containment #3969 introduces. Stale "using last successful build" copy removed.
  • WebSocketRuntimePort reconnect correctness: fail-closed hello; bounded first-connect queue (64, drop-oldest); session-scoped commands issued while disconnected are dropped with synthesized rejections instead of replaying into a new session; a desired-runtime lease survives reconnects and is re-sent after every compatible hello (the reference's lease-loss bug is inverted and pinned by test).
  • Track A multi-root ownership: projectRoots.ts (canonical/symlink-aware root resolution, nested projects, multi-workspace) + routing in the extension — excluding two review-verified reference bugs: closing the last .baml document no longer stops the owner server (kept the playground webview alive), and aggregate status preserves error when every client start fails.

Known limits (deliberate)

  • ensureProjectRuntime/releaseProjectRuntime client plumbing is dormant: typed, unit-tested, nothing sends them yet — the seam for the demand-gating server work (D5).
  • A client that pipelines didOpen in the same instant as initialized (no ordering gap) can still have that notification dropped at admission — contract-conformant per the design doc; VS Code never does this.
  • Oversized (>8 MiB) stdio requests answer with a null-id error (the id is unknowable without buffering the oversized body).
  • The pendingTestTarget deep-link auto-run is converted to the preparing state during a rebuild window but not auto-retried.

Validation

  • cargo test --lib -p baml_lsp_server 67/67 (22 scheduler, 9 runtime, 7 framing/budget, plus existing) · -p bex_project 45/45 (+1 pre-existing ignored)
  • cargo clippy -p baml_lsp_server -p bex_project --all-targets -- -D warnings clean · cargo check --target wasm32-unknown-unknown -p bridge_wasm clean · cargo fmt --check clean
  • TS: playground 125/125, extension 13/13 (8 projectRoots), webview 19/19 (3 new gating), promptfiddle + all typechecks green
  • Harness (/tmp/baml-inlay-repro, 400 fns, release build of this branch): $/cancelRequest now answers -32800 (Phase 0: completed normally); typing inlay p50 2.0 ms / p95 51.7 ms; zero request errors; zero unanswered; 10-edit burst → 0 refresh storms.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VEMxti5WQ8bCgJUuX9AQvo

…gating

Rust (stacked on the Phase 0 core):
- IngressScheduler (transport-neutral admission/lifecycle/cancellation/
  budget state machine) relocated into baml_lsp_server, with fixes: forced
  cancellation of protected methods can no longer be downgraded; permanent
  oversize is contained per-message (-32803) instead of closing the
  session; oversized $/cancelRequest no longer livelocks the control lane;
  pipelined `initialized` is admitted FIFO and accepted at dispatch.
- New LspRuntime: stdio and browser LSP sessions dispatch through one
  scheduler; cancellation drained on the transport thread claims responses
  while handlers run ($/cancelRequest -> -32800); browser takeover revokes
  the old session's sink and replays document overlays; bounded outbound
  budgets with serialize-once shared frames.
- Bounded stdio framing: 16 KiB header cap; oversized bodies discarded in
  chunks with a recoverable typed error; abnormal exit is nonzero.
- bex_project kept minimal: LspError::RequestCanceled (-32800) at the one
  serialization boundary (I7); dispatch-scoped cancellation token checked
  at handler entry and after the source guard (never Salsa unwinding);
  connection-scoped LSP sessions (fresh encoding negotiation + workspace
  roots per connection, shared project registry).

TypeScript:
- D6 playground run-gating on is_bex_current: Run/test/preview disabled
  with a "Preparing current build..." state during rebuild windows;
  projectNotReady rejections render as that transient state and clear on
  the next current ProjectUpdate (no protocol version bump).
- WebSocketRuntimePort reconnect correctness: fail-closed hello, bounded
  first-connect queue, session-scoped commands dropped on disconnect with
  synthesized rejections, desired-runtime lease resent after every hello.
- Track A: projectRoots.ts canonical multi-root ownership + routing in the
  VS Code extension; closing the last document no longer stops the owner
  server; aggregate status preserves startup failures.

Verification: baml_lsp_server 67/67, bex_project 45/45 (+1 pre-existing
ignored), clippy -D warnings clean, wasm check clean; playground 125/125,
extension 13/13, webview 19/19, all typechecks green. Harness (400 fns):
$/cancelRequest now answers -32800; typing inlay p50 2.0 ms, p95 51.7 ms,
zero errors, zero unanswered requests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VEMxti5WQ8bCgJUuX9AQvo
@vercel

vercel Bot commented Jul 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
beps Ready Ready Preview, Comment Jul 11, 2026 12:57am
promptfiddle Ready Ready Preview, Comment Jul 11, 2026 12:57am
promptfiddle2 Ready Ready Preview, Comment Jul 11, 2026 12:57am

Request Review

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: de5fb057-364e-4cbd-89cc-d94e02fb6534

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch paulo/lsp-ingress-phase1

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

⏭️ Performance benchmarks were skipped

Perf benchmarks (CodSpeed) are opt-in on pull requests — they no longer run on every push. They always run automatically after merge to canary/main.

To run them on this PR, do any of the following, then push a commit (or re-run CI):

  • Add RUN_CODSPEED=1 to the PR description, or
  • Include run-perf or /perf in the PR title or any commit message.

@github-actions

github-actions Bot commented Jul 11, 2026

Copy link
Copy Markdown

Binary size checks failed

1 violations · ✅ 6 passed

⚠️ Please fix the size gate issues or acknowledge them by updating baselines.

Artifact Platform File Gzip Gated on Baseline Delta Status
baml-cli Linux 🔒 22.1 MB 9.5 MB file 21.5 MB +611.9 KB (+2.8%) OK
packed-program Linux 🔒 16.0 MB 6.7 MB file 15.6 MB +361.9 KB (+2.3%) OK
baml-cli macOS 🔒 17.0 MB 8.2 MB file 16.6 MB +481.3 KB (+2.9%) OK
packed-program macOS 🔒 12.4 MB 5.9 MB file 12.1 MB +281.9 KB (+2.3%) OK
baml-cli Windows 🔒 18.6 MB 8.4 MB file 18.1 MB +510.0 KB (+2.8%) OK
packed-program Windows 🔒 13.3 MB 6.0 MB file 13.0 MB +294.8 KB (+2.3%) OK
bridge_wasm WASM 14.8 MB 🔒 4.2 MB gzip 4.1 MB +125.1 KB (+3.1%) FAIL

🔒 = the size this artifact is GATED on (ceiling + delta). Binaries gate on file size (installed binary); WASM gates on gzip (download size). The other size is shown for information only.

Details & how to fix

Violations:

  • bridge_wasm (WASM) gzip_bytes: 4.2 MB exceeds limit of 4.2 MB (exceeded by +3.3 KB, policy: max_gzip_bytes)
  • bridge_wasm (WASM) gzip_delta_pct: +3.1% exceeds limit of 3.0% (exceeded by +0.1pp, policy: max_delta_pct)

Add/update baselines:

.ci/size-gate/wasm32-unknown-unknown.toml:

[artifacts.bridge_wasm]
file_bytes = 14760377
gzip_bytes = 4184019

Generated by cargo size-gate · workflow run

Drive-less file:///tmp URIs fail Url::to_file_path on Windows, so
canonical_document_identity_str returned None and the test panicked
on the Windows CI runner only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VEMxti5WQ8bCgJUuX9AQvo
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.

1 participant