Design Philosophy: “A Silently Lying LSP Is the Worst”
Running with the wrong.venv is worse than disabling LSP functionality and returning errors.
Why This Matters
- Offering completions with wrong dependencies generates code that imports non-existent modules
- Type checking appears to pass, but fails at runtime
- Developers continue coding based on false information, believing “LSP is working”
typemux-cc’s Choice
- Explicitly return errors when
.venvis not found - Explain the situation in error messages so users can take action
- “Getting an error” is healthier than “nothing happening”
System Architecture
Overall Structure
The proxy sits between Claude Code and multiple LSP backends (pyright, ty, or pyrefly), performing:- Message routing: Route JSON-RPC messages to the correct backend based on document venv
- venv detection: Search for
.venvon:textDocument/didOpen(always)- Any URI-bearing LSP request on cache miss (fallback to full venv resolution)
- NOTE: Cached documents reuse the last known venv and are not re-searched
- Multi-backend pool: Manage concurrent backend processes (one per venv, up to
max_backends) - State restoration: Resend open documents when spawning a new backend
- Diagnostics cleanup: Clear diagnostics for documents outside the target venv
- Warmup readiness: Queue index-dependent requests until backends finish building their cross-file index
Message Routing Flow
Request Routing Decision Tree
Startup Sequence with Fallback Venv
Multi-Venv Routing
Design Principles
1. Explicit Errors Over Silent Failures
When.venv is not found, typemux-cc returns an explicit error message rather than:
- Using a global Python installation
- Using an incorrect virtual environment
- Silently falling back to limited functionality
2. Session-Based Stale Message Detection
Each backend gets a unique session ID (monotonically increasing counter). When a backend crashes or is evicted:- All pending requests for that session are cancelled
- Responses from old sessions are discarded
- New backend gets a new session ID
src/backend_pool.rs:44-55):
3. Proxy ID Rewriting for Backend Requests
LSP backends can send requests to the client (e.g.,window/workDoneProgress/create). With multiple backends, their request IDs can collide.
Solution: Rewrite IDs with negative numbers (src/state.rs:80-91):
- Backend sends request with its own ID (e.g.,
id: 1) - Proxy allocates a unique negative proxy ID (e.g.,
id: -1) to avoid collision with client IDs (positive) - Original ID is stored in
pending_backend_requests - Message forwarded to client with rewritten ID
- Client response routed back, ID restored to original
4. Document State Caching for Fast Revival
The proxy maintains an in-memory cache of all open documents (src/state.rs:30-37):
- When spawning a backend for a new venv, all relevant documents must be restored
- Client (Claude Code) keeps files open, so backend state must match
- Even without an active backend,
didChangecontents continue to be recorded
project-b/.venv, only documents under project-b/ are restored. Documents under project-a/ are skipped, and empty diagnostics are sent to clear stale errors.
Non-Goals
No Multi-Client Support
typemux-cc is designed exclusively for Claude Code. No support for other LSP clients.
No Poetry/Conda/Pipenv
Only
.venv (virtualenv/venv) is supported. No environment resolution for other tools.No Mixed Backend Types
All backends in the pool must be the same type (pyright, ty, or pyrefly). No simultaneous parallel operation of multiple backend types.
Event Loop Architecture
The main event loop insrc/proxy/mod.rs uses tokio::select! with 4 arms:
- Arm 1: Reads LSP messages from Claude Code via stdin
- Arm 2: Receives messages from all backend reader tasks via mpsc channel
- Arm 3: Periodic 60s sweep to evict idle backends (TTL-based eviction)
- Arm 4: Dynamic deadline for transitioning warming backends to ready (fail-open)
Source Code Structure
| File | Responsibility |
|---|---|
main.rs | Entry point, CLI argument parsing, logging setup |
backend.rs | LSP backend process management (pyright, ty, pyrefly) |
backend_pool.rs | Multi-backend pool, LRU/TTL management, warmup state |
state.rs | Proxy state: pool, documents, pending requests |
message.rs | JSON-RPC message type definitions (RpcMessage, RpcId, RpcError) |
framing.rs | JSON-RPC framing (Content-Length header processing) |
text_edit.rs | Incremental text edit application for didChange |
venv.rs | .venv search logic (parent traversal, git toplevel boundary) |
error.rs | Error type definitions (ProxyError, BackendError, etc.) |
proxy/mod.rs | Main event loop (tokio::select! with 4 arms) |
proxy/client_dispatch.rs | Client message routing, warmup queueing, cancel handling |
proxy/backend_dispatch.rs | Backend message routing, proxy ID rewriting, progress detection |
proxy/pool_management.rs | LRU/TTL eviction, crash recovery, warmup expiry |
proxy/initialization.rs | Backend initialization handshake, document restoration |
proxy/document.rs | Document tracking (didOpen, didChange, didClose) |
proxy/diagnostics.rs | Diagnostic message handling, stale diagnostics cleanup |