Skip to main content

Module conversation

Module conversation 

Source
Expand description

Conversation identity for read-cache scoping.

The [unchanged] re-read stub means “you already have this in context” — which is only true within the same conversation / context window. The read SessionCache is shared across all chats served by one daemon, so without scoping a file delivered in chat A could be stubbed for a re-read in chat B (which never received it).

Cursor’s hooks write the live conversation id to active_transcript.json (2h TTL); we read it via crate::hook_handlers::load_active_transcript but cache it behind a short TTL so the read hot path never stats+parses a file on every call. The last-known-good value is retained across a transient refresh miss, so a momentary read failure never spuriously invalidates valid stubs.

A Cursor subagent (CURSOR_TASK_ID set) is given its own task:{id} scope, so it is never served — nor records — a stub under another agent’s identity; this lets the stub gate replace the old blanket subagent force-fresh (#956).

§Concurrency hardening (#1040)

active_transcript.json is a single, last-writer-wins slot, and an MCP ctx_read call carries no caller identity (ToolContext has none), so with two concurrent top-level chats the daemon cannot prove which chat is asking: the resolved id may be the other chat’s (last writer) or a TTL-stale value. A matching id is therefore untrustworthy while more than one conversation is live. Rather than risk serving chat B a stub for content only chat A received, the gate withholds every stub while more than one conversation has been active recently — correctness over the re-read savings. Single-conversation daemons (the common case) keep the full savings; sightings are sampled on each transcript refresh. Subagents never feed this signal (they short-circuit on their task: scope), so a parent + its subagents are not counted as concurrent.

§Zero detection lag, and the host limit (#1042)

The stub decision resolves the caller with current_conversation_id_fresh, which re-samples active_transcript.json (bypassing the REFRESH_TTL cache) and notes the writer before the gate runs. A freshly-appeared second chat is therefore detected with no lag, closing the small window in which a stub could still leak before the next refresh sampled it.

What is not reachable as a pure lean-ctx change is recovering the stub savings while chats run concurrently: that needs a per-call caller identity, and Cursor exposes none. The MCP tools/call carries no conversation id (no documented _meta), and a beforeMCPExecution hook can gate a call’s permission but not rewrite its arguments. So the daemon cannot prove which chat a direct ctx_read belongs to, and withholding under concurrency stays the correct ceiling until the host adds per-call identity.

Disabled with LEAN_CTX_CONVERSATION_SCOPE=0 (falls back to the legacy process-scoped behavior).

Functions§

conversation_allows_cold_stub
Whether a cold [unchanged] stub may be served — i.e. one backed only by the persisted index (crate::core::read_stub_index) after a daemon restart, with no live in-memory entry.
conversation_allows_stub
Whether a [unchanged] stub may be served for an entry that was delivered to delivered, given the current conversation.
current_conversation_id
The current conversation id, or None when no conversation context is available (hooks not installed, TTL expired with no prior value, or scoping disabled). None preserves the legacy process-scoped cache behavior.
current_conversation_id_fresh
Like current_conversation_id but bypasses the REFRESH_TTL cache to re-sample active_transcript.json now. Used only on the re-read stub decision path: re-sampling notes a freshly-appeared second chat into the recency log (via refresh) before the gate runs, so concurrency is detected with no TTL lag and a stub can never leak to chat B in the window before detection catches up (#1042). The cost is one tiny, OS-cached transcript read per re-read — negligible against the source re-read it guards.