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;
hosts without one (Claude Code / Codex / CodeBuddy) supply a per-session
session_id instead, which load_active_transcript returns as the scope id
so a new session never inherits a prior one’s stubs (#1004). Either way it
carries a 2h TTL and 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 todelivered, given thecurrentconversation. - current_
conversation_ id - The current conversation id, or
Nonewhen no conversation context is available (hooks not installed, TTL expired with no prior value, or scoping disabled).Nonepreserves the legacy process-scoped cache behavior. - current_
conversation_ id_ fresh - Like
current_conversation_idbut bypasses theREFRESH_TTLcache to re-sampleactive_transcript.jsonnow. Used only on the re-read stub decision path: re-sampling notes a freshly-appeared second chat into the recency log (viarefresh) 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.