Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/reference/generated/config-keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Top-level configuration keys
- `content_defined_chunking` (bool, default `false`) β€” Enable Rabin-Karp chunking for cache-optimal output ordering
- `custom_aliases` (array, default `[]`) β€” Custom command aliases (array of {command, alias} entries)
- `default_tool_categories` (string[], default `[]`) β€” Tool categories active by default (core, arch, debug, memory, metrics, session). Override via LCTX_DEFAULT_CATEGORIES
- `delta_explicit` (boolean, default `false`) β€” Serve explicit full/lines re-reads of changed cached files as diffs (opt-in). Override via LCTX_DELTA_EXPLICIT=1
- `disabled_tools` (string[], default `[]`) β€” Tools to exclude from the MCP tool list
- `enable_wakeup_ctx` (bool, default `true`) β€” Append wakeup briefing (facts, session summary) to ctx_overview output. Set false to reduce context bloat when calling ctx_overview frequently.
- `excluded_commands` (string[], default `[]`) β€” Commands to exclude from shell hook interception
Expand Down
26 changes: 26 additions & 0 deletions rust/src/core/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,13 @@ pub struct Config {
/// Override via LCTX_NO_DEGRADE=1 env var.
#[serde(default)]
pub no_degrade: bool,
/// Serve explicit `full`/`lines:N-M` re-reads of session-cached files as
/// deltas: when the file changed on disk since it was cached, the read
/// returns `mode=diff` instead of re-emitting content the model already
/// holds. First reads are unaffected; `fresh=true` always bypasses.
/// Opt-in. Override via LCTX_DELTA_EXPLICIT=1/0 env var.
#[serde(default)]
pub delta_explicit: bool,
/// Persistent profile name. Checked after LEAN_CTX_PROFILE env var.
/// Set via `lean-ctx config set profile passthrough` or editing config.toml.
#[serde(default)]
Expand Down Expand Up @@ -491,6 +498,7 @@ impl Default for Config {
prefer_native_editor: false,
default_tool_categories: Vec::new(),
no_degrade: false,
delta_explicit: false,
profile: None,
tool_profile: None,
tools_enabled: Vec::new(),
Expand Down Expand Up @@ -834,6 +842,21 @@ impl Config {
self.no_degrade
}

/// Returns `true` if explicit `full`/`lines:N-M` re-reads of
/// cached-but-changed files should be served as deltas (`mode=diff`)
/// instead of re-emitting full content.
///
/// Checks the `LCTX_DELTA_EXPLICIT` env var first, then the config.toml
/// field. Unlike a presence-only knob, an explicit `0`/`false` in the env
/// forces the feature OFF even when the config field is `true`, so the env
/// can fully override config in both directions.
pub fn delta_explicit_effective(&self) -> bool {
if let Ok(val) = std::env::var("LCTX_DELTA_EXPLICIT") {
return val == "1" || val.eq_ignore_ascii_case("true");
}
self.delta_explicit
}

/// Effective max_disk_mb from env or config.
pub fn max_disk_mb_effective(&self) -> u64 {
std::env::var("LEAN_CTX_MAX_DISK_MB")
Expand Down Expand Up @@ -1324,6 +1347,9 @@ impl Config {
if local.no_degrade {
self.no_degrade = true;
}
if local.delta_explicit {
self.delta_explicit = true;
}
if local.profile.is_some() {
self.profile = local.profile;
}
Expand Down
8 changes: 8 additions & 0 deletions rust/src/core/config/schema/sections_core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,14 @@ pub(super) fn build(sections: &mut BTreeMap<String, SectionSchema>) {
"Disable all automatic read-mode degradation. Override via LCTX_NO_DEGRADE=1",
),
);
root.insert(
"delta_explicit".into(),
key(
"boolean",
serde_json::json!(cfg.delta_explicit),
"Serve explicit full/lines re-reads of changed cached files as diffs (opt-in). Override via LCTX_DELTA_EXPLICIT=1",
),
);
root.insert(
"profile".into(),
key(
Expand Down
144 changes: 144 additions & 0 deletions rust/src/core/config/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,150 @@ mod no_degrade_tests {
}
}

#[cfg(test)]
mod delta_explicit_tests {
use super::super::*;

// --- Defaults ---

#[test]
fn default_is_false() {
let cfg = Config::default();
assert!(!cfg.delta_explicit);
}

#[test]
fn effective_false_when_unset() {
if std::env::var("LCTX_DELTA_EXPLICIT").is_ok() {
return;
}
let cfg = Config::default();
assert!(!cfg.delta_explicit_effective());
}

// --- Config field ---

#[test]
fn config_field_true_respected_when_no_env() {
if std::env::var("LCTX_DELTA_EXPLICIT").is_ok() {
return;
}
let cfg = Config {
delta_explicit: true,
..Default::default()
};
assert!(cfg.delta_explicit_effective());
}

#[test]
fn config_field_false_respected_when_no_env() {
if std::env::var("LCTX_DELTA_EXPLICIT").is_ok() {
return;
}
let cfg = Config {
delta_explicit: false,
..Default::default()
};
assert!(!cfg.delta_explicit_effective());
}

// --- Env override (both directions) ---

#[test]
fn env_overrides_config_field_in_both_directions() {
// All env mutation serializes through this lock (Rust 2024 set_var is
// `unsafe`; the lock is the documented soundness precondition).
let _lock = crate::core::data_dir::test_env_lock();

// env=1 turns the feature ON even when the config field is false.
crate::test_env::set_var("LCTX_DELTA_EXPLICIT", "1");
let off_cfg = Config {
delta_explicit: false,
..Default::default()
};
assert!(
off_cfg.delta_explicit_effective(),
"LCTX_DELTA_EXPLICIT=1 must enable the feature over a false config field"
);

// env=0 forces it OFF even when the config field is true.
crate::test_env::set_var("LCTX_DELTA_EXPLICIT", "0");
let on_cfg = Config {
delta_explicit: true,
..Default::default()
};
assert!(
!on_cfg.delta_explicit_effective(),
"LCTX_DELTA_EXPLICIT=0 must disable the feature over a true config field"
);

// `true`/`false` spellings are honoured too (case-insensitive).
crate::test_env::set_var("LCTX_DELTA_EXPLICIT", "true");
assert!(off_cfg.delta_explicit_effective());
crate::test_env::set_var("LCTX_DELTA_EXPLICIT", "FALSE");
assert!(!on_cfg.delta_explicit_effective());

// Restore: with the var removed the config field decides again.
crate::test_env::remove_var("LCTX_DELTA_EXPLICIT");
assert!(on_cfg.delta_explicit_effective());
assert!(!off_cfg.delta_explicit_effective());
}

// --- TOML deserialization ---

#[test]
fn deserialization_true() {
let cfg: Config = toml::from_str("delta_explicit = true").unwrap();
assert!(cfg.delta_explicit);
}

#[test]
fn deserialization_false() {
let cfg: Config = toml::from_str("delta_explicit = false").unwrap();
assert!(!cfg.delta_explicit);
}

#[test]
fn deserialization_absent_defaults_false() {
let cfg: Config = toml::from_str("").unwrap();
assert!(!cfg.delta_explicit);
}

// --- Round-trip (serialize β†’ deserialize preserves the field) ---

#[test]
fn round_trip_preserves_field() {
let cfg = Config {
delta_explicit: true,
..Default::default()
};
let serialized = toml::to_string(&cfg).expect("Config must serialize to TOML");
let restored: Config =
toml::from_str(&serialized).expect("serialized Config must round-trip");
assert!(
restored.delta_explicit,
"delta_explicit must survive a TOML serialize β†’ deserialize round-trip"
);
}

// --- Coexistence with other config fields ---

#[test]
fn delta_explicit_independent_of_no_degrade() {
if std::env::var("LCTX_DELTA_EXPLICIT").is_ok() || std::env::var("LCTX_NO_DEGRADE").is_ok()
{
return;
}
let cfg = Config {
delta_explicit: true,
no_degrade: true,
..Default::default()
};
assert!(cfg.delta_explicit_effective());
assert!(cfg.no_degrade_effective());
}
}

#[cfg(test)]
mod rules_scope_tests {
use super::super::*;
Expand Down
83 changes: 83 additions & 0 deletions rust/src/tools/ctx_read/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,89 @@ pub fn try_stub_hit_readonly(cache: &SessionCache, path: &str) -> Option<ReadOut
})
}

/// Outcome of [`resolve_explicit_delta_mode`]: the (possibly rewritten) read
/// mode plus an optional advisory note to surface to the agent.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeltaExplicitDecision {
/// The mode the read should proceed with (rewritten only when the feature
/// fires; otherwise the caller's mode, unchanged).
pub mode: String,
/// A byte-stable advisory appended to the read body when the mode was
/// rewritten to `diff`. `None` when nothing was rewritten or the collapse
/// was a silent `lines:`β†’`full` stub.
pub note: Option<String>,
}

/// Decide whether an **explicit** `full`/`lines:N-M` re-read of a session-cached
/// file should be served as a delta instead of re-emitting content the model
/// already holds (the `delta_explicit` opt-in; env `LCTX_DELTA_EXPLICIT`).
///
/// Returns the mode the read should proceed with:
/// - **Changed on disk** (verified mtime+md5 stale) and full content is cached β†’
/// `diff`, plus an advisory note. The diff carries exactly the new
/// information in a fraction of the tokens.
/// - **Unchanged** and the request is `lines:` of an already-fully-delivered
/// file β†’ `full`, so the read collapses to the ~15-token `[unchanged]` stub
/// instead of re-extracting a window the model has seen.
/// - Otherwise the caller's `mode` is returned untouched.
///
/// First reads (nothing cached) and `fresh=true` are never affected β€” the
/// caller gates those before calling. Staleness uses the **verified** variant
/// ([`crate::core::cache::is_cache_entry_stale_verified`]) so a same-second
/// write on a coarse-granularity filesystem cannot be mistaken for "unchanged"
/// and yield a misleading empty diff (#498 determinism).
///
/// Pure w.r.t. (cache, path, mode, enabled): no wall-clock, counters, or
/// randomness enter the result, so identical inputs stay byte-stable.
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.starts_with("lines:")) {
return unchanged;
}
let Some(entry) = cache.get(path) else {
// First read this session β€” nothing to diff against.
return unchanged;
};
let stale =
crate::core::cache::is_cache_entry_stale_verified(path, entry.stored_mtime, &entry.hash);
if stale {
// Only divert to a diff when full content is actually cached: the diff
// base is that full content (see `handle_diff`), never a compressed
// view. Without it, `handle_diff` would have nothing to compare.
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;
}
// Unchanged on disk: a `lines:` window of a file already delivered in full
// re-emits text the model holds β€” collapse to the full-mode stub
// (~15 tokens). A plain `full` re-read already hits that stub downstream.
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,
Expand Down
Loading
Loading