- Status: Implemented; release verification pending — 2026-07-22
- Date: 2026-07-21
- Builds on: ADR-0004 (multi-agent concurrency primitives), ADR-0011 (control-plane sprawl)
Context (verified at HEAD 91794a0f)
- Cache eviction can kill live runtimes.
PROJECT_CONTEXT_CACHE_LIMIT = 4
(state.rs:69); eviction protects only three scopes — default, the resolver's current
scope, and the scope being inserted (state/project_accessors.rs:234). In a shared
HTTP daemon with 5+ bound projects, another session's actively used context is
evicted and shut down (watchers, LSP pools, semantic engines die mid-request).
- Two daemons write the same store. The readonly and mutation launchd daemons run
the same binary against the same repo with independent watchers and index writers.
SQLite locking prevents corruption but not duplicate cold builds, last-writer races,
or cross-daemon analysis-cache pollution (observed during shared-runtime validation:
version-agnostic cache keys served stale results across daemon generations).
- No refresh ordering. A slow, older refresh can land after a newer watcher-driven
update; nothing versions index generations.
Decision
- Exactly one writable runtime per canonical project. Every
ProjectContext
acquires an OS advisory lease at
~/.codelens/runtime/project-writers/<sha256(canonical-project)>.lock. The path is
outside the checkout so repository-controlled symlinks cannot redirect lease
metadata. A second process fails closed with typed project_writer_busy (-32010);
it does not open a private or nominally read-only fallback runtime. The launchd
deployment therefore exposes one HTTP endpoint (:7838), with readonly/review/
builder policy selected per session through profile and RBAC.
- Observation tickets plus commit CAS.
refresh_all, index_files, remove, and
lazy ensure operations allocate ordered observations shared by every persistent
SymbolIndex for the same database. A stale observation may finish analysis but
cannot overwrite a newer indexed value or tombstone. committed_generation
advances only after a successful transaction that changed stored analysis.
- Per-fingerprint analysis singleflight. Expensive parse/import/call extraction is
coalesced by
{relative_path, mtime_ms, content_hash} across refresh, incremental
indexing, lazy ensure, and persistent index instances. Mutation tickets remain
independent, and unrelated fingerprints continue in parallel. Cold project-runtime
construction has a separate per-project build singleflight. Embedding and report-
artifact coalescing remain follow-up optimizations rather than part of this safety
boundary.
- Reader generation fence. Resolved symbol-backed handlers are classified in the
generated tool metadata. Dispatch captures the exact active
SymbolIndex generation;
if it changes before a successful response completes, the payload is discarded and a
typed retryable index_generation_changed (-32011) error is returned. Handlers are
never replayed automatically.
- Session-aware eviction. Session project binding and cache retirement share one
synchronization boundary. Live session scopes and in-flight
Arc holders are
protected; an idle context is fully shut down and releases its lease before a bind may
rebuild it. The cache limit remains a bound on idle runtimes, not active sessions.
Consequences
- Parallel hosts can share one daemon without cross-session runtime teardown or mixed-
generation symbol responses.
- Memory ceiling becomes
limit × idle + live sessions instead of a hard 4; a follow-up
knob (CODELENS_PROJECT_CACHE_LIMIT) makes the idle bound operator-tunable.
- A crashed holder releases the OS lease automatically; the next holder increments the
durable lease generation. Coordination-store failure remains independently fail-closed.
vm_stat-based load shedding and embedding/artifact singleflight remain backlog items;
they are not implied by the writer-safety contract.
Verification (exit criteria)
- Two processes targeting one project: one full runtime succeeds, the contender receives
project_writer_busy before WAL contention; killing the holder permits immediate
reacquisition with a higher lease generation.
- Concurrent refresh/index/ensure calls for one fingerprint perform one analysis; changed
content performs a distinct analysis; disjoint files remain parallel; newest CAS wins.
- A symbol-backed request whose generation changes returns retryable
-32011 and never
exposes its mixed payload; a stable request and an existing handler error pass through.
- Five live session bindings can exceed the four-entry idle cache without active-context
shutdown; bind-versus-evict races never observe a half-retired runtime.
- WAL process tests preserve an old explicit read snapshot, expose the commit to a new
snapshot, and leave no partial row after an uncommitted writer is killed.
- One release smoke starts
:7838, rejects a same-project contender, restarts after kill,
resurrects the prior HTTP session, and serves its original project binding.