Skip to main content

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 .venv is not found
  • Explain the situation in error messages so users can take action
  • “Getting an error” is healthier than “nothing happening”
This philosophy permeates the entire architecture, from strict venv mode to warmup readiness behavior.

System Architecture

Overall Structure

The proxy sits between Claude Code and multiple LSP backends (pyright, ty, or pyrefly), performing:
  1. Message routing: Route JSON-RPC messages to the correct backend based on document venv
  2. venv detection: Search for .venv on:
    • 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
  3. Multi-backend pool: Manage concurrent backend processes (one per venv, up to max_backends)
  4. State restoration: Resend open documents when spawning a new backend
  5. Diagnostics cleanup: Clear diagnostics for documents outside the target venv
  6. 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
Backend Instance Structure (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):
  1. Backend sends request with its own ID (e.g., id: 1)
  2. Proxy allocates a unique negative proxy ID (e.g., id: -1) to avoid collision with client IDs (positive)
  3. Original ID is stored in pending_backend_requests
  4. Message forwarded to client with rewritten ID
  5. 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):
Why needed:
  • 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, didChange contents continue to be recorded
Selective Restoration: When spawning a backend for 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 in src/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

See Multi-Backend Pool for detailed pool management, Venv Detection for search algorithm, and Warmup Readiness for index-dependent request queuing.