1use super::{
2 CrpMode, HookPoint, PluginManager, ReadMode, ReadOutput, ReadTuning, SessionCache,
3 count_tokens, dedup_hook, handle_with_options_inner, kernel, protocol,
4};
5
6pub fn handle(cache: &mut SessionCache, path: &str, mode: &str, crp_mode: CrpMode) -> String {
8 handle_with_options(cache, path, mode, false, crp_mode, None)
9}
10
11pub fn handle_fresh(cache: &mut SessionCache, path: &str, mode: &str, crp_mode: CrpMode) -> String {
13 handle_with_options(cache, path, mode, true, crp_mode, None)
14}
15
16pub fn handle_with_task(
18 cache: &mut SessionCache,
19 path: &str,
20 mode: &str,
21 crp_mode: CrpMode,
22 task: Option<&str>,
23) -> String {
24 let mut result = handle_with_options(cache, path, mode, false, crp_mode, task);
25 kernel::enrich_with_kernel(&mut result, task);
26 result
27}
28
29pub fn handle_with_task_resolved(
31 cache: &mut SessionCache,
32 path: &str,
33 mode: &str,
34 crp_mode: CrpMode,
35 task: Option<&str>,
36) -> ReadOutput {
37 handle_with_options_resolved(
38 cache,
39 path,
40 mode,
41 false,
42 crp_mode,
43 task,
44 ReadTuning::resolve(None, &[]),
45 )
46}
47
48pub fn handle_with_task_resolved_tuned(
52 cache: &mut SessionCache,
53 path: &str,
54 mode: &str,
55 crp_mode: CrpMode,
56 task: Option<&str>,
57 aggressiveness: Option<f64>,
58 protect: &[String],
59) -> ReadOutput {
60 handle_with_options_resolved(
61 cache,
62 path,
63 mode,
64 false,
65 crp_mode,
66 task,
67 ReadTuning::resolve(aggressiveness, protect),
68 )
69}
70
71#[allow(clippy::too_many_arguments)]
74pub fn handle_with_preread(
75 cache: &mut SessionCache,
76 path: &str,
77 mode: &str,
78 fresh: bool,
79 crp_mode: CrpMode,
80 task: Option<&str>,
81 aggressiveness: Option<f64>,
82 protect: &[String],
83 preread: String,
84) -> ReadOutput {
85 handle_with_options_resolved_preread(
86 cache,
87 path,
88 mode,
89 fresh,
90 crp_mode,
91 task,
92 ReadTuning::resolve(aggressiveness, protect),
93 Some(preread),
94 )
95}
96
97pub fn handle_fresh_with_task(
99 cache: &mut SessionCache,
100 path: &str,
101 mode: &str,
102 crp_mode: CrpMode,
103 task: Option<&str>,
104) -> String {
105 handle_with_options(cache, path, mode, true, crp_mode, task)
106}
107
108pub fn handle_fresh_with_task_resolved(
110 cache: &mut SessionCache,
111 path: &str,
112 mode: &str,
113 crp_mode: CrpMode,
114 task: Option<&str>,
115) -> ReadOutput {
116 handle_with_options_resolved(
117 cache,
118 path,
119 mode,
120 true,
121 crp_mode,
122 task,
123 ReadTuning::resolve(None, &[]),
124 )
125}
126
127pub fn handle_fresh_with_task_resolved_tuned(
129 cache: &mut SessionCache,
130 path: &str,
131 mode: &str,
132 crp_mode: CrpMode,
133 task: Option<&str>,
134 aggressiveness: Option<f64>,
135 protect: &[String],
136) -> ReadOutput {
137 handle_with_options_resolved(
138 cache,
139 path,
140 mode,
141 true,
142 crp_mode,
143 task,
144 ReadTuning::resolve(aggressiveness, protect),
145 )
146}
147
148fn handle_with_options(
149 cache: &mut SessionCache,
150 path: &str,
151 mode: &str,
152 fresh: bool,
153 crp_mode: CrpMode,
154 task: Option<&str>,
155) -> String {
156 handle_with_options_resolved(
157 cache,
158 path,
159 mode,
160 fresh,
161 crp_mode,
162 task,
163 ReadTuning::resolve(None, &[]),
164 )
165 .content
166}
167
168pub(crate) fn force_fresh_env() -> bool {
171 static FORCE_FRESH: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
172 *FORCE_FRESH.get_or_init(|| {
173 std::env::var("LEAN_CTX_FORCE_FRESH").is_ok_and(|v| v == "1" || v == "true")
174 })
175}
176
177pub(crate) fn is_subagent_context() -> bool {
191 static IS_SUBAGENT: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
192 *IS_SUBAGENT.get_or_init(|| {
193 std::env::var("CURSOR_TASK_ID").is_ok_and(|v| !v.is_empty())
194 || std::env::var("CLAUDE_CODE_ENTRYPOINT")
195 .ok()
196 .as_deref()
197 .map(str::trim)
198 == Some("local-agent")
199 })
200}
201
202#[allow(clippy::fn_params_excessive_bools)]
205pub(crate) fn effective_fresh_flags(
206 fresh: bool,
207 force_fresh: bool,
208 subagent_context: bool,
209 delivery_for_subagents: bool,
210) -> (bool, bool) {
211 let effective_fresh_for_cache = fresh || force_fresh || subagent_context;
212 let effective_fresh_for_delivery =
213 fresh || force_fresh || (subagent_context && !delivery_for_subagents);
214 (effective_fresh_for_cache, effective_fresh_for_delivery)
215}
216
217pub(crate) fn effective_fresh_for_delivery(fresh: bool) -> bool {
218 let config = crate::core::config::Config::load();
219 effective_fresh_flags(
220 fresh,
221 force_fresh_env(),
222 is_subagent_context(),
223 config.ocla.delivery.delivery_for_subagents,
224 )
225 .1
226}
227
228fn handle_with_options_resolved(
229 cache: &mut SessionCache,
230 path: &str,
231 mode: &str,
232 fresh: bool,
233 crp_mode: CrpMode,
234 task: Option<&str>,
235 tuning: ReadTuning<'_>,
236) -> ReadOutput {
237 handle_with_options_resolved_preread(cache, path, mode, fresh, crp_mode, task, tuning, None)
238}
239
240fn handle_with_options_resolved_preread(
241 cache: &mut SessionCache,
242 path: &str,
243 mode: &str,
244 fresh: bool,
245 crp_mode: CrpMode,
246 task: Option<&str>,
247 tuning: ReadTuning<'_>,
248 preread: Option<String>,
249) -> ReadOutput {
250 let config = crate::core::config::Config::load();
253 let (effective_fresh_for_cache, effective_fresh_for_delivery) = effective_fresh_flags(
254 fresh,
255 force_fresh_env(),
256 is_subagent_context(),
257 config.ocla.delivery.delivery_for_subagents,
258 );
259
260 let compress_protected = mode != "raw"
261 && !mode.starts_with("lines:")
262 && crate::core::config::Config::load()
263 .proxy
264 .is_path_compress_protected(path);
265
266 let delivery_metadata = config
269 .ocla
270 .delivery_enabled()
271 .then(|| file_blake3_prefix(path))
272 .flatten();
273
274 if !effective_fresh_for_delivery
275 && !compress_protected
276 && let Some((hash, mtime)) = delivery_metadata
277 && let Some(stub) = try_cross_agent_stub(path, mode, hash, mtime)
278 {
279 cache.store(path, &stub.content);
280 cache.mark_full_delivered(path);
281 return stub;
282 }
283
284 if mode == "auto" {
285 let touched: Vec<String> = cache
286 .get_all_entries()
287 .iter()
288 .map(|(p, _)| (*p).clone())
289 .collect();
290 if crate::core::relevance_gate::should_gate(path, mode, task, &touched) {
291 let meta = std::fs::metadata(path);
292 let byte_count = meta.as_ref().map_or(0, std::fs::Metadata::len);
293 let line_count = preread
294 .as_ref()
295 .map_or(0, |c| bytecount::count(c.as_bytes(), b'\n'));
296 let stub = crate::core::relevance_gate::irrelevant_stub(path, line_count, byte_count);
297 let stub_tokens = count_tokens(&stub);
298 return ReadOutput {
299 content: stub,
300 resolved_mode: "auto".into(),
301 output_tokens: stub_tokens,
302 is_cache_hit: false,
303 };
304 }
305 }
306
307 if PluginManager::has_listener("pre_read") {
308 PluginManager::fire_hook_background(HookPoint::PreRead {
309 path: path.to_string(),
310 });
311 }
312
313 if let Ok(mut bt) = crate::core::bounce_tracker::global().lock() {
314 bt.next_seq();
315 }
316 let mut result = handle_with_options_inner(
317 cache,
318 path,
319 mode,
320 effective_fresh_for_cache,
321 crp_mode,
322 task,
323 tuning,
324 preread,
325 );
326
327 if let Some(entry) = cache.get_mut(path) {
328 entry.last_mode.clone_from(&result.resolved_mode);
329 if matches!(result.resolved_mode.as_str(), "full" | "full-compact")
330 && entry.full_content_delivered
331 && result.is_cache_hit
332 && entry.bump_reread() >= crate::core::cache::full_degradation_threshold()
333 {
334 entry.full_content_delivered = false;
335 entry.reset_reread_count();
336 crate::core::auto_mode_resolver::count_source("full_delivery_degraded");
337 }
338 if !matches!(result.resolved_mode.as_str(), "full" | "full-compact") {
344 entry.full_content_delivered = false;
345 entry.reset_reread_count();
346 }
347 }
348
349 if !result.is_cache_hit
350 && let Some((hash, mtime)) = delivery_metadata
351 {
352 let line_count = cache.get(path).map_or(0, |entry| entry.line_count as u32);
353 record_cross_agent_delivery(path, hash, mtime, line_count, result.output_tokens);
354 }
355
356 let dedup_allowed = result
358 .resolved_mode
359 .parse::<ReadMode>()
360 .is_ok_and(|m| m.is_lossy_summary());
361 if dedup_allowed && let Some(deduped) = cache.apply_dedup(path, &result.content) {
362 let new_tokens = count_tokens(&deduped);
363 if new_tokens < result.output_tokens {
364 result.content = deduped;
365 result.output_tokens = new_tokens;
366 }
367 }
368
369 if let Some(stub) = dedup_hook::maybe_dedup(path, &result.content, mode, fresh) {
371 let stub_tokens = count_tokens(&stub);
372 if stub_tokens < result.output_tokens {
373 result.content = stub;
374 result.output_tokens = stub_tokens;
375 result.is_cache_hit = true;
376 }
377 }
378
379 crate::core::context_kernel::adaptive_hook::update_from_bounce_tracker();
381 if let Ok(mut bt) = crate::core::bounce_tracker::global().lock() {
382 let original_tokens = cache.get(path).map_or(0, |e| e.original_tokens);
383 bt.record_read(
384 path,
385 &result.resolved_mode,
386 result.output_tokens,
387 original_tokens,
388 );
389
390 let compressed = result
400 .resolved_mode
401 .parse::<ReadMode>()
402 .map_or(true, |m| m.counts_as_compressed());
403 if compressed {
404 crate::core::adaptive_thresholds::record_quality_signal(
405 path,
406 crate::core::threshold_learning::QualitySignal::CleanCompressed,
407 );
408 } else if result.resolved_mode == "full"
409 && result.output_tokens > 2000
410 && bt.bounce_rate_for_extension(path).unwrap_or(0.0) < 0.05
411 {
412 crate::core::adaptive_thresholds::record_quality_signal(
413 path,
414 crate::core::threshold_learning::QualitySignal::WastedFull,
415 );
416 }
417 }
418
419 if PluginManager::has_listener("post_compress") {
421 let original_tokens = cache.get(path).map_or(0, |e| e.original_tokens);
422 PluginManager::fire_hook_background(HookPoint::PostCompress {
423 path: path.to_string(),
424 original_tokens,
425 compressed_tokens: result.output_tokens,
426 });
427 }
428
429 {
436 let self_agent = crate::core::scent_field::scent_agent_id();
437 let scent_path = crate::core::pathutil::normalize_tool_path(path);
438 std::thread::spawn(move || {
439 crate::core::scent_field::deposit(
440 self_agent,
441 crate::core::scent_field::ScentKind::Hot,
442 &scent_path,
443 0.3,
444 );
445 });
446 }
447
448 crate::core::context_gc::maybe_gc(cache);
449
450 result
451}
452
453pub fn try_stub_hit_readonly(cache: &SessionCache, path: &str) -> Option<ReadOutput> {
464 let current_conversation = crate::core::conversation::current_conversation_id_fresh();
468 try_stub_hit_readonly_scoped(cache, path, current_conversation.as_deref())
469}
470
471pub(crate) fn try_stub_hit_readonly_scoped(
475 cache: &SessionCache,
476 path: &str,
477 current_conversation: Option<&str>,
478) -> Option<ReadOutput> {
479 let no_deg = crate::core::config::Config::load().no_degrade_effective();
480 let prof = crate::core::profiles::active_profile();
481 let force_full = no_deg
482 || (prof.read.default_mode_effective() == "full"
483 && prof.compression.crp_mode_effective() == "off");
484 let policy_allows_stub =
485 crate::server::compaction_sync::effective_cache_policy() != "safe" && !force_full;
486 if !policy_allows_stub {
487 return None;
488 }
489
490 if let Some(file_ref) = cache.get_file_ref_readonly(path) {
492 let (cached_mtime, cached_hash, line_count, delivered_conv) = {
493 let entry = cache.get(path)?;
494 (
495 entry.stored_mtime,
496 entry.hash.clone(),
497 entry.line_count,
498 entry.delivered_conversation.clone(),
499 )
500 };
501 if crate::core::cache::is_cache_entry_stale_verified(path, cached_mtime, &cached_hash)
502 || !cache.is_full_delivered(path)
503 {
504 return None;
505 }
506 if !crate::core::conversation::conversation_allows_stub(
512 current_conversation,
513 delivered_conv.as_deref(),
514 ) {
515 crate::core::cache_telemetry::record_conversation_mismatch();
516 return None;
517 }
518 let original_tokens = cache.record_cache_hit(path)?.original_tokens;
519 crate::core::telemetry::global_metrics().record_cache(true);
520 let stub = render_unchanged_stub(&file_ref, path, line_count);
521 crate::core::stats::record_reread(original_tokens.saturating_sub(stub.output_tokens));
522 return Some(stub);
523 }
524
525 let rec = crate::core::read_stub_index::lookup(path)?;
531 if crate::core::cache::is_cache_entry_stale_verified(path, rec.stored_mtime(), &rec.hash) {
532 return None;
533 }
534 if !crate::core::conversation::conversation_allows_cold_stub(
535 current_conversation,
536 rec.delivered_conversation.as_deref(),
537 ) {
538 crate::core::cache_telemetry::record_conversation_mismatch();
539 return None;
540 }
541 Some(render_unchanged_stub(&rec.file_ref, path, rec.line_count))
542}
543
544fn render_unchanged_stub(file_ref: &str, path: &str, line_count: usize) -> ReadOutput {
552 let short = protocol::shorten_path(path);
553 let out = if crate::core::protocol::meta_visible() {
554 format!(
555 "{file_ref}={short} [unchanged {line_count}L]\nUnchanged on disk. Use fresh=true to force re-read.",
556 )
557 } else {
558 format!("{file_ref}={short} [unchanged {line_count}L · fresh=true to re-read]")
559 };
560 let out = crate::core::redaction::redact_text_if_enabled(&out);
561 let sent = count_tokens(&out);
562 ReadOutput {
563 content: out,
564 resolved_mode: "full".into(),
565 output_tokens: sent,
566 is_cache_hit: true,
567 }
568}
569
570#[derive(Debug, Clone, PartialEq, Eq)]
573pub struct DeltaExplicitDecision {
574 pub mode: String,
577 pub note: Option<String>,
581}
582
583pub fn resolve_explicit_delta_mode(
605 cache: &SessionCache,
606 path: &str,
607 mode: &str,
608 explicit_mode: bool,
609 fresh: bool,
610 enabled: bool,
611) -> DeltaExplicitDecision {
612 let unchanged = DeltaExplicitDecision {
613 mode: mode.to_string(),
614 note: None,
615 };
616 if fresh
617 || !enabled
618 || !explicit_mode
619 || !(mode == "full" || mode == "full-compact" || mode.starts_with("lines:"))
620 {
621 return unchanged;
622 }
623 let Some(entry) = cache.get(path) else {
624 return unchanged;
626 };
627 let stale =
628 crate::core::cache::is_cache_entry_stale_verified(path, entry.stored_mtime, &entry.hash);
629 if stale {
630 if entry.content().is_some() {
634 return DeltaExplicitDecision {
635 mode: "diff".to_string(),
636 note: Some(format!(
637 "[delta-explicit] requested mode={mode} served as a diff: the file \
638 changed since your last read and the diff is the new information. \
639 Pass fresh=true if you need the full content re-emitted."
640 )),
641 };
642 }
643 return unchanged;
644 }
645 if mode.starts_with("lines:") && cache.is_full_delivered(path) {
649 return DeltaExplicitDecision {
650 mode: "full".to_string(),
651 note: None,
652 };
653 }
654 unchanged
655}
656
657pub(crate) fn file_blake3_prefix(path: &str) -> Option<([u8; 12], u64)> {
658 let meta = std::fs::metadata(path).ok()?;
659 let mtime = meta
660 .modified()
661 .ok()?
662 .duration_since(std::time::UNIX_EPOCH)
663 .ok()?
664 .as_secs();
665 let bytes = std::fs::read(path).ok()?;
666 let hash = blake3::hash(&bytes);
667 let full = hash.as_bytes();
668 let mut prefix = [0u8; 12];
669 prefix.copy_from_slice(&full[..12]);
670 Some((prefix, mtime))
671}
672
673pub(crate) fn try_cross_agent_stub(
674 path: &str,
675 mode: &str,
676 hash: [u8; 12],
677 mtime: u64,
678) -> Option<ReadOutput> {
679 if !crate::core::config::Config::load().ocla.delivery_enabled() {
680 return None;
681 }
682 if matches!(mode, "full" | "raw" | "diff") {
683 return None;
684 }
685 let current_agent = std::env::var("CURSOR_TASK_ID")
686 .or_else(|_| std::env::var("CLAUDECODE"))
687 .unwrap_or_else(|_| "local-agent".to_string());
688 let current_conversation = crate::core::conversation::current_conversation_id()
689 .unwrap_or_else(|| current_agent.clone());
690 let reg = crate::core::ocla::OclaRegistry::global();
691 let record = crate::daemon_client::try_delivery_check_blocking(
692 &hash,
693 mtime,
694 path,
695 Some(¤t_agent),
696 Some(¤t_conversation),
697 )
698 .or_else(|| {
699 reg.delivery_registry.check_delivery(
700 &hash,
701 mtime,
702 path,
703 Some(¤t_agent),
704 Some(¤t_conversation),
705 )
706 })?;
707
708 let short = protocol::shorten_path(path);
709 let stub = format!(
710 "{short} [cross-agent · {lines}L · read by {agent} · use fresh=true to force]",
711 lines = record.line_count,
712 agent = record.agent_id,
713 );
714 let tokens = count_tokens(&stub);
715 reg.delivery_registry
716 .record_stub_served(&record, tokens as u64);
717 Some(ReadOutput {
718 content: stub,
719 resolved_mode: "cross-agent-stub".into(),
720 output_tokens: tokens,
721 is_cache_hit: true,
722 })
723}
724
725pub(crate) fn record_cross_agent_delivery(
726 path: &str,
727 hash: [u8; 12],
728 mtime: u64,
729 line_count: u32,
730 tokens: usize,
731) {
732 if !crate::core::config::Config::load().ocla.delivery_enabled() {
733 return;
734 }
735 let agent_id = std::env::var("CURSOR_TASK_ID")
736 .or_else(|_| std::env::var("CLAUDECODE"))
737 .unwrap_or_else(|_| "local-agent".to_string());
738 let conversation_id =
739 crate::core::conversation::current_conversation_id().unwrap_or_else(|| agent_id.clone());
740 let entry = crate::core::ocla::types::DeliveryEntry {
741 blake3: hash,
742 path: path.into(),
743 line_count,
744 token_count: tokens as u64,
745 agent_id,
746 conversation_id,
747 mtime,
748 };
749 crate::daemon_client::try_delivery_record_blocking(&entry);
750 let reg = crate::core::ocla::OclaRegistry::global();
751 reg.delivery_registry.record_delivery(entry);
752}
753
754#[cfg(test)]
755mod tests {
756 use super::{
757 SessionCache, effective_fresh_flags, try_cross_agent_stub, try_stub_hit_readonly_scoped,
758 };
759 use std::sync::atomic::Ordering;
760
761 #[test]
762 fn cross_agent_stub_miss_returns_none() {
763 let stub = try_cross_agent_stub("/nonexistent/file.rs", "auto", [0; 12], 0);
764 assert!(stub.is_none());
765 }
766
767 #[test]
768 fn subagent_delivery_policy_keeps_cache_fresh_but_allows_delivery_by_default() {
769 let delivery_for_subagents =
770 crate::core::config::DeliveryConfig::default().delivery_for_subagents;
771 assert!(
772 delivery_for_subagents,
773 "delivery must default to enabled for subagents"
774 );
775 let (cache_fresh, delivery_fresh) =
776 effective_fresh_flags(false, false, true, delivery_for_subagents);
777 assert!(cache_fresh, "subagent cache must remain isolated");
778 assert!(
779 !delivery_fresh,
780 "default policy must allow a cross-agent delivery lookup"
781 );
782 }
783
784 #[test]
785 fn subagent_delivery_policy_can_force_fresh_delivery() {
786 let (cache_fresh, delivery_fresh) = effective_fresh_flags(false, false, true, false);
787 assert!(cache_fresh);
788 assert!(delivery_fresh, "disabled policy must bypass delivery stubs");
789 }
790
791 #[test]
792 fn cross_agent_fallback_is_deterministic() {
793 let _lock = crate::core::data_dir::test_env_lock();
796 crate::test_env::remove_var("CURSOR_TASK_ID");
797 crate::test_env::remove_var("CLAUDECODE");
798 let id1 = std::env::var("CURSOR_TASK_ID")
799 .or_else(|_| std::env::var("CLAUDECODE"))
800 .unwrap_or_else(|_| "local-agent".to_string());
801 let id2 = std::env::var("CURSOR_TASK_ID")
802 .or_else(|_| std::env::var("CLAUDECODE"))
803 .unwrap_or_else(|_| "local-agent".to_string());
804 assert_eq!(id1, id2, "fallback agent ID must be deterministic");
805 assert!(!id1.contains("proc:"), "must not contain PID");
806 }
807
808 #[test]
809 fn warm_stub_hit_records_central_telemetry() {
810 let dir = tempfile::tempdir().unwrap();
811 let file = dir.path().join("telemetry-hit.rs");
812 std::fs::write(&file, "fn telemetry_hit() {}\n").unwrap();
813 let path = file.to_string_lossy();
814 let mut cache = SessionCache::new();
815 cache.store(&path, "fn telemetry_hit() {}\n");
816 cache.mark_full_delivered(&path);
817
818 let metrics = crate::core::telemetry::global_metrics();
819 let before = metrics.cache_hits.load(Ordering::Relaxed);
820 let output = try_stub_hit_readonly_scoped(&cache, &path, None);
821 let after = metrics.cache_hits.load(Ordering::Relaxed);
822
823 assert!(output.is_some(), "warm re-read must use the stub cache");
824 assert!(
825 after > before,
826 "stub cache hit must increment central telemetry"
827 );
828 }
829}