use std::path::Path;
use crate::core::cache::SessionCache;
use crate::core::compressor;
use crate::core::deps;
use crate::core::entropy;
use crate::core::plugins::{PluginManager, executor::HookPoint};
use crate::core::protocol;
use crate::core::signatures;
use crate::core::symbol_map::{self, SymbolMap};
use crate::core::tokens::count_tokens;
use crate::tools::CrpMode;
pub(crate) mod render;
pub(crate) use render::*;
pub(crate) mod mode;
pub(crate) use mode::ReadMode;
#[cfg(test)]
mod tests;
pub struct ReadOutput {
pub content: String,
pub resolved_mode: String,
pub output_tokens: usize,
}
fn is_cacheable_mode(mode: &str) -> bool {
mode.parse::<ReadMode>()
.is_ok_and(|m| m.is_compressed_cacheable())
}
fn mode_allows_raw_cap(mode: &str) -> bool {
mode.parse::<ReadMode>()
.map_or(true, |m| m.allows_raw_cap())
}
fn compressed_cache_key(
mode: &str,
crp_mode: CrpMode,
task: Option<&str>,
aggressiveness: Option<f64>,
protect: &[String],
) -> String {
let versioned_mode = match mode {
"map" => "map:v2",
"signatures" => "signatures:v2",
_ => mode,
};
let base = if crp_mode.is_tdd() {
format!("{versioned_mode}:tdd")
} else {
versioned_mode.to_string()
};
let keyed = match task.map(str::trim).filter(|t| !t.is_empty()) {
Some(t) => {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
t.hash(&mut h);
format!("{base}:t{:x}", h.finish())
}
None => base,
};
let mut key = keyed;
let aggr_frag = crate::core::aggressiveness::cache_fragment(aggressiveness);
if !aggr_frag.is_empty() {
key = format!("{key}:{aggr_frag}");
}
let protect_frag = crate::core::protect::protect_fragment(protect);
if !protect_frag.is_empty() {
key = format!("{key}:{protect_frag}");
}
key
}
fn append_compressed_hint(output: &str, file_path: &str) -> String {
match crate::core::recovery::read_footer(file_path) {
Some(footer) => format!("{output}\n{footer}"),
None => output.to_string(),
}
}
pub fn read_file_lossy(path: &str) -> Result<String, std::io::Error> {
if crate::core::binary_detect::is_binary_file(path) {
let msg = crate::core::binary_detect::binary_file_message(path);
return Err(std::io::Error::other(msg));
}
{
let canonical =
crate::core::pathutil::safe_canonicalize_bounded(std::path::Path::new(path), 2000);
if let Ok(cwd) = std::env::current_dir() {
let root = crate::core::pathutil::safe_canonicalize_bounded(&cwd, 2000);
if !canonical.starts_with(&root) {
let allow = crate::core::pathjail::allow_paths_from_env_and_config();
let data_dir_ok = crate::core::data_dir::lean_ctx_data_dir()
.is_ok_and(|d| canonical.starts_with(d));
let tmp_ok = canonical.starts_with(std::env::temp_dir());
if !allow.iter().any(|a| canonical.starts_with(a)) && !data_dir_ok && !tmp_ok {
tracing::warn!(
"defense-in-depth: path may escape project root: {}",
canonical.display()
);
}
}
}
}
let cap = crate::core::limits::max_read_bytes();
let file = open_with_retry(path)?;
let meta = file
.metadata()
.map_err(|e| std::io::Error::other(format!("cannot stat open file descriptor: {e}")))?;
if meta.len() > cap as u64 {
return Err(std::io::Error::other(format!(
"file too large ({} bytes, limit {} bytes via LCTX_MAX_READ_BYTES). \
Increase the limit or use a line-range read: mode=\"lines:1-100\"",
meta.len(),
cap
)));
}
use std::io::Read;
let mut bytes = Vec::with_capacity(meta.len() as usize);
std::io::BufReader::new(file).read_to_end(&mut bytes)?;
let s = match String::from_utf8(bytes) {
Ok(s) => s,
Err(e) => String::from_utf8_lossy(e.as_bytes()).into_owned(),
};
Ok(crate::core::io_boundary::strip_utf8_bom(s))
}
fn open_with_retry(path: &str) -> Result<std::fs::File, std::io::Error> {
match open_nofollow(path) {
Ok(f) => Ok(f),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
std::thread::sleep(std::time::Duration::from_millis(50));
open_nofollow(path).map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
std::io::Error::other(format!(
"file not found: {path} โ verify the path with ctx_tree or ctx_search"
))
} else {
e
}
})
}
Err(e) => Err(e),
}
}
#[cfg(unix)]
fn open_nofollow(path: &str) -> Result<std::fs::File, std::io::Error> {
use std::os::unix::fs::OpenOptionsExt;
use std::path::Path;
let p = Path::new(path);
if let (Some(parent), Some(filename)) = (p.parent(), p.file_name())
&& parent.exists()
{
let canonical_parent = crate::core::pathutil::safe_canonicalize_bounded(parent, 2000);
let canonical_path = canonical_parent.join(filename);
return std::fs::OpenOptions::new()
.read(true)
.custom_flags(libc::O_NOFOLLOW)
.open(&canonical_path);
}
std::fs::OpenOptions::new()
.read(true)
.custom_flags(libc::O_NOFOLLOW)
.open(path)
}
#[cfg(not(unix))]
fn open_nofollow(path: &str) -> Result<std::fs::File, std::io::Error> {
std::fs::File::open(path)
}
pub fn handle(cache: &mut SessionCache, path: &str, mode: &str, crp_mode: CrpMode) -> String {
handle_with_options(cache, path, mode, false, crp_mode, None)
}
pub fn handle_fresh(cache: &mut SessionCache, path: &str, mode: &str, crp_mode: CrpMode) -> String {
handle_with_options(cache, path, mode, true, crp_mode, None)
}
pub fn handle_with_task(
cache: &mut SessionCache,
path: &str,
mode: &str,
crp_mode: CrpMode,
task: Option<&str>,
) -> String {
handle_with_options(cache, path, mode, false, crp_mode, task)
}
pub fn handle_with_task_resolved(
cache: &mut SessionCache,
path: &str,
mode: &str,
crp_mode: CrpMode,
task: Option<&str>,
) -> ReadOutput {
handle_with_options_resolved(
cache,
path,
mode,
false,
crp_mode,
task,
ReadTuning::resolve(None, &[]),
)
}
pub fn handle_with_task_resolved_tuned(
cache: &mut SessionCache,
path: &str,
mode: &str,
crp_mode: CrpMode,
task: Option<&str>,
aggressiveness: Option<f64>,
protect: &[String],
) -> ReadOutput {
handle_with_options_resolved(
cache,
path,
mode,
false,
crp_mode,
task,
ReadTuning::resolve(aggressiveness, protect),
)
}
#[allow(clippy::too_many_arguments)]
pub fn handle_with_preread(
cache: &mut SessionCache,
path: &str,
mode: &str,
fresh: bool,
crp_mode: CrpMode,
task: Option<&str>,
aggressiveness: Option<f64>,
protect: &[String],
preread: String,
) -> ReadOutput {
handle_with_options_resolved_preread(
cache,
path,
mode,
fresh,
crp_mode,
task,
ReadTuning::resolve(aggressiveness, protect),
Some(preread),
)
}
pub fn handle_fresh_with_task(
cache: &mut SessionCache,
path: &str,
mode: &str,
crp_mode: CrpMode,
task: Option<&str>,
) -> String {
handle_with_options(cache, path, mode, true, crp_mode, task)
}
pub fn handle_fresh_with_task_resolved(
cache: &mut SessionCache,
path: &str,
mode: &str,
crp_mode: CrpMode,
task: Option<&str>,
) -> ReadOutput {
handle_with_options_resolved(
cache,
path,
mode,
true,
crp_mode,
task,
ReadTuning::resolve(None, &[]),
)
}
pub fn handle_fresh_with_task_resolved_tuned(
cache: &mut SessionCache,
path: &str,
mode: &str,
crp_mode: CrpMode,
task: Option<&str>,
aggressiveness: Option<f64>,
protect: &[String],
) -> ReadOutput {
handle_with_options_resolved(
cache,
path,
mode,
true,
crp_mode,
task,
ReadTuning::resolve(aggressiveness, protect),
)
}
fn handle_with_options(
cache: &mut SessionCache,
path: &str,
mode: &str,
fresh: bool,
crp_mode: CrpMode,
task: Option<&str>,
) -> String {
handle_with_options_resolved(
cache,
path,
mode,
fresh,
crp_mode,
task,
ReadTuning::resolve(None, &[]),
)
.content
}
fn force_fresh_env() -> bool {
static FORCE_FRESH: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*FORCE_FRESH.get_or_init(|| {
std::env::var("LEAN_CTX_FORCE_FRESH").is_ok_and(|v| v == "1" || v == "true")
})
}
fn is_subagent_context() -> bool {
static IS_SUBAGENT: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*IS_SUBAGENT.get_or_init(|| std::env::var("CURSOR_TASK_ID").is_ok_and(|v| !v.is_empty()))
}
fn handle_with_options_resolved(
cache: &mut SessionCache,
path: &str,
mode: &str,
fresh: bool,
crp_mode: CrpMode,
task: Option<&str>,
tuning: ReadTuning<'_>,
) -> ReadOutput {
handle_with_options_resolved_preread(cache, path, mode, fresh, crp_mode, task, tuning, None)
}
fn handle_with_options_resolved_preread(
cache: &mut SessionCache,
path: &str,
mode: &str,
fresh: bool,
crp_mode: CrpMode,
task: Option<&str>,
tuning: ReadTuning<'_>,
preread: Option<String>,
) -> ReadOutput {
let effective_fresh = fresh
|| force_fresh_env()
|| (is_subagent_context() && !crate::core::conversation::scope_enabled());
if PluginManager::has_listener("pre_read") {
PluginManager::fire_hook_background(HookPoint::PreRead {
path: path.to_string(),
});
}
if let Ok(mut bt) = crate::core::bounce_tracker::global().lock() {
bt.next_seq();
}
let mut result = handle_with_options_inner(
cache,
path,
mode,
effective_fresh,
crp_mode,
task,
tuning,
preread,
);
if let Some(entry) = cache.get_mut(path) {
entry.last_mode.clone_from(&result.resolved_mode);
}
let dedup_allowed = result
.resolved_mode
.parse::<ReadMode>()
.is_ok_and(|m| m.is_lossy_summary());
if dedup_allowed && let Some(deduped) = cache.apply_dedup(path, &result.content) {
let new_tokens = count_tokens(&deduped);
if new_tokens < result.output_tokens {
result.content = deduped;
result.output_tokens = new_tokens;
}
}
if let Ok(mut bt) = crate::core::bounce_tracker::global().lock() {
let original_tokens = cache.get(path).map_or(0, |e| e.original_tokens);
bt.record_read(
path,
&result.resolved_mode,
result.output_tokens,
original_tokens,
);
let compressed = result
.resolved_mode
.parse::<ReadMode>()
.map_or(true, |m| m.counts_as_compressed());
if compressed {
crate::core::adaptive_thresholds::record_quality_signal(
path,
crate::core::threshold_learning::QualitySignal::CleanCompressed,
);
} else if result.resolved_mode == "full"
&& result.output_tokens > 2000
&& bt.bounce_rate_for_extension(path).unwrap_or(0.0) < 0.05
{
crate::core::adaptive_thresholds::record_quality_signal(
path,
crate::core::threshold_learning::QualitySignal::WastedFull,
);
}
}
if PluginManager::has_listener("post_compress") {
let original_tokens = cache.get(path).map_or(0, |e| e.original_tokens);
PluginManager::fire_hook_background(HookPoint::PostCompress {
path: path.to_string(),
original_tokens,
compressed_tokens: result.output_tokens,
});
}
{
let self_agent = crate::core::scent_field::scent_agent_id();
let scent_path = crate::core::pathutil::normalize_tool_path(path);
std::thread::spawn(move || {
crate::core::scent_field::deposit(
self_agent,
crate::core::scent_field::ScentKind::Hot,
&scent_path,
0.3,
);
});
}
result
}
pub fn try_stub_hit_readonly(cache: &SessionCache, path: &str) -> Option<ReadOutput> {
let current_conversation = crate::core::conversation::current_conversation_id_fresh();
try_stub_hit_readonly_scoped(cache, path, current_conversation.as_deref())
}
fn try_stub_hit_readonly_scoped(
cache: &SessionCache,
path: &str,
current_conversation: Option<&str>,
) -> Option<ReadOutput> {
let no_deg = crate::core::config::Config::load().no_degrade_effective();
let prof = crate::core::profiles::active_profile();
let force_full = no_deg
|| (prof.read.default_mode_effective() == "full"
&& prof.compression.crp_mode_effective() == "off");
let policy_allows_stub =
crate::server::compaction_sync::effective_cache_policy() != "safe" && !force_full;
if !policy_allows_stub {
return None;
}
if let Some(file_ref) = cache.get_file_ref_readonly(path) {
let (cached_mtime, cached_hash, line_count, delivered_conv) = {
let entry = cache.get(path)?;
(
entry.stored_mtime,
entry.hash.clone(),
entry.line_count,
entry.delivered_conversation.clone(),
)
};
if crate::core::cache::is_cache_entry_stale_verified(path, cached_mtime, &cached_hash)
|| !cache.is_full_delivered(path)
{
return None;
}
if !crate::core::conversation::conversation_allows_stub(
current_conversation,
delivered_conv.as_deref(),
) {
crate::core::cache_telemetry::record_conversation_mismatch();
return None;
}
cache.record_cache_hit(path);
return Some(render_unchanged_stub(&file_ref, path, line_count));
}
let rec = crate::core::read_stub_index::lookup(path)?;
if crate::core::cache::is_cache_entry_stale_verified(path, rec.stored_mtime(), &rec.hash) {
return None;
}
if !crate::core::conversation::conversation_allows_cold_stub(
current_conversation,
rec.delivered_conversation.as_deref(),
) {
crate::core::cache_telemetry::record_conversation_mismatch();
return None;
}
Some(render_unchanged_stub(&rec.file_ref, path, rec.line_count))
}
fn render_unchanged_stub(file_ref: &str, path: &str, line_count: usize) -> ReadOutput {
let short = protocol::shorten_path(path);
let out = if crate::core::protocol::meta_visible() {
format!(
"{file_ref}={short} [unchanged {line_count}L]\nUnchanged on disk. Use fresh=true to force re-read.",
)
} else {
format!("{file_ref}={short} [unchanged {line_count}L ยท fresh=true to re-read]")
};
let out = crate::core::redaction::redact_text_if_enabled(&out);
let sent = count_tokens(&out);
ReadOutput {
content: out,
resolved_mode: "full".into(),
output_tokens: sent,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeltaExplicitDecision {
pub mode: String,
pub note: Option<String>,
}
pub fn resolve_explicit_delta_mode(
cache: &SessionCache,
path: &str,
mode: &str,
explicit_mode: bool,
fresh: bool,
enabled: bool,
) -> DeltaExplicitDecision {
let unchanged = DeltaExplicitDecision {
mode: mode.to_string(),
note: None,
};
if fresh
|| !enabled
|| !explicit_mode
|| !(mode == "full" || mode == "full-compact" || mode.starts_with("lines:"))
{
return unchanged;
}
let Some(entry) = cache.get(path) else {
return unchanged;
};
let stale =
crate::core::cache::is_cache_entry_stale_verified(path, entry.stored_mtime, &entry.hash);
if stale {
if entry.content().is_some() {
return DeltaExplicitDecision {
mode: "diff".to_string(),
note: Some(format!(
"[delta-explicit] requested mode={mode} served as a diff: the file \
changed since your last read and the diff is the new information. \
Pass fresh=true if you need the full content re-emitted."
)),
};
}
return unchanged;
}
if mode.starts_with("lines:") && cache.is_full_delivered(path) {
return DeltaExplicitDecision {
mode: "full".to_string(),
note: None,
};
}
unchanged
}
fn handle_with_options_inner(
cache: &mut SessionCache,
path: &str,
mode: &str,
fresh: bool,
crp_mode: CrpMode,
task: Option<&str>,
tuning: ReadTuning<'_>,
preread: Option<String>,
) -> ReadOutput {
let file_ref = cache.get_file_ref(path);
let short = protocol::shorten_path(path);
let ext = Path::new(path)
.extension()
.and_then(|e| e.to_str())
.unwrap_or("");
let mode = if mode != "raw"
&& !mode.starts_with("lines:")
&& crate::core::config::Config::load()
.proxy
.is_path_compress_protected(path)
{
"full"
} else {
mode
};
if fresh {
if mode == "diff" {
let warning = "[warning] fresh+diff is redundant โ fresh invalidates cache, no diff possible. Use mode=full with fresh=true instead.";
return ReadOutput {
content: warning.to_string(),
resolved_mode: "diff".into(),
output_tokens: count_tokens(warning),
};
}
cache.invalidate(path);
}
if mode == "diff" {
let (out, _) = handle_diff(cache, path, &file_ref);
let out = crate::core::redaction::redact_text_if_enabled(&out);
let sent = count_tokens(&out);
return ReadOutput {
content: out,
resolved_mode: "diff".into(),
output_tokens: sent,
};
}
if mode != "full"
&& let Some(existing) = cache.get(path)
{
let stale = crate::core::cache::is_cache_entry_stale_verified(
path,
existing.stored_mtime,
&existing.hash,
);
if stale {
cache.invalidate(path);
}
}
let cache_snapshot = cache
.get(path)
.map(|existing| (existing.original_tokens, existing.content()));
if let Some((original_tokens, content_opt)) = cache_snapshot {
let resolved_mode = if mode == "auto" {
tuning
.auto_density_mode()
.unwrap_or_else(|| resolve_auto_mode(Some(cache), path, original_tokens, task))
} else {
mode.to_string()
};
if resolved_mode == "full" || resolved_mode == "full-compact" {
if let Some(out) = try_stub_hit_readonly(cache, path) {
return out;
}
if resolved_mode == "full-compact" {
let content = match read_file_lossy(path) {
Ok(c) => c,
Err(e) => {
let msg = format!("ERROR: {e}");
return ReadOutput {
content: msg,
resolved_mode: "error".into(),
output_tokens: 0,
};
}
};
let (out, _) = format_full_compact_output(&content);
let out = crate::core::redaction::redact_text_if_enabled(&out);
let sent = count_tokens(&out);
return ReadOutput {
content: out,
resolved_mode: "full-compact".into(),
output_tokens: sent,
};
}
let (out, _) = handle_full_with_auto_delta(cache, path, &file_ref, &short, ext, task);
let out = crate::core::redaction::redact_text_if_enabled(&out);
let sent = count_tokens(&out);
return ReadOutput {
content: out,
resolved_mode: "full".into(),
output_tokens: sent,
};
}
if is_cacheable_mode(&resolved_mode) {
let cache_key = compressed_cache_key(
&resolved_mode,
crp_mode,
task,
tuning.aggressiveness,
tuning.protect,
);
let compressed_hit = cache.get_compressed(path, &cache_key).cloned();
if let Some(cached_output) = compressed_hit {
let out = crate::core::redaction::redact_text_if_enabled(&cached_output);
let sent = count_tokens(&out);
return ReadOutput {
content: out,
resolved_mode,
output_tokens: sent,
};
}
}
if let Some(content) = content_opt {
let (out, _) = process_mode_tuned(
&content,
&resolved_mode,
&file_ref,
&short,
ext,
original_tokens,
crp_mode,
path,
task,
tuning,
);
let out = if mode_allows_raw_cap(&resolved_mode) {
let framed_tokens = count_tokens(&out);
cap_to_raw(out, framed_tokens, &content, original_tokens)
} else {
out
};
if is_cacheable_mode(&resolved_mode) {
let cache_key = compressed_cache_key(
&resolved_mode,
crp_mode,
task,
tuning.aggressiveness,
tuning.protect,
);
cache.set_compressed(path, &cache_key, out.clone());
}
let out = crate::core::redaction::redact_text_if_enabled(&out);
let sent = count_tokens(&out);
return ReadOutput {
content: out,
resolved_mode,
output_tokens: sent,
};
}
cache.invalidate(path);
}
let content = if let Some(pr) = preread {
pr
} else {
match read_file_lossy(path) {
Ok(c) => c,
Err(e) => {
let msg = format!("ERROR: {e}");
let tokens = count_tokens(&msg);
return ReadOutput {
content: msg,
resolved_mode: "error".into(),
output_tokens: tokens,
};
}
}
};
let store_result = cache.store(path, &content);
let is_line_range = mode.starts_with("lines:");
let hints = crate::core::profiles::active_profile().output_hints;
let is_repeat_read = store_result.read_count > 1;
let similar_hint = if !is_line_range && is_repeat_read && hints.semantic_hint() {
find_similar_and_update_semantic_index(path, &content)
} else {
None
};
let graph_hint: Option<String> = None;
if mode == "full" || mode == "full-compact" {
cache.mark_full_delivered(path);
if mode == "full-compact" {
let (output, _) = format_full_compact_output(&content);
let output = crate::core::redaction::redact_text_if_enabled(&output);
let sent = count_tokens(&output);
return ReadOutput {
content: output,
resolved_mode: "full-compact".into(),
output_tokens: sent,
};
}
let (mut output, _) = format_full_output(
&file_ref,
&short,
ext,
&content,
store_result.original_tokens,
store_result.line_count,
task,
);
if let Some(hint) = &graph_hint {
output.push_str(&format!("\n{hint}"));
}
if let Some(hint) = similar_hint {
output.push_str(&format!("\n{hint}"));
}
let framed_tokens = count_tokens(&output);
let output = cap_to_raw(
output,
framed_tokens,
&content,
store_result.original_tokens,
);
let output = crate::core::redaction::redact_text_if_enabled(&output);
let sent = count_tokens(&output);
return ReadOutput {
content: output,
resolved_mode: "full".into(),
output_tokens: sent,
};
}
let resolved_mode = if mode == "auto" {
tuning
.auto_density_mode()
.unwrap_or_else(|| resolve_auto_mode(None, path, store_result.original_tokens, task))
} else {
mode.to_string()
};
let (output, _sent) = process_mode_tuned(
&content,
&resolved_mode,
&file_ref,
&short,
ext,
store_result.original_tokens,
crp_mode,
path,
task,
tuning,
);
let mut output = if mode_allows_raw_cap(&resolved_mode) {
let framed_tokens = count_tokens(&output);
cap_to_raw(
output,
framed_tokens,
&content,
store_result.original_tokens,
)
} else {
output
};
if is_cacheable_mode(&resolved_mode) {
let cache_key = compressed_cache_key(
&resolved_mode,
crp_mode,
task,
tuning.aggressiveness,
tuning.protect,
);
cache.set_compressed(path, &cache_key, output.clone());
}
if let Some(hint) = &graph_hint {
output.push_str(&format!("\n{hint}"));
}
if let Some(hint) = similar_hint {
output.push_str(&format!("\n{hint}"));
}
let output = crate::core::redaction::redact_text_if_enabled(&output);
let final_tokens = count_tokens(&output);
ReadOutput {
content: output,
resolved_mode,
output_tokens: final_tokens,
}
}
pub fn is_instruction_file(path: &str) -> bool {
let lower = path.to_lowercase();
let filename = std::path::Path::new(&lower)
.file_name()
.and_then(|f| f.to_str())
.unwrap_or("");
matches!(
filename,
"skill.md"
| "agents.md"
| "rules.md"
| ".cursorrules"
| ".clinerules"
| "lean-ctx.md"
| "lean-ctx.mdc"
) || lower.contains("/skills/")
|| lower.contains("/.cursor/rules/")
|| lower.contains("/.claude/rules/")
|| lower.contains("/agents.md")
}
fn cap_to_raw(
framed: String,
framed_tokens: usize,
raw_content: &str,
raw_tokens: usize,
) -> String {
if raw_tokens > 0 && framed_tokens > raw_tokens {
let prevented = (framed_tokens - raw_tokens) as u64;
crate::core::cache_telemetry::record_raw_cap(prevented);
raw_content.to_string()
} else {
framed
}
}
fn resolve_auto_mode(
cache: Option<&SessionCache>,
file_path: &str,
original_tokens: usize,
task: Option<&str>,
) -> String {
let ctx = crate::core::auto_mode_resolver::AutoModeContext {
path: file_path,
token_count: original_tokens,
task,
cache,
};
crate::core::auto_mode_resolver::resolve(&ctx).mode
}
fn find_similar_and_update_semantic_index(path: &str, content: &str) -> Option<String> {
const MAX_CONTENT_BYTES_FOR_SEMANTIC: usize = 32_768;
if content.len() > MAX_CONTENT_BYTES_FOR_SEMANTIC {
return None;
}
let cfg = crate::core::config::Config::load();
let profile = crate::core::config::MemoryProfile::effective(&cfg);
if !profile.semantic_cache_enabled() {
return None;
}
let project_root = detect_project_root(path);
let session_id = format!("{}", std::process::id());
let mut index = crate::core::semantic_cache::SemanticCacheIndex::load_or_create(&project_root);
let similar = index.find_similar(content, 0.7);
let relevant: Vec<_> = similar
.into_iter()
.filter(|(p, _)| p != path)
.take(3)
.collect();
index.add_file(path, content, &session_id);
if let Err(e) = index.save(&project_root) {
tracing::warn!("lean-ctx: failed to persist semantic index: {e}");
}
if relevant.is_empty() {
return None;
}
let hints: Vec<String> = relevant
.iter()
.map(|(p, score)| format!(" {p} ({:.0}% similar)", score * 100.0))
.collect();
Some(format!(
"[semantic: {} similar file(s) in cache]\n{}",
relevant.len(),
hints.join("\n")
))
}
fn detect_project_root(path: &str) -> String {
crate::core::protocol::detect_project_root_or_cwd(path)
}
pub fn graph_related_hint(path: &str) -> Option<String> {
let project_root = detect_project_root(path);
crate::core::graph_context::build_related_hint(path, &project_root, 5)
}
const AUTO_DELTA_THRESHOLD: f64 = 0.6;
fn handle_full_with_auto_delta(
cache: &mut SessionCache,
path: &str,
file_ref: &str,
short: &str,
ext: &str,
task: Option<&str>,
) -> (String, usize) {
let _mode_guard = crate::core::savings_footer::ModeGuard::new("full");
let Ok(disk_content) = read_file_lossy(path) else {
cache.record_cache_hit(path);
if let Some(existing) = cache.get(path) {
if !crate::core::protocol::meta_visible()
&& let Some(cached) = existing.content()
{
return format_full_output(
file_ref,
short,
ext,
&cached,
existing.original_tokens,
existing.line_count,
task,
);
}
let out = format!(
"[using cached version โ file read failed]\n{file_ref}={short} cached {}t {}L",
existing.read_count(),
existing.line_count
);
let sent = count_tokens(&out);
return (out, sent);
}
let out = if crate::core::protocol::meta_visible() && !file_ref.is_empty() {
format!("[file read failed and no cached version available] {file_ref}={short}")
} else {
format!("[file read failed and no cached version available] {short}")
};
let sent = count_tokens(&out);
return (out, sent);
};
let no_deg = crate::core::config::Config::load().no_degrade_effective();
let prof = crate::core::profiles::active_profile();
let force_full = no_deg
|| (prof.read.default_mode_effective() == "full"
&& prof.compression.crp_mode_effective() == "off");
let old_content = cache
.get(path)
.and_then(crate::core::cache::CacheEntry::content)
.unwrap_or_default();
let store_result = cache.store(path, &disk_content);
if store_result.was_hit {
let policy_allows_stub =
crate::server::compaction_sync::effective_cache_policy() != "safe" && !force_full;
if policy_allows_stub && store_result.full_content_delivered {
let out = if crate::core::protocol::meta_visible() {
format!(
"{file_ref}={short} [unchanged {}L]\nUnchanged on disk. Use fresh=true to force re-read.",
store_result.line_count
)
} else {
format!(
"{file_ref}={short} [unchanged {}L ยท fresh=true to re-read]",
store_result.line_count
)
};
let sent = count_tokens(&out);
return (out, sent);
}
cache.mark_full_delivered(path);
return format_full_output(
file_ref,
short,
ext,
&disk_content,
store_result.original_tokens,
store_result.line_count,
task,
);
}
let diff = compressor::diff_content(&old_content, &disk_content);
let diff_tokens = count_tokens(&diff);
let full_tokens = store_result.original_tokens;
if !force_full
&& full_tokens > 0
&& (diff_tokens as f64) < (full_tokens as f64 * AUTO_DELTA_THRESHOLD)
{
let savings = protocol::format_savings(full_tokens, diff_tokens);
let head = if crate::core::protocol::meta_visible() && !file_ref.is_empty() {
format!("{file_ref}={short}")
} else {
short.to_string()
};
let out = format!(
"{head} [auto-delta] โ{}L\n{diff}\n{savings}",
disk_content.lines().count()
);
return (out, diff_tokens);
}
format_full_output(
file_ref,
short,
ext,
&disk_content,
store_result.original_tokens,
store_result.line_count,
task,
)
}
fn handle_diff(cache: &mut SessionCache, path: &str, file_ref: &str) -> (String, usize) {
let _mode_guard = crate::core::savings_footer::ModeGuard::new("diff");
let short = protocol::shorten_path(path);
let old_content = cache
.get(path)
.and_then(crate::core::cache::CacheEntry::content);
let new_content = match read_file_lossy(path) {
Ok(c) => c,
Err(e) => {
let msg = format!("ERROR: {e}");
let tokens = count_tokens(&msg);
return (msg, tokens);
}
};
let original_tokens = count_tokens(&new_content);
let diff_output = if let Some(old) = &old_content {
compressor::diff_content(old, &new_content)
} else {
cache.store(path, &new_content);
let msg = format!(
"{file_ref}={short} [no cached version for diff โ use mode=full first, then diff on re-read]"
);
let sent = count_tokens(&msg);
return (msg, sent);
};
cache.store(path, &new_content);
let sent = count_tokens(&diff_output);
let savings = protocol::format_savings(original_tokens, sent);
let head = if crate::core::protocol::meta_visible() && !file_ref.is_empty() {
format!("{file_ref}={short}")
} else {
short
};
(format!("{head} [diff]\n{diff_output}\n{savings}"), sent)
}