Skip to main content

lean_ctx/tools/ctx_read/
dispatch.rs

1use super::{
2    CrpMode, HookPoint, PluginManager, ReadMode, ReadOutput, ReadTuning, SessionCache,
3    count_tokens, dedup_hook, handle_with_options_inner, kernel, protocol,
4};
5const MAX_RELAY_CONTENT_BYTES: usize = 8192;
6
7/// Modes whose compressed output is useful for cross-agent relay.
8const RELAY_ELIGIBLE_MODES: &[&str] = &["map", "map:v2", "signatures", "signatures:v2"];
9
10/// Extract relay-eligible content from a read result.
11fn relay_eligible_content(result: &ReadOutput) -> (Option<&str>, Option<&str>) {
12    let mode = result.resolved_mode.as_str();
13    if RELAY_ELIGIBLE_MODES.iter().any(|m| mode.starts_with(m))
14        && result.content.len() <= MAX_RELAY_CONTENT_BYTES
15    {
16        (Some(&result.content), Some(mode))
17    } else {
18        (None, None)
19    }
20}
21/// Reads a file through the cache and applies the requested compression mode.
22pub fn handle(cache: &mut SessionCache, path: &str, mode: &str, crp_mode: CrpMode) -> String {
23    handle_with_options(cache, path, mode, false, crp_mode, None)
24}
25
26/// Like `handle`, but invalidates the cache first to force a fresh disk read.
27pub fn handle_fresh(cache: &mut SessionCache, path: &str, mode: &str, crp_mode: CrpMode) -> String {
28    handle_with_options(cache, path, mode, true, crp_mode, None)
29}
30
31/// Reads a file with task-aware filtering to prioritize task-relevant content.
32pub fn handle_with_task(
33    cache: &mut SessionCache,
34    path: &str,
35    mode: &str,
36    crp_mode: CrpMode,
37    task: Option<&str>,
38) -> String {
39    let mut result = handle_with_options(cache, path, mode, false, crp_mode, task);
40    kernel::enrich_with_kernel(&mut result, task);
41    result
42}
43
44/// Like `handle_with_task`, also returns the resolved mode name and pre-counted tokens.
45pub fn handle_with_task_resolved(
46    cache: &mut SessionCache,
47    path: &str,
48    mode: &str,
49    crp_mode: CrpMode,
50    task: Option<&str>,
51) -> ReadOutput {
52    handle_with_options_resolved(
53        cache,
54        path,
55        mode,
56        false,
57        crp_mode,
58        task,
59        ReadTuning::resolve(None, &[]),
60    )
61}
62
63/// Like [`handle_with_task_resolved`] but with an explicit per-call
64/// aggressiveness (the `ctx_read` `aggressiveness` arg, #714). `None` falls back
65/// to the `LEAN_CTX_AGGRESSIVENESS` env var / config field.
66pub fn handle_with_task_resolved_tuned(
67    cache: &mut SessionCache,
68    path: &str,
69    mode: &str,
70    crp_mode: CrpMode,
71    task: Option<&str>,
72    aggressiveness: Option<f64>,
73    protect: &[String],
74) -> ReadOutput {
75    handle_with_options_resolved(
76        cache,
77        path,
78        mode,
79        false,
80        crp_mode,
81        task,
82        ReadTuning::resolve(aggressiveness, protect),
83    )
84}
85
86/// Like [`handle_with_task_resolved_tuned`] but accepts pre-read file content,
87/// avoiding disk I/O under the cache write-lock (Two-Phase Read pattern, #1098).
88#[allow(clippy::too_many_arguments)]
89pub fn handle_with_preread(
90    cache: &mut SessionCache,
91    path: &str,
92    mode: &str,
93    fresh: bool,
94    crp_mode: CrpMode,
95    task: Option<&str>,
96    aggressiveness: Option<f64>,
97    protect: &[String],
98    preread: String,
99) -> ReadOutput {
100    handle_with_options_resolved_preread(
101        cache,
102        path,
103        mode,
104        fresh,
105        crp_mode,
106        task,
107        ReadTuning::resolve(aggressiveness, protect),
108        Some(preread),
109    )
110}
111
112/// Fresh read with task-aware filtering (invalidates cache first).
113pub fn handle_fresh_with_task(
114    cache: &mut SessionCache,
115    path: &str,
116    mode: &str,
117    crp_mode: CrpMode,
118    task: Option<&str>,
119) -> String {
120    handle_with_options(cache, path, mode, true, crp_mode, task)
121}
122
123/// Fresh read with task-aware filtering, also returns the resolved mode name and pre-counted tokens.
124pub fn handle_fresh_with_task_resolved(
125    cache: &mut SessionCache,
126    path: &str,
127    mode: &str,
128    crp_mode: CrpMode,
129    task: Option<&str>,
130) -> ReadOutput {
131    handle_with_options_resolved(
132        cache,
133        path,
134        mode,
135        true,
136        crp_mode,
137        task,
138        ReadTuning::resolve(None, &[]),
139    )
140}
141
142/// Fresh-read variant of [`handle_with_task_resolved_tuned`] (#714).
143pub fn handle_fresh_with_task_resolved_tuned(
144    cache: &mut SessionCache,
145    path: &str,
146    mode: &str,
147    crp_mode: CrpMode,
148    task: Option<&str>,
149    aggressiveness: Option<f64>,
150    protect: &[String],
151) -> ReadOutput {
152    handle_with_options_resolved(
153        cache,
154        path,
155        mode,
156        true,
157        crp_mode,
158        task,
159        ReadTuning::resolve(aggressiveness, protect),
160    )
161}
162
163fn handle_with_options(
164    cache: &mut SessionCache,
165    path: &str,
166    mode: &str,
167    fresh: bool,
168    crp_mode: CrpMode,
169    task: Option<&str>,
170) -> String {
171    handle_with_options_resolved(
172        cache,
173        path,
174        mode,
175        fresh,
176        crp_mode,
177        task,
178        ReadTuning::resolve(None, &[]),
179    )
180    .content
181}
182
183/// `LEAN_CTX_FORCE_FRESH=1` — an explicit operator override that always forces a
184/// cold full read, independent of conversation scoping.
185pub(crate) fn force_fresh_env() -> bool {
186    static FORCE_FRESH: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
187    *FORCE_FRESH.get_or_init(|| {
188        std::env::var("LEAN_CTX_FORCE_FRESH").is_ok_and(|v| v == "1" || v == "true")
189    })
190}
191
192/// Detects a subagent (forked agent) execution context.
193///
194/// A subagent must never be served a stub for content only the parent received.
195/// That used to be enforced by force-freshing *every* subagent read; with
196/// conversation scoping (#954/#955) the subagent instead runs under its own scope
197/// (`conversation::current_conversation_id` → `task:{id}` or `proc:{id}`), so
198/// the stub gate withholds cross-agent stubs precisely while restoring the
199/// subagent's *own* cheap re-reads. The blanket force-fresh is therefore kept
200/// only as the fallback when scoping is disabled (#956).
201///
202/// Checks `CURSOR_TASK_ID` (Cursor) and `CLAUDE_CODE_ENTRYPOINT=local-agent`
203/// (future Claude Code subagent marker). Current Claude Code is handled by
204/// per-process scoping in [`crate::core::conversation`] (#1292).
205pub(crate) fn is_subagent_context() -> bool {
206    static IS_SUBAGENT: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
207    *IS_SUBAGENT.get_or_init(|| {
208        std::env::var("CURSOR_TASK_ID").is_ok_and(|v| !v.is_empty())
209            || std::env::var("CLAUDE_CODE_ENTRYPOINT")
210                .ok()
211                .as_deref()
212                .map(str::trim)
213                == Some("local-agent")
214    })
215}
216
217/// Keeps subagent cache isolation while independently deciding whether a
218/// cross-agent delivery lookup may return a stub.
219#[allow(clippy::fn_params_excessive_bools)]
220pub(crate) fn effective_fresh_flags(
221    fresh: bool,
222    force_fresh: bool,
223    subagent_context: bool,
224    delivery_for_subagents: bool,
225) -> (bool, bool) {
226    let effective_fresh_for_cache = fresh || force_fresh || subagent_context;
227    let effective_fresh_for_delivery =
228        fresh || force_fresh || (subagent_context && !delivery_for_subagents);
229    (effective_fresh_for_cache, effective_fresh_for_delivery)
230}
231
232pub(crate) fn effective_fresh_for_delivery(fresh: bool) -> bool {
233    let config = crate::core::config::Config::load();
234    effective_fresh_flags(
235        fresh,
236        force_fresh_env(),
237        is_subagent_context(),
238        config.ocla.delivery.delivery_for_subagents,
239    )
240    .1
241}
242
243fn handle_with_options_resolved(
244    cache: &mut SessionCache,
245    path: &str,
246    mode: &str,
247    fresh: bool,
248    crp_mode: CrpMode,
249    task: Option<&str>,
250    tuning: ReadTuning<'_>,
251) -> ReadOutput {
252    handle_with_options_resolved_preread(cache, path, mode, fresh, crp_mode, task, tuning, None)
253}
254
255fn handle_with_options_resolved_preread(
256    cache: &mut SessionCache,
257    path: &str,
258    mode: &str,
259    fresh: bool,
260    crp_mode: CrpMode,
261    task: Option<&str>,
262    tuning: ReadTuning<'_>,
263    preread: Option<String>,
264) -> ReadOutput {
265    // Subagents retain isolated session caches, but can use delivery stubs when
266    // configured because those stubs explicitly identify another agent's read.
267    let config = crate::core::config::Config::load();
268    let (effective_fresh_for_cache, effective_fresh_for_delivery) = effective_fresh_flags(
269        fresh,
270        force_fresh_env(),
271        is_subagent_context(),
272        config.ocla.delivery.delivery_for_subagents,
273    );
274
275    let compress_protected = mode != "raw"
276        && !mode.starts_with("lines:")
277        && crate::core::config::Config::load()
278            .proxy
279            .is_path_compress_protected(path);
280
281    // Hash once for cross-agent delivery. The same snapshot is used for both
282    // the pre-read lookup and the post-read record, avoiding a second disk read.
283    let delivery_metadata = config
284        .ocla
285        .delivery_enabled()
286        .then(|| file_blake3_prefix(path))
287        .flatten();
288
289    if !effective_fresh_for_delivery
290        && !compress_protected
291        && let Some((hash, mtime)) = delivery_metadata
292        && let Some(stub) = try_cross_agent_stub(path, mode, hash, mtime)
293    {
294        return stub;
295    }
296
297    if mode == "auto" {
298        let touched: Vec<String> = cache
299            .get_all_entries()
300            .iter()
301            .map(|(p, _)| (*p).clone())
302            .collect();
303        if crate::core::relevance_gate::should_gate(path, mode, task, &touched) {
304            let meta = std::fs::metadata(path);
305            let byte_count = meta.as_ref().map_or(0, std::fs::Metadata::len);
306            let line_count = preread
307                .as_ref()
308                .map_or(0, |c| bytecount::count(c.as_bytes(), b'\n'));
309            let stub = crate::core::relevance_gate::irrelevant_stub(path, line_count, byte_count);
310            let stub_tokens = count_tokens(&stub);
311            return ReadOutput {
312                content: stub,
313                resolved_mode: "auto".into(),
314                output_tokens: stub_tokens,
315                is_cache_hit: false,
316            };
317        }
318    }
319
320    if PluginManager::has_listener("pre_read") {
321        PluginManager::fire_hook_background(HookPoint::PreRead {
322            path: path.to_string(),
323        });
324    }
325
326    if let Ok(mut bt) = crate::core::bounce_tracker::global().lock() {
327        bt.next_seq();
328    }
329    let mut result = handle_with_options_inner(
330        cache,
331        path,
332        mode,
333        effective_fresh_for_cache,
334        crp_mode,
335        task,
336        tuning,
337        preread,
338    );
339
340    if let Some(entry) = cache.get_mut(path) {
341        entry.last_mode.clone_from(&result.resolved_mode);
342        if matches!(result.resolved_mode.as_str(), "full" | "full-compact")
343            && entry.full_content_delivered
344            && result.is_cache_hit
345            && entry.bump_reread() >= crate::core::cache::full_degradation_threshold()
346        {
347            entry.full_content_delivered = false;
348            entry.reset_reread_count();
349            crate::core::auto_mode_resolver::count_source("full_delivery_degraded");
350        }
351        // #841: a partial/filtered read means the model's most recent view is NOT
352        // the full content. Clear the delivery flag so a subsequent mode="full"
353        // re-delivers real content instead of the [unchanged] stub. Without this,
354        // a task→full sequence returns an empty stub because the flag was set by an
355        // earlier full delivery and never cleared by the intervening non-full read.
356        if !matches!(result.resolved_mode.as_str(), "full" | "full-compact") {
357            entry.full_content_delivered = false;
358            entry.reset_reread_count();
359        }
360    }
361
362    if !result.is_cache_hit
363        && let Some((hash, mtime)) = delivery_metadata
364    {
365        let line_count = cache.get(path).map_or(0, |entry| entry.line_count as u32);
366        let relay = relay_eligible_content(&result);
367        record_cross_agent_delivery(
368            path,
369            hash,
370            mtime,
371            line_count,
372            result.output_tokens,
373            relay.0,
374            relay.1,
375        );
376    }
377
378    // SSOT via [`ReadMode`] (#528): lossy summaries may elide shared blocks.
379    let dedup_allowed = result
380        .resolved_mode
381        .parse::<ReadMode>()
382        .is_ok_and(|m| m.is_lossy_summary());
383    if dedup_allowed && let Some(deduped) = cache.apply_dedup(path, &result.content) {
384        let new_tokens = count_tokens(&deduped);
385        if new_tokens < result.output_tokens {
386            result.content = deduped;
387            result.output_tokens = new_tokens;
388        }
389    }
390
391    // R28: Kernel content dedup — detect re-reads of unchanged content.
392    if let Some(stub) = dedup_hook::maybe_dedup(path, &result.content, mode, fresh) {
393        let stub_tokens = count_tokens(&stub);
394        if stub_tokens < result.output_tokens {
395            result.content = stub;
396            result.output_tokens = stub_tokens;
397            result.is_cache_hit = true;
398        }
399    }
400
401    // R30: Feed bounce-tracker signal into adaptive compression bridge.
402    crate::core::context_kernel::adaptive_hook::update_from_bounce_tracker();
403    if let Ok(mut bt) = crate::core::bounce_tracker::global().lock() {
404        let original_tokens = cache.get(path).map_or(0, |e| e.original_tokens);
405        bt.record_read(
406            path,
407            &result.resolved_mode,
408            result.output_tokens,
409            original_tokens,
410        );
411
412        // Quality signals (#538): compressed reads count as clean until a
413        // bounce proves otherwise (the bounce signal outweighs 6:1); large
414        // full reads of never-bouncing extensions are wasted compression
415        // opportunities and push the learned threshold up.
416        // SSOT via [`ReadMode`] (#528): only verbatim `full` and the `diff`
417        // delta are uncompressed. A resolved window is always the canonical
418        // `lines:N-M` (parses to `Lines` ⇒ compressed); the default of `true`
419        // for the unreachable bare `"lines"` keeps prior behaviour everywhere a
420        // real resolved mode can occur.
421        let compressed = result
422            .resolved_mode
423            .parse::<ReadMode>()
424            .map_or(true, |m| m.counts_as_compressed());
425        if compressed {
426            crate::core::adaptive_thresholds::record_quality_signal(
427                path,
428                crate::core::threshold_learning::QualitySignal::CleanCompressed,
429            );
430        } else if result.resolved_mode == "full"
431            && result.output_tokens > 2000
432            && bt.bounce_rate_for_extension(path).unwrap_or(0.0) < 0.05
433        {
434            crate::core::adaptive_thresholds::record_quality_signal(
435                path,
436                crate::core::threshold_learning::QualitySignal::WastedFull,
437            );
438        }
439    }
440
441    // Plugin seam: emit the realized compression stats. Same zero-cost guard.
442    if PluginManager::has_listener("post_compress") {
443        let original_tokens = cache.get(path).map_or(0, |e| e.original_tokens);
444        PluginManager::fire_hook_background(HookPoint::PostCompress {
445            path: path.to_string(),
446            original_tokens,
447            compressed_tokens: result.output_tokens,
448        });
449    }
450
451    // Stigmergy (#540): deposit a Hot scent for this read in the background
452    // (the field file lock may briefly block; never stall the read path). The
453    // foreign-claim hint is intentionally NOT appended to the body: it carries a
454    // relative timestamp ("claimed Nm ago"), which would make the output a
455    // non-pure function of wall-clock time and defeat provider prompt caching
456    // (#498). The deposit remains so the field still reflects active work.
457    {
458        let self_agent = crate::core::scent_field::scent_agent_id();
459        let scent_path = crate::core::pathutil::normalize_tool_path(path);
460        std::thread::spawn(move || {
461            crate::core::scent_field::deposit(
462                self_agent,
463                crate::core::scent_field::ScentKind::Hot,
464                &scent_path,
465                0.3,
466            );
467        });
468    }
469
470    crate::core::context_gc::maybe_gc(cache);
471
472    result
473}
474
475/// Attempt to serve a `mode="full"` cache hit (`[unchanged …]`) using only a
476/// shared borrow of the cache.
477///
478/// Returns `None` when the file is not cached, was modified on disk, full
479/// content was never delivered, or the cache policy forbids stubbing — in those
480/// cases the caller must fall back to the write path.
481///
482/// This is the read-locked fast path: it needs no `&mut SessionCache`, so the
483/// dominant "re-read an unchanged file" case proceeds under a shared lock and
484/// parallel reads of distinct files no longer serialize on a global write lock.
485pub fn try_stub_hit_readonly(cache: &SessionCache, path: &str) -> Option<ReadOutput> {
486    // Resolve the caller *fresh* (TTL-bypassed): the stub gate's concurrency
487    // detection must see a just-appeared second chat with zero lag, else a stub
488    // could leak across chats in the pre-detection window (#1042).
489    let current_conversation = crate::core::conversation::current_conversation_id_fresh();
490    try_stub_hit_readonly_scoped(cache, path, current_conversation.as_deref())
491}
492
493/// Conversation-scoped core of [`try_stub_hit_readonly`]. The current
494/// conversation id is injected (not read from the global resolver) so the
495/// conversation gate can be tested deterministically without global state.
496pub(crate) fn try_stub_hit_readonly_scoped(
497    cache: &SessionCache,
498    path: &str,
499    current_conversation: Option<&str>,
500) -> Option<ReadOutput> {
501    let no_deg = crate::core::config::Config::load().no_degrade_effective();
502    let prof = crate::core::profiles::active_profile();
503    let force_full = no_deg
504        || (prof.read.default_mode_effective() == "full"
505            && prof.compression.crp_mode_effective() == "off");
506    let policy_allows_stub =
507        crate::server::compaction_sync::effective_cache_policy() != "safe" && !force_full;
508    if !policy_allows_stub {
509        return None;
510    }
511
512    // Warm path: a live in-memory entry is the freshest source of truth.
513    if let Some(file_ref) = cache.get_file_ref_readonly(path) {
514        let (cached_mtime, cached_hash, line_count, delivered_conv) = {
515            let entry = cache.get(path)?;
516            (
517                entry.stored_mtime,
518                entry.hash.clone(),
519                entry.line_count,
520                entry.delivered_conversation.clone(),
521            )
522        };
523        if crate::core::cache::is_cache_entry_stale_verified(path, cached_mtime, &cached_hash)
524            || !cache.is_full_delivered(path)
525        {
526            return None;
527        }
528        // Conversation scoping (#954): only stub when THIS conversation received
529        // the content. A different (or unknown) conversation re-delivers in full
530        // rather than emit a misleading stub. `current == None` (hooks absent)
531        // preserves legacy process-scoped behavior, so single-chat hit rates are
532        // unchanged.
533        if !crate::core::conversation::conversation_allows_stub(
534            current_conversation,
535            delivered_conv.as_deref(),
536        ) {
537            crate::core::cache_telemetry::record_conversation_mismatch();
538            return None;
539        }
540        let original_tokens = cache.record_cache_hit(path)?.original_tokens;
541        crate::core::telemetry::global_metrics().record_cache(true);
542        let stub = render_unchanged_stub(&file_ref, path, line_count);
543        crate::core::stats::record_reread(original_tokens.saturating_sub(stub.output_tokens));
544        return Some(stub);
545    }
546
547    // Cold fallback (#955): no live entry (e.g. after a daemon restart or idle
548    // clear). Serve the stub from the persisted index iff the file is unchanged
549    // AND the *same known* conversation is asking — a stricter gate than the warm
550    // path, because a cold stub crosses a process boundary (no "no context →
551    // legacy" escape; see `conversation_allows_cold_stub`).
552    let rec = crate::core::read_stub_index::lookup(path)?;
553    if crate::core::cache::is_cache_entry_stale_verified(path, rec.stored_mtime(), &rec.hash) {
554        return None;
555    }
556    if !crate::core::conversation::conversation_allows_cold_stub(
557        current_conversation,
558        rec.delivered_conversation.as_deref(),
559    ) {
560        crate::core::cache_telemetry::record_conversation_mismatch();
561        return None;
562    }
563    Some(render_unchanged_stub(&rec.file_ref, path, rec.line_count))
564}
565
566/// Renders the `[unchanged …]` stub body shared by the warm and cold stub paths.
567///
568/// #498 determinism: the stub is a pure function of (file_ref, path, line_count),
569/// so identical re-reads stay byte-stable and provider prompt caching applies.
570/// The `fresh=true` escape is a *static* suffix (no rotating proof lines or
571/// read-count notes), so a re-reader in non-meta mode still sees how to force the
572/// content (#513) without breaking byte-stability.
573fn render_unchanged_stub(file_ref: &str, path: &str, line_count: usize) -> ReadOutput {
574    let short = protocol::shorten_path(path);
575    let out = if crate::core::protocol::meta_visible() {
576        format!(
577            "{file_ref}={short} [unchanged {line_count}L]\nUnchanged on disk. Use fresh=true to force re-read.",
578        )
579    } else {
580        format!("{file_ref}={short} [unchanged {line_count}L · fresh=true to re-read]")
581    };
582    let out = crate::core::redaction::redact_text_if_enabled(&out);
583    let sent = count_tokens(&out);
584    ReadOutput {
585        content: out,
586        resolved_mode: "full".into(),
587        output_tokens: sent,
588        is_cache_hit: true,
589    }
590}
591
592/// Outcome of [`resolve_explicit_delta_mode`]: the (possibly rewritten) read
593/// mode plus an optional advisory note to surface to the agent.
594#[derive(Debug, Clone, PartialEq, Eq)]
595pub struct DeltaExplicitDecision {
596    /// The mode the read should proceed with (rewritten only when the feature
597    /// fires; otherwise the caller's mode, unchanged).
598    pub mode: String,
599    /// A byte-stable advisory appended to the read body when the mode was
600    /// rewritten to `diff`. `None` when nothing was rewritten or the collapse
601    /// was a silent `lines:`→`full` stub.
602    pub note: Option<String>,
603}
604
605/// Decide whether an **explicit** `full`/`lines:N-M` re-read of a session-cached
606/// file should be served as a delta instead of re-emitting content the model
607/// already holds (the `delta_explicit` opt-in; env `LCTX_DELTA_EXPLICIT`).
608///
609/// Returns the mode the read should proceed with:
610/// - **Changed on disk** (verified mtime+md5 stale) and full content is cached →
611///   `diff`, plus an advisory note. The diff carries exactly the new
612///   information in a fraction of the tokens.
613/// - **Unchanged** and the request is `lines:` of an already-fully-delivered
614///   file → `full`, so the read collapses to the ~15-token `[unchanged]` stub
615///   instead of re-extracting a window the model has seen.
616/// - Otherwise the caller's `mode` is returned untouched.
617///
618/// First reads (nothing cached) and `fresh=true` are never affected — the
619/// caller gates those before calling. Staleness uses the **verified** variant
620/// ([`crate::core::cache::is_cache_entry_stale_verified`]) so a same-second
621/// write on a coarse-granularity filesystem cannot be mistaken for "unchanged"
622/// and yield a misleading empty diff (#498 determinism).
623///
624/// Pure w.r.t. (cache, path, mode, enabled): no wall-clock, counters, or
625/// randomness enter the result, so identical inputs stay byte-stable.
626pub fn resolve_explicit_delta_mode(
627    cache: &SessionCache,
628    path: &str,
629    mode: &str,
630    explicit_mode: bool,
631    fresh: bool,
632    enabled: bool,
633) -> DeltaExplicitDecision {
634    let unchanged = DeltaExplicitDecision {
635        mode: mode.to_string(),
636        note: None,
637    };
638    if fresh
639        || !enabled
640        || !explicit_mode
641        || !(mode == "full" || mode == "full-compact" || mode.starts_with("lines:"))
642    {
643        return unchanged;
644    }
645    let Some(entry) = cache.get(path) else {
646        // First read this session — nothing to diff against.
647        return unchanged;
648    };
649    let stale =
650        crate::core::cache::is_cache_entry_stale_verified(path, entry.stored_mtime, &entry.hash);
651    if stale {
652        // Only divert to a diff when full content is actually cached: the diff
653        // base is that full content (see `handle_diff`), never a compressed
654        // view. Without it, `handle_diff` would have nothing to compare.
655        if entry.content().is_some() {
656            return DeltaExplicitDecision {
657                mode: "diff".to_string(),
658                note: Some(format!(
659                    "[delta-explicit] requested mode={mode} served as a diff: the file \
660                     changed since your last read and the diff is the new information. \
661                     Pass fresh=true if you need the full content re-emitted."
662                )),
663            };
664        }
665        return unchanged;
666    }
667    // Unchanged on disk: a `lines:` window of a file already delivered in full
668    // re-emits text the model holds — collapse to the full-mode stub
669    // (~15 tokens). A plain `full` re-read already hits that stub downstream.
670    if mode.starts_with("lines:") && cache.is_full_delivered(path) {
671        return DeltaExplicitDecision {
672            mode: "full".to_string(),
673            note: None,
674        };
675    }
676    unchanged
677}
678
679pub(crate) fn file_blake3_prefix(path: &str) -> Option<([u8; 12], u64)> {
680    let meta = std::fs::metadata(path).ok()?;
681    let mtime = meta
682        .modified()
683        .ok()?
684        .duration_since(std::time::UNIX_EPOCH)
685        .ok()?
686        .as_secs();
687    let bytes = std::fs::read(path).ok()?;
688    let hash = blake3::hash(&bytes);
689    let full = hash.as_bytes();
690    let mut prefix = [0u8; 12];
691    prefix.copy_from_slice(&full[..12]);
692    Some((prefix, mtime))
693}
694
695pub(crate) fn try_cross_agent_stub(
696    path: &str,
697    mode: &str,
698    hash: [u8; 12],
699    mtime: u64,
700) -> Option<ReadOutput> {
701    if !crate::core::config::Config::load().ocla.delivery_enabled() {
702        return None;
703    }
704    if matches!(mode, "full" | "raw" | "diff") {
705        return None;
706    }
707    let current_agent = std::env::var("CURSOR_TASK_ID")
708        .or_else(|_| std::env::var("CLAUDECODE"))
709        .unwrap_or_else(|_| format!("local-{}", std::process::id()));
710    let current_conversation = crate::core::conversation::current_conversation_id()
711        .unwrap_or_else(|| current_agent.clone());
712    let reg = crate::core::ocla::OclaRegistry::global();
713    let record = crate::daemon_client::try_delivery_check_blocking(
714        &hash,
715        mtime,
716        path,
717        Some(&current_agent),
718        Some(&current_conversation),
719    )
720    .or_else(|| {
721        reg.delivery_registry.check_delivery(
722            &hash,
723            mtime,
724            path,
725            Some(&current_agent),
726            Some(&current_conversation),
727        )
728    })?;
729
730    let short = protocol::shorten_path(path);
731
732    if let Some(ref content) = record.relay_content {
733        let relay_mode = record.relay_mode.as_deref().unwrap_or("map");
734        let header = format!(
735            "{short} [relayed from {} · {relay_mode} · {}L]",
736            record.agent_id, record.line_count,
737        );
738        let body = format!("{header}\n{content}");
739        let tokens = count_tokens(&body);
740        reg.delivery_registry
741            .record_stub_served(&record, tokens as u64);
742        return Some(ReadOutput {
743            content: body,
744            resolved_mode: "cross-agent-relay".into(),
745            output_tokens: tokens,
746            is_cache_hit: true,
747        });
748    }
749
750    let stub = format!(
751        "{short} [cross-agent · {lines}L · read by {agent} · use fresh=true to force]",
752        lines = record.line_count,
753        agent = record.agent_id,
754    );
755    let tokens = count_tokens(&stub);
756    reg.delivery_registry
757        .record_stub_served(&record, tokens as u64);
758    Some(ReadOutput {
759        content: stub,
760        resolved_mode: "cross-agent-stub".into(),
761        output_tokens: tokens,
762        is_cache_hit: true,
763    })
764}
765
766pub(crate) fn record_cross_agent_delivery(
767    path: &str,
768    hash: [u8; 12],
769    mtime: u64,
770    line_count: u32,
771    tokens: usize,
772    relay_content: Option<&str>,
773    relay_mode: Option<&str>,
774) {
775    if !crate::core::config::Config::load().ocla.delivery_enabled() {
776        return;
777    }
778    let agent_id = std::env::var("CURSOR_TASK_ID")
779        .or_else(|_| std::env::var("CLAUDECODE"))
780        .unwrap_or_else(|_| format!("local-{}", std::process::id()));
781    let conversation_id =
782        crate::core::conversation::current_conversation_id().unwrap_or_else(|| agent_id.clone());
783    let entry = crate::core::ocla::types::DeliveryEntry {
784        blake3: hash,
785        path: path.into(),
786        line_count,
787        token_count: tokens as u64,
788        agent_id,
789        conversation_id,
790        mtime,
791        relay_content: relay_content
792            .filter(|c| c.len() <= MAX_RELAY_CONTENT_BYTES)
793            .map(str::to_string),
794        relay_mode: relay_mode.map(str::to_string),
795    };
796    crate::daemon_client::try_delivery_record_blocking(&entry);
797    let reg = crate::core::ocla::OclaRegistry::global();
798    reg.delivery_registry.record_delivery(entry);
799}
800
801#[cfg(test)]
802mod tests {
803    use super::{
804        SessionCache, effective_fresh_flags, try_cross_agent_stub, try_stub_hit_readonly_scoped,
805    };
806    use std::sync::atomic::Ordering;
807
808    #[test]
809    fn cross_agent_stub_miss_returns_none() {
810        let stub = try_cross_agent_stub("/nonexistent/file.rs", "auto", [0; 12], 0);
811        assert!(stub.is_none());
812    }
813
814    #[test]
815    fn subagent_delivery_policy_keeps_cache_fresh_but_allows_delivery_by_default() {
816        let delivery_for_subagents =
817            crate::core::config::DeliveryConfig::default().delivery_for_subagents;
818        assert!(
819            delivery_for_subagents,
820            "delivery must default to enabled for subagents"
821        );
822        let (cache_fresh, delivery_fresh) =
823            effective_fresh_flags(false, false, true, delivery_for_subagents);
824        assert!(cache_fresh, "subagent cache must remain isolated");
825        assert!(
826            !delivery_fresh,
827            "default policy must allow a cross-agent delivery lookup"
828        );
829    }
830
831    #[test]
832    fn subagent_delivery_policy_can_force_fresh_delivery() {
833        let (cache_fresh, delivery_fresh) = effective_fresh_flags(false, false, true, false);
834        assert!(cache_fresh);
835        assert!(delivery_fresh, "disabled policy must bypass delivery stubs");
836    }
837
838    #[test]
839    fn cross_agent_fallback_is_deterministic() {
840        // When no CURSOR_TASK_ID or CLAUDECODE env var is set, the fallback
841        // must be deterministic (not PID-based) for provider cache stability.
842        let _lock = crate::core::data_dir::test_env_lock();
843        crate::test_env::remove_var("CURSOR_TASK_ID");
844        crate::test_env::remove_var("CLAUDECODE");
845        let id1 = std::env::var("CURSOR_TASK_ID")
846            .or_else(|_| std::env::var("CLAUDECODE"))
847            .unwrap_or_else(|_| format!("local-{}", std::process::id()));
848        let id2 = std::env::var("CURSOR_TASK_ID")
849            .or_else(|_| std::env::var("CLAUDECODE"))
850            .unwrap_or_else(|_| format!("local-{}", std::process::id()));
851        assert_eq!(id1, id2, "fallback agent ID must be deterministic");
852        assert!(!id1.contains("proc:"), "must not contain PID");
853    }
854
855    #[test]
856    fn warm_stub_hit_records_central_telemetry() {
857        let dir = tempfile::tempdir().unwrap();
858        let file = dir.path().join("telemetry-hit.rs");
859        std::fs::write(&file, "fn telemetry_hit() {}\n").unwrap();
860        let path = file.to_string_lossy();
861        let mut cache = SessionCache::new();
862        cache.store(&path, "fn telemetry_hit() {}\n");
863        cache.mark_full_delivered(&path);
864
865        let metrics = crate::core::telemetry::global_metrics();
866        let before = metrics.cache_hits.load(Ordering::Relaxed);
867        let output = try_stub_hit_readonly_scoped(&cache, &path, None);
868        let after = metrics.cache_hits.load(Ordering::Relaxed);
869
870        assert!(output.is_some(), "warm re-read must use the stub cache");
871        assert!(
872            after > before,
873            "stub cache hit must increment central telemetry"
874        );
875    }
876
877    #[test]
878    fn relay_does_not_poison_session_cache() {
879        let mut cache = SessionCache::new();
880        let path = "/tmp/test_relay_poison.rs";
881        cache.store(path, "original content");
882        assert_eq!(
883            cache
884                .get(path)
885                .map(|e| e.compressed_outputs.contains_key("cross-agent-relay")),
886            Some(false),
887            "cross-agent-relay must not exist in session cache"
888        );
889    }
890}