Skip to main content

lean_ctx_server/tools/registered/
ctx_read.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use std::sync::atomic::{AtomicBool, Ordering};
4use std::sync::{Arc, Mutex};
5
6use rmcp::ErrorData;
7use rmcp::model::Tool;
8use serde_json::{Map, Value};
9
10use crate::server::tool_trait::{
11    McpTool, ToolContext, ToolOutput, get_bool, get_f64, get_int, get_str, get_str_array,
12    require_resolved_path,
13};
14use lean_ctx::core::cache::ReuseOutcome;
15
16/// Per-file lock that serializes concurrent reads of the same path.
17///
18/// When multiple subagents read sequentially through a shared set of files,
19/// they tend to hit the same path at the same time. Without per-file locking
20/// they all contend on the global cache write lock while doing redundant I/O.
21/// This lock ensures only one thread reads a given file from disk; the others
22/// wait cheaply on the per-file mutex, then hit the warm cache.
23///
24/// Backed by the shared `core::path_locks` registry so reads and edits of the
25/// same path coordinate through a single mutex (see issue #320).
26fn per_file_lock(path: &str) -> Arc<Mutex<()>> {
27    lean_ctx::core::path_locks::per_file_lock(path)
28}
29
30pub struct CtxReadTool;
31
32#[path = "ctx_read_delivery.rs"]
33mod delivery;
34use delivery::{
35    CacheSourceIdentity, PendingCacheDelivery, PendingCrossAgentDelivery, PendingReadDelivery,
36};
37
38pub(super) struct PreparedRead {
39    pub(super) output: ToolOutput,
40    delivery: Option<PendingReadDelivery>,
41}
42
43impl PreparedRead {
44    pub(super) fn commit(self, ctx: &ToolContext, record_agent_budget: bool) -> ToolOutput {
45        if let Some(delivery) = self.delivery {
46            delivery.commit(ctx, &self.output, record_agent_budget);
47        }
48        self.output
49    }
50}
51
52#[path = "ctx_read_dispatch.rs"]
53pub mod dispatch;
54
55impl CtxReadTool {
56    #[allow(clippy::unused_self)]
57    pub(super) fn handle_inner(
58        &self,
59        args: &Map<String, Value>,
60        ctx: &ToolContext,
61        path: &str,
62        engine_interface_v1: bool,
63    ) -> Result<ToolOutput, ErrorData> {
64        self.handle_inner_prepared(args, ctx, path, engine_interface_v1)
65            .map(|prepared| prepared.commit(ctx, true))
66    }
67
68    #[allow(clippy::unused_self)]
69    pub(super) fn handle_inner_prepared(
70        &self,
71        args: &Map<String, Value>,
72        ctx: &ToolContext,
73        path: &str,
74        engine_interface_v1: bool,
75    ) -> Result<PreparedRead, ErrorData> {
76        let session_lock = ctx
77            .session
78            .as_ref()
79            .ok_or_else(|| ErrorData::internal_error("session not available", None))?;
80        let cache_lock = ctx
81            .cache
82            .as_ref()
83            .ok_or_else(|| ErrorData::internal_error("cache not available", None))?;
84
85        let current_task = {
86            let deadline = std::time::Instant::now() + std::time::Duration::from_secs(8);
87            let mut attempt = 0u32;
88            loop {
89                // #1018: use try_read_owned instead of Handle::block_on to avoid
90                // the async-runtime-saturation anti-pattern on Windows.
91                if let Ok(guard) = session_lock.clone().try_read_owned() {
92                    break guard
93                        .task
94                        .as_ref()
95                        .map(|t| (t.description.clone(), t.intent.clone()));
96                }
97                attempt += 1;
98                if std::time::Instant::now() >= deadline {
99                    tracing::warn!(
100                        "session read-lock timeout after {attempt} attempts in ctx_read for {path}"
101                    );
102                    break None;
103                }
104                std::thread::sleep(std::time::Duration::from_millis(25));
105            }
106        };
107        // #1590: `auto_infer_task` fabricates descriptions like
108        // "Working on /repo/src/printer (explore)" from touched-file patterns.
109        // That is fine as a telemetry hint, but its "keywords" (Working,
110        // explore, a directory path) carry no signal about a file's contents —
111        // filtering a file down to a quarter of its lines on that basis, or
112        // pinning it to `full` as an intent target, invents a task the caller
113        // never stated. Only an explicitly set task steers a read.
114        let task_is_inferred = current_task
115            .as_ref()
116            .is_some_and(|(_, intent)| !task_intent_steers_read(intent.as_deref()));
117        let current_task = current_task.map(|(description, _)| description);
118        let task_ref = if task_is_inferred {
119            None
120        } else {
121            current_task.as_deref()
122        };
123        // #513: `raw=true` is the intuitive "give me the exact bytes" escape an
124        // agent reaches for. Alias it to mode="raw" (verbatim, unframed) and
125        // force a fresh disk read below so a re-read never collapses to an
126        // `[unchanged]`/auto-delta stub. An explicit raw flag wins over `mode`.
127        let arg_raw = get_bool(args, "raw").unwrap_or(false);
128        let explicit_mode_arg = resolve_explicit_mode(arg_raw, get_str(args, "mode"));
129        let explicit_mode = explicit_mode_arg.is_some();
130        // #1209: a malformed line-range payload (e.g. `lines:44:48` with a colon
131        // instead of a dash) must fail loudly, not silently return an empty
132        // window on small files or bounce to a full-file dump on large ones.
133        // `lines:` and `anchored:` share `parse_line_range`, so validating both
134        // line-range modes at the boundary catches the typo for either. Other
135        // modes are left untouched: bare `density:` is a valid fallback and
136        // unknown keywords keep the historical warn-and-fall-back behaviour.
137        if let Some(ref requested) = explicit_mode_arg
138            && (requested.starts_with("lines:") || requested.starts_with("anchored:"))
139            && let Err(e) = requested.parse::<crate::tools::ctx_read::ReadMode>()
140        {
141            return Err(ErrorData::invalid_params(e.user_message(), None));
142        }
143        let configured_mode = (!explicit_mode)
144            .then(lean_ctx::core::auto_mode_resolver::configured_default_mode)
145            .flatten();
146        // No range requested: the outline rule decides between the outline of
147        // a large file and the whole small file (owner decision 2026-10-30).
148        let learned_mode = if !explicit_mode && configured_mode.is_none() {
149            if let Ok(cache) = cache_lock.try_read() {
150                Some(crate::tools::ctx_smart_read::select_unranged_mode(
151                    &cache, path, task_ref,
152                ))
153            } else {
154                tracing::debug!(
155                    "cache lock contested during auto-mode selection for {path}; \
156                     resolving cold"
157                );
158                Some(crate::tools::ctx_smart_read::select_unranged_mode(
159                    &lean_ctx::core::cache::SessionCache::new(),
160                    path,
161                    task_ref,
162                ))
163            }
164        } else {
165            None
166        };
167        let auto_or_no_mode =
168            explicit_mode_arg.as_deref().is_none_or(|m| m == "auto") && configured_mode.is_none();
169        let mut mode = lean_ctx::core::auto_mode_resolver::resolve_mode_precedence(
170            explicit_mode_arg,
171            configured_mode,
172            learned_mode,
173            "full",
174        );
175        let mut fresh = get_bool(args, "fresh").unwrap_or(false);
176        // Reacquire original bytes under current rules; an older compressed
177        // view can have omitted the classification marker that now blocks it.
178        if lean_ctx::core::policy::runtime::is_active() {
179            fresh = true;
180        }
181        // #513: a raw/verbatim request always reads from disk — the whole point
182        // is exact current bytes, never a cached stub or delta.
183        if arg_raw || engine_interface_v1 {
184            fresh = true;
185        }
186        let cache_policy = crate::server::compaction_sync::effective_cache_policy();
187        let cache_policy_bypassed = cache_policy == "off";
188        if cache_policy_bypassed {
189            fresh = true;
190        }
191        let aggressiveness =
192            lean_ctx::core::aggressiveness::effective(get_f64(args, "aggressiveness"));
193        let protect = get_str_array(args, "protect").unwrap_or_default();
194        // One-knob UX: when the caller sets aggressiveness without pinning a mode,
195        // route through the proven density path at the mapped target. An explicit
196        // mode (incl. entropy/task) instead has the knob tune it via ReadTuning.
197        if !explicit_mode && let Some(a) = aggressiveness {
198            // SSOT mode construction via the typed `ReadMode` (#528): the typed
199            // `Density` Display emits the same `density:0.NN` the pipeline parses.
200            mode = crate::tools::ctx_read::ReadMode::Density(
201                lean_ctx::core::aggressiveness::AggressivenessProfile::from_level(a).density_target,
202            )
203            .to_string();
204        }
205        // `start_line` (and its `offset`/`limit` aliases) can pin a line window.
206        // The resolution lives in `apply_line_window`/`resolve_line_window` so
207        // the runtime path and the unit tests share one implementation and can
208        // never drift (GitHub #432 aliases, #259 explicit-mode, #253 line-1).
209        apply_line_window(
210            &mut mode,
211            &mut fresh,
212            explicit_mode,
213            get_int(args, "start_line"),
214            get_int(args, "offset"),
215            get_int(args, "limit"),
216        );
217
218        // A concrete per-call request outranks saved views and adaptive heuristics.
219        // `auto` explicitly delegates the choice; mandatory admission still runs.
220        let pinned_mode = explicit_mode && mode != "auto";
221        if pinned_mode
222            && (matches!(mode.as_str(), "raw" | "full" | "full-compact")
223                || mode.starts_with("lines:")
224                || mode.starts_with("anchored"))
225        {
226            // Exact/full requests must not collapse to a cache stub or a delta,
227            // including when the older delta_explicit preference is enabled.
228            fresh = true;
229        }
230
231        let resolved_agent_id = ctx.agent_id.as_ref().and_then(|a| match a.try_read() {
232            Ok(guard) => guard.clone(),
233            Err(_) => None,
234        });
235        let gate_result = if pinned_mode {
236            crate::server::context_gate::pre_dispatch_pinned_read_for_agent(
237                path,
238                &mode,
239                resolved_agent_id.as_deref(),
240            )
241        } else {
242            crate::server::context_gate::pre_dispatch_read_for_agent(
243                path,
244                &mode,
245                task_ref,
246                Some(&ctx.project_root),
247                resolved_agent_id.as_deref(),
248                fresh,
249            )
250        };
251        let mut engine_policy_admission = engine::admission_or_reject(
252            engine_interface_v1,
253            &gate_result,
254            &mode,
255            &ctx.project_root,
256            path,
257        )?;
258        let budget_warning = gate_result.budget_warning.clone();
259        // A read without a range (no mode, or `auto`, no window, no density
260        // knob) is answered by the outline rule; only a view the user saved
261        // for this file (overlay) still replaces it, the gate's heuristics do not.
262        let unranged = aggressiveness.is_none()
263            && auto_or_no_mode
264            && !mode.starts_with("lines:")
265            && !mode.starts_with("anchored");
266        let user_view = matches!(
267            gate_result.reason,
268            Some("overlay-set-view" | "pinned" | "excluded")
269        );
270        // #513: an explicit raw/verbatim request is never silently downgraded by
271        // the budget gate — the caller asked for exact bytes.
272        let mut mode_override_note: Option<String> = None;
273        if mode != "raw"
274            && (!unranged || user_view)
275            && let Some(overridden) = gate_result.overridden_mode
276        {
277            if explicit_mode {
278                let reason = gate_result.reason.unwrap_or("context-gate");
279                // #1588: an override the caller cannot escape is a dead end.
280                // Name the escape hatch for the two heuristics that honour it.
281                let escape = if matches!(
282                    reason,
283                    "bounce-prevention" | "intent-target" | "learned-full"
284                ) {
285                    ", pass fresh=true to keep the requested mode"
286                } else {
287                    ""
288                };
289                mode_override_note = Some(format!(
290                    "[mode overridden: {mode} -> {overridden}, reason={reason}{escape}]"
291                ));
292            }
293            mode = overridden;
294        }
295
296        let (instruction_mode, instruction_mode_note) = resolve_instruction_file_mode(path, &mode);
297        let mut mode = instruction_mode;
298        engine::require_aggressive(
299            engine_interface_v1,
300            &mode,
301            &ctx.project_root,
302            path,
303            &mut engine_policy_admission,
304        )?;
305
306        // Delta-aware explicit re-reads (opt-in: config `delta_explicit`, env
307        // LCTX_DELTA_EXPLICIT). Re-requesting full/lines:N-M content for a file
308        // this session already read re-emits content the model already holds;
309        // when the file changed on disk, a diff carries the same information in
310        // a fraction of the tokens, and an unchanged lines: request of a
311        // fully-delivered file collapses to the full-mode stub. The decision is
312        // a pure function of (cache, path, mode) — see
313        // `ctx_read::resolve_explicit_delta_mode`. First reads are unaffected;
314        // fresh=true always bypasses. Runs BEFORE the lines:→fresh guard below
315        // so a changed-file lines: re-read can still be diverted to a diff.
316        let mut delta_explicit_note: Option<String> = None;
317        if !fresh
318            && explicit_mode
319            && (mode == "full" || mode == "full-compact" || mode.starts_with("lines:"))
320            && lean_ctx::core::config::Config::load().delta_explicit_effective()
321            && let Ok(cache) = cache_lock.try_read()
322        {
323            let decision = crate::tools::ctx_read::resolve_explicit_delta_mode(
324                &cache,
325                path,
326                &mode,
327                explicit_mode,
328                fresh,
329                true,
330            );
331            mode = decision.mode;
332            delta_explicit_note = decision.note;
333        }
334
335        if mode.starts_with("lines:") {
336            fresh = true;
337        }
338
339        engine::reject_non_text_extension(
340            engine_interface_v1,
341            &ctx.project_root,
342            path,
343            &mut engine_policy_admission,
344        )?;
345        // A directory has no text to read; failing with the OS error ("Is a
346        // directory") left the agent guessing. Answer with its entries, as
347        // ctx_tree would, so the next call can name a file.
348        if !engine_interface_v1 && std::path::Path::new(path).is_dir() {
349            let (tree, _) = crate::tools::ctx_tree::handle(path, 1, false, true);
350            if tree.starts_with("ERROR:") {
351                return Err(ErrorData::invalid_params(tree, None));
352            }
353            return Ok(PreparedRead {
354                output: ToolOutput::simple(format!(
355                    "{path} is a directory — pass one of its files to ctx_read:\n{tree}"
356                )),
357                delivery: None,
358            });
359        }
360        if lean_ctx::core::binary_detect::is_llm_viewable_image(path) {
361            // Text detectors cannot inspect pixels: the gateway says so, and
362            // withholds the image where every detector is mandatory.
363            let bytes = std::fs::metadata(path).map_or(0, |meta| meta.len());
364            let note = lean_ctx::core::context_admission::admit_media(path, bytes)
365                .map_err(|error| ErrorData::invalid_params(error.to_string(), None))?;
366            return read_image_file(path).map(|mut output| {
367                if let (Some(note), Some(blocks)) = (note, output.content_blocks.as_mut()) {
368                    blocks.push(rmcp::model::ContentBlock::text(note));
369                }
370                PreparedRead {
371                    output,
372                    delivery: None,
373                }
374            });
375        }
376        if !engine_interface_v1 && lean_ctx::core::binary_detect::is_binary_file(path) {
377            let msg = lean_ctx::core::binary_detect::binary_file_message(path);
378            return Err(ErrorData::invalid_params(msg, None));
379        }
380        if !engine_interface_v1 {
381            let cap = lean_ctx::core::limits::max_read_bytes() as u64;
382            if let Ok(meta) = std::fs::metadata(path)
383                && meta.len() > cap
384            {
385                let msg = format!(
386                    "File too large ({} bytes, limit {} bytes via LCTX_MAX_READ_BYTES). \
387                     Use mode=\"lines:1-100\" or start_line+limit for partial reads, \
388                     mode=\"anchored\" with start_line+limit for edit-ready windows, \
389                     or increase the limit.",
390                    meta.len(),
391                    cap
392                );
393                return Err(ErrorData::invalid_params(msg, None));
394            }
395        }
396
397        // Compaction-aware: if host compacted since last check, reset delivery flags
398        // so post-compaction reads deliver full content instead of stubs.
399        if !fresh
400            && let Ok(data_dir) = lean_ctx::core::data_dir::lean_ctx_data_dir()
401            && let Ok(mut cache) = cache_lock.try_write()
402        {
403            crate::server::compaction_sync::sync_if_compacted(&mut cache, &data_dir);
404        }
405
406        // Fast path: if both per-file lock and cache write-lock are immediately
407        // available, execute inline without spawning a thread. This avoids thread +
408        // channel overhead for the ~90% of calls that are cache hits.
409        let read_timeout = std::time::Duration::from_secs(30);
410        let cancelled = Arc::new(AtomicBool::new(false));
411        let engine_snapshot = engine::SourceSnapshot::default();
412        let pending_cache = Arc::new(Mutex::new(PendingCacheDelivery::default()));
413        // Hash once for cross-agent delivery (avoids re-reading on record).
414        let delivery_metadata = (!engine_interface_v1
415            && lean_ctx::core::config::Config::load()
416                .ocla
417                .delivery_enabled())
418        .then(|| crate::tools::ctx_read::file_blake3_prefix(path))
419        .flatten();
420        let (output, resolved_mode, original, is_cache_hit, file_ref, _cache_stats, reuse_outcome) = {
421            let crp_mode = ctx.crp_mode;
422            let fast_result = 'fast: {
423                let file_lock = per_file_lock(path);
424                let Some(_file_guard) = file_lock.try_lock().ok() else {
425                    break 'fast None;
426                };
427
428                // Phase 1 (shared lock): the dominant case is re-reading an
429                // unchanged file. Serve the `[unchanged]` stub under a *read* lock
430                // so parallel reads of distinct files run concurrently instead of
431                // serializing on the global write lock. `auto` is included because
432                // a warm `auto` re-read of a fully-delivered file resolves to a
433                // full cache-hit; `try_stub_hit_readonly` self-guards (returns None
434                // unless full content was delivered and the file is unchanged), so a
435                // first or compressed-only `auto` read still falls through to
436                // Phase 2. When aggressiveness is set `mode` was already rewritten
437                // to `density:` upstream, so it never reaches this `auto` branch.
438                if !fresh
439                    && (mode == "full" || mode == "full-compact" || mode == "auto")
440                    && let Ok(cache) = cache_lock.try_read()
441                    && let Some(read_output) =
442                        crate::tools::ctx_read::try_stub_hit_readonly(&cache, path)
443                {
444                    let hit = read_output.is_cache_hit;
445                    let content = read_output.content;
446                    let rmode = read_output.resolved_mode;
447                    let orig = cache.get(path).map_or(0, |e| e.original_tokens);
448                    let fref = cache.file_ref_map().get(path).cloned();
449                    let stats = cache.get_stats();
450                    let stats_snapshot = (stats.total_reads(), stats.cache_hits());
451                    break 'fast Some((
452                        content,
453                        rmode,
454                        orig,
455                        hit,
456                        fref,
457                        stats_snapshot,
458                        ReuseOutcome::UnchangedStub,
459                    ));
460                }
461
462                // Misses use the slow path's double-check sequence: it does a
463                // second cache check, computes without the global lock, then
464                // briefly takes a write lock to store the result.
465                None
466            };
467
468            if let Some(result) = fast_result {
469                result
470            } else {
471                let cache_lock = cache_lock.clone();
472                let mode = mode.clone();
473                let task_owned = current_task.clone();
474                let protect_owned = protect.clone();
475                let path_owned = path.to_string();
476                let project_root_owned = ctx.project_root.clone();
477                let cancel_flag = cancelled.clone();
478                let snapshot_worker = engine_snapshot.clone();
479                let pending_cache_worker = pending_cache.clone();
480                let (tx, rx) = std::sync::mpsc::sync_channel(1);
481                lean_ctx::core::task_spine::TaskSpine::spawn_thread(move || {
482                    let file_lock = per_file_lock(&path_owned);
483
484                    // The per-file lock only deduplicates concurrent reads of
485                    // one file. When an earlier read holds it past the
486                    // deadline (an abandoned read of a huge file, a slow
487                    // mount), this read proceeds without it: duplicate work
488                    // beats failing the call — the cache store below has its
489                    // own lock, and "retry in a moment" met the same holder.
490                    let _file_guard = {
491                        let deadline =
492                            std::time::Instant::now() + std::time::Duration::from_secs(10);
493                        loop {
494                            if cancel_flag.load(Ordering::Relaxed) {
495                                return;
496                            }
497                            if let Ok(guard) = file_lock.try_lock() {
498                                break Some(guard);
499                            }
500                            if std::time::Instant::now() >= deadline {
501                                tracing::warn!(
502                                    "ctx_read: per-file lock still held after 10s for \
503                                     {path_owned}, reading without it"
504                                );
505                                break None;
506                            }
507                            std::thread::sleep(std::time::Duration::from_millis(50));
508                        }
509                    };
510
511                    if cancel_flag.load(Ordering::Relaxed) {
512                        return;
513                    }
514
515                    // ── Two-Phase Read (#1098) ──────────────────────────
516                    //
517                    // Phase 1 (read lock): try the [unchanged] stub — this is the
518                    // ~70% case (repeated reads of unchanged files). Previously
519                    // missing in the slow path, forcing every slow-path call into
520                    // the expensive write-lock branch.
521                    if !fresh
522                        && (mode == "full" || mode == "full-compact" || mode == "auto")
523                        && let Ok(cache) = cache_lock.try_read()
524                        && let Some(read_output) =
525                            crate::tools::ctx_read::try_stub_hit_readonly(&cache, &path_owned)
526                    {
527                        let content = read_output.content;
528                        let rmode = read_output.resolved_mode;
529                        let orig = cache.get(&path_owned).map_or(0, |e| e.original_tokens);
530                        let hit = true;
531                        let fref = cache.file_ref_map().get(path_owned.as_str()).cloned();
532                        let stats = cache.get_stats();
533                        let stats_snapshot = (stats.total_reads(), stats.cache_hits());
534                        let _ = tx.send((
535                            content,
536                            rmode,
537                            orig,
538                            hit,
539                            fref,
540                            stats_snapshot,
541                            ReuseOutcome::UnchangedStub,
542                        ));
543                        return;
544                    }
545
546                    // The session-local stub is checked first so the current
547                    // agent's own delivery always wins. On a miss, a verified
548                    // cross-agent delivery can avoid the disk read below.
549                    if !crate::tools::ctx_read::effective_fresh_for_delivery(fresh)
550                        && let Some(fp) = delivery_metadata
551                        && let Some(read_output) = crate::tools::ctx_read::try_cross_agent_stub(
552                            &path_owned,
553                            &mode,
554                            fp.hash,
555                            fp.mtime,
556                            crp_mode,
557                            crate::tools::ctx_read::ReadTuning::resolve(
558                                aggressiveness,
559                                &protect_owned,
560                            ),
561                        )
562                    {
563                        let _ = tx.send((
564                            read_output.content,
565                            read_output.resolved_mode,
566                            0,
567                            read_output.is_cache_hit,
568                            None,
569                            (0, 0),
570                            ReuseOutcome::CrossFileRef,
571                        ));
572                        return;
573                    }
574
575                    // Phase 2a: disk I/O under per-file lock but WITHOUT cache lock.
576                    let preread = match engine::read_source(
577                        engine_interface_v1,
578                        &path_owned,
579                        &project_root_owned,
580                        &snapshot_worker,
581                    ) {
582                        Ok(c) => Some(c),
583                        Err(e) => {
584                            tracing::warn!("ctx_read: cannot read {path_owned}: {e}");
585                            if engine_interface_v1 {
586                                let _ = tx.send((
587                                    "Engine v1 source read failed".to_owned(),
588                                    "error".into(),
589                                    0,
590                                    false,
591                                    None,
592                                    (0, 0),
593                                    ReuseOutcome::Cold,
594                                ));
595                                return;
596                            }
597                            None
598                        }
599                    };
600
601                    if cancel_flag.load(Ordering::Relaxed) {
602                        return;
603                    }
604
605                    // ── Phase 2b: Three-sub-phase read (#807) ──────────
606                    //
607                    // Previously held the global cache write-lock for the
608                    // entire computation (tree-sitter, entropy compression).
609                    // For large files this caused 30+ second lock holds and
610                    // cascading timeouts for all concurrent tool calls.
611                    //
612                    // New: prepare (brief lock) → compute (no lock) → store (brief lock).
613
614                    // #1590: an inferred task never steers a lossy view.
615                    let task_ref = if task_is_inferred {
616                        None
617                    } else {
618                        task_owned.as_deref()
619                    };
620                    let tuning =
621                        crate::tools::ctx_read::ReadTuning::resolve(aggressiveness, &protect_owned);
622
623                    // Helper: acquire write lock with deadline. `None` means the
624                    // lock stayed contended past the deadline; callers degrade
625                    // to an uncached read instead of failing it (#1925).
626                    macro_rules! acquire_write {
627                        ($deadline_secs:expr, $label:expr) => {{
628                            let deadline =
629                                std::time::Instant::now() + cache_lock_deadline($deadline_secs);
630                            loop {
631                                if cancel_flag.load(Ordering::Relaxed) {
632                                    return;
633                                }
634                                if let Ok(guard) = cache_lock.try_write() {
635                                    break Some(guard);
636                                }
637                                if std::time::Instant::now() >= deadline {
638                                    tracing::warn!(
639                                        "ctx_read: cache write-lock timeout ({}) for {path_owned}, \
640                                         serving uncached",
641                                        $label,
642                                    );
643                                    break None;
644                                }
645                                std::thread::sleep(std::time::Duration::from_millis(50));
646                            }
647                        }};
648                    }
649
650                    // Work that needs no cache state runs before the write lock
651                    // (#1925). The first `count_tokens` call builds the BPE tables
652                    // (seconds in debug builds); done under the lock it starved
653                    // every concurrent read into the deadline. Counting the
654                    // preread here also turns the count inside `cache.store()`
655                    // into a token-cache hit.
656                    let preread_tokens =
657                        preread.as_deref().map(lean_ctx::core::tokens::count_tokens);
658                    let mode_eff = if mode != "raw"
659                        && !mode.starts_with("lines:")
660                        && lean_ctx::core::config::Config::load()
661                            .proxy
662                            .is_path_compress_protected(&path_owned)
663                    {
664                        "full".to_string()
665                    } else {
666                        mode.clone()
667                    };
668
669                    // 2b-i: Brief write lock — prepare cache state, resolve
670                    // mode, check for hits. Sub-millisecond: HashMap lookups,
671                    // staleness checks, raw-content storage for new files.
672                    let (outcome, source_identity) = 'prepare: {
673                        let Some(mut cache) = acquire_write!(10, "prepare 10s") else {
674                            match prepare_uncached(
675                                preread,
676                                preread_tokens,
677                                &path_owned,
678                                &mode_eff,
679                                &tuning,
680                                task_ref,
681                            ) {
682                                Ok(uncached) => break 'prepare (uncached, None),
683                                Err(msg) => {
684                                    let _ = tx.send((
685                                        msg,
686                                        "error".into(),
687                                        0,
688                                        false,
689                                        None,
690                                        (0, 0),
691                                        ReuseOutcome::Cold,
692                                    ));
693                                    return;
694                                }
695                            }
696                        };
697                        let mut reuse_outcome = if cache_policy_bypassed {
698                            ReuseOutcome::PolicyBypass
699                        } else if fresh {
700                            ReuseOutcome::FreshBypass
701                        } else {
702                            ReuseOutcome::Cold
703                        };
704
705                        let file_ref = cache.get_file_ref(&path_owned);
706
707                        let effective_fresh = fresh
708                            || crate::tools::ctx_read::force_fresh_env()
709                            || (crate::tools::ctx_read::is_subagent_context()
710                                && !lean_ctx::core::conversation::scope_enabled());
711
712                        if effective_fresh {
713                            cache.invalidate(&path_owned);
714                        }
715
716                        if !effective_fresh {
717                            // #1584: `diff` is the one mode for which a stale
718                            // entry is the *point* — it holds the baseline the
719                            // delta is computed against. Dropping it here left
720                            // `handle_diff` with nothing to compare and made
721                            // every post-edit diff answer "no cached version".
722                            // Every other mode still refuses to serve content
723                            // that no longer matches the file on disk.
724                            let stale = mode_eff != "diff"
725                                && cache.get(&path_owned).is_some_and(|e| {
726                                    lean_ctx::core::cache::is_cache_entry_stale_verified(
727                                        &path_owned,
728                                        e.stored_mtime,
729                                        &e.hash,
730                                    )
731                                });
732                            if stale {
733                                cache.invalidate(&path_owned);
734                                reuse_outcome = ReuseOutcome::Stale;
735                            }
736                        }
737
738                        let snap = cache
739                            .get(&path_owned)
740                            .map(|e| (e.original_tokens, e.content()));
741
742                        // #1584: `diff` is a delta view, not a whole-file
743                        // render. It needs the cached baseline and has no arm in
744                        // `process_mode_tuned`, so letting it fall through to the
745                        // generic compute path made the MCP handler answer
746                        // "[WARNING: unknown mode 'diff']" and return the entire
747                        // file — while the CLI path handled it correctly. Both
748                        // paths now share `handle_diff`.
749                        let outcome = if mode_eff == "diff" {
750                            // A file that cannot be read is this read's typed
751                            // failure (`"error"`, answered as a tool error
752                            // below) with the same text — never a `diff` reply.
753                            let (out, baseline, diff_mode) = if fresh {
754                                let warning = "[warning] fresh+diff is redundant — fresh invalidates the cache, so no baseline is left to diff against. Use mode=full with fresh=true instead.";
755                                (warning.to_string(), None, "diff")
756                            } else {
757                                match crate::tools::ctx_read::prepare_diff(
758                                    &cache,
759                                    &path_owned,
760                                    &file_ref,
761                                ) {
762                                    Ok((out, _sent, baseline)) => (out, baseline, "diff"),
763                                    Err(failure) => (failure, None, "error"),
764                                }
765                            };
766                            let out = lean_ctx::core::redaction::redact_text_if_enabled(&out);
767                            let orig = baseline.as_deref().map_or_else(
768                                || cache.get(&path_owned).map_or(0, |e| e.original_tokens),
769                                lean_ctx::core::tokens::count_tokens,
770                            );
771                            pending_cache_worker
772                                .lock()
773                                .unwrap_or_else(std::sync::PoisonError::into_inner)
774                                .diff_baseline = baseline;
775                            let fref = cache.file_ref_map().get(path_owned.as_str()).cloned();
776                            let s = cache.get_stats();
777                            PrepareOutcome::Hit(
778                                out,
779                                diff_mode.to_string(),
780                                orig,
781                                false,
782                                fref,
783                                (s.total_reads(), s.cache_hits()),
784                                reuse_outcome,
785                            )
786                        } else if let Some((orig_tok, content_opt)) = snap {
787                            let resolved = if mode_eff == "auto" {
788                                tuning.auto_density_mode().unwrap_or_else(|| {
789                                    crate::tools::ctx_read::resolve_unranged_auto_mode(
790                                        Some(&cache),
791                                        &path_owned,
792                                        orig_tok,
793                                        cache.get(&path_owned).map_or(0, |e| e.line_count),
794                                        task_ref,
795                                    )
796                                })
797                            } else {
798                                mode_eff
799                            };
800
801                            if (resolved == "full" || resolved == "full-compact")
802                                && let Some(out) = crate::tools::ctx_read::try_stub_hit_readonly(
803                                    &cache,
804                                    &path_owned,
805                                )
806                            {
807                                let orig = cache.get(&path_owned).map_or(0, |e| e.original_tokens);
808                                let fref = cache.file_ref_map().get(path_owned.as_str()).cloned();
809                                let s = cache.get_stats();
810                                PrepareOutcome::Hit(
811                                    out.content,
812                                    out.resolved_mode,
813                                    orig,
814                                    true,
815                                    fref,
816                                    (s.total_reads(), s.cache_hits()),
817                                    ReuseOutcome::UnchangedStub,
818                                )
819                            } else if crate::tools::ctx_read::is_cacheable_mode(&resolved) {
820                                let ck = crate::tools::ctx_read::request_scoped_key(
821                                    crate::tools::ctx_read::compressed_cache_key(
822                                        &resolved,
823                                        crp_mode,
824                                        task_ref,
825                                        tuning.aggressiveness,
826                                        tuning.protect,
827                                    ),
828                                    mode == "auto",
829                                );
830                                if let Some(hit) = cache.get_compressed(&path_owned, &ck).cloned() {
831                                    lean_ctx::core::auto_mode_resolver::count_source(
832                                        "compressed_cache_hit",
833                                    );
834                                    let hit =
835                                        lean_ctx::core::redaction::redact_text_if_enabled(&hit);
836                                    let orig =
837                                        cache.get(&path_owned).map_or(0, |e| e.original_tokens);
838                                    let fref =
839                                        cache.file_ref_map().get(path_owned.as_str()).cloned();
840                                    let s = cache.get_stats();
841                                    PrepareOutcome::Hit(
842                                        hit,
843                                        resolved,
844                                        orig,
845                                        true,
846                                        fref,
847                                        (s.total_reads(), s.cache_hits()),
848                                        ReuseOutcome::RenderCacheHit,
849                                    )
850                                } else {
851                                    if reuse_outcome == ReuseOutcome::Cold
852                                        && cache.was_compressed_variant_evicted(&path_owned, &ck)
853                                    {
854                                        reuse_outcome = ReuseOutcome::VariantEvicted;
855                                    }
856                                    let c = content_opt
857                                        .or_else(|| preread.as_deref().map(String::from));
858                                    PrepareOutcome::Compute {
859                                        file_ref,
860                                        resolved_mode: resolved,
861                                        content: c.unwrap_or_default(),
862                                        original_tokens: orig_tok,
863                                        reuse_outcome,
864                                        cacheable: true,
865                                    }
866                                }
867                            } else {
868                                let c =
869                                    content_opt.or_else(|| preread.as_deref().map(String::from));
870                                PrepareOutcome::Compute {
871                                    file_ref,
872                                    resolved_mode: resolved,
873                                    content: c.unwrap_or_default(),
874                                    original_tokens: orig_tok,
875                                    reuse_outcome,
876                                    cacheable: true,
877                                }
878                            }
879                        } else {
880                            let raw = match preread {
881                                Some(c) if !c.is_empty() => c,
882                                _ => match crate::tools::ctx_read::read_file_lossy(&path_owned) {
883                                    Ok(c) if !c.is_empty() => c,
884                                    Ok(_) => {
885                                        tracing::debug!(
886                                            "ctx_read: skipping cache for empty content: {path_owned}"
887                                        );
888                                        let _ = tx.send((
889                                            empty_file_notice(&path_owned),
890                                            "full".into(),
891                                            0,
892                                            false,
893                                            None,
894                                            (0, 0),
895                                            ReuseOutcome::Cold,
896                                        ));
897                                        return;
898                                    }
899                                    Err(e) => {
900                                        tracing::debug!(
901                                            "ctx_read: skipping cache for empty content: {path_owned}"
902                                        );
903                                        let _ = tx.send((
904                                            format!("Cannot read file: {path_owned}: {e}"),
905                                            "error".into(),
906                                            0,
907                                            false,
908                                            None,
909                                            (0, 0),
910                                            ReuseOutcome::Cold,
911                                        ));
912                                        return;
913                                    }
914                                },
915                            };
916                            let sr = cache.store(&path_owned, &raw);
917                            let resolved = if mode_eff == "auto" {
918                                tuning.auto_density_mode().unwrap_or_else(|| {
919                                    crate::tools::ctx_read::resolve_unranged_auto_mode(
920                                        None,
921                                        &path_owned,
922                                        sr.original_tokens,
923                                        sr.line_count,
924                                        task_ref,
925                                    )
926                                })
927                            } else {
928                                mode_eff
929                            };
930                            PrepareOutcome::Compute {
931                                file_ref,
932                                resolved_mode: resolved,
933                                content: raw,
934                                original_tokens: sr.original_tokens,
935                                reuse_outcome,
936                                cacheable: true,
937                            }
938                        };
939                        let identity = cache.get(&path_owned).map(CacheSourceIdentity::from_entry);
940                        (outcome, identity)
941                    }; // write lock released
942
943                    if let PrepareOutcome::Hit(c, rm, orig, hit, fref, ss, reuse_outcome) = outcome
944                    {
945                        // Update last_mode for compressed-cache hits so the auto-mode
946                        // resolver can reuse this mode on future re-reads (#E26).
947                        let mut pending = pending_cache_worker
948                            .lock()
949                            .unwrap_or_else(std::sync::PoisonError::into_inner);
950                        let diff_baseline = pending.diff_baseline.take();
951                        *pending = PendingCacheDelivery {
952                            identity: source_identity,
953                            last_mode: Some(rm.clone()),
954                            diff_baseline,
955                            ..PendingCacheDelivery::default()
956                        };
957                        let _ = tx.send((c, rm, orig, hit, fref, ss, reuse_outcome));
958                        return;
959                    }
960                    let PrepareOutcome::Compute {
961                        file_ref,
962                        resolved_mode,
963                        content: compute_content,
964                        original_tokens,
965                        reuse_outcome,
966                        cacheable,
967                    } = outcome
968                    else {
969                        unreachable!()
970                    };
971
972                    if cancel_flag.load(Ordering::Relaxed) {
973                        return;
974                    }
975
976                    // 2b-ii: Heavy computation WITHOUT cache lock.
977                    // Tree-sitter, entropy compression, mode rendering all
978                    // run under the per-file mutex only (serializes same-file
979                    // reads, but does not block other files or tool calls).
980                    let short = lean_ctx::core::protocol::shorten_path(&path_owned);
981                    let ext_s = std::path::Path::new(&*path_owned)
982                        .extension()
983                        .and_then(|e| e.to_str())
984                        .unwrap_or("");
985
986                    let source_hash = blake3::hash(compute_content.as_bytes());
987                    let mut source_fingerprint = [0u8; 12];
988                    source_fingerprint.copy_from_slice(&source_hash.as_bytes()[..12]);
989                    let (mut computed, rmode) = if resolved_mode == "full"
990                        || resolved_mode == "full-compact"
991                    {
992                        if resolved_mode == "full-compact" {
993                            let (out, _) = crate::tools::ctx_read::format_full_compact_output(
994                                &compute_content,
995                            );
996                            (out, "full-compact".to_string())
997                        } else {
998                            let lc = compute_content.lines().count();
999                            let (out, _) = crate::tools::ctx_read::format_full_output(
1000                                &file_ref,
1001                                &short,
1002                                ext_s,
1003                                &compute_content,
1004                                original_tokens,
1005                                lc,
1006                                task_ref,
1007                            );
1008                            let ft = lean_ctx::core::tokens::count_tokens(&out);
1009                            let out = crate::tools::ctx_read::cap_to_raw(
1010                                out,
1011                                ft,
1012                                &compute_content,
1013                                original_tokens,
1014                                "full",
1015                            );
1016                            (out, "full".to_string())
1017                        }
1018                    } else {
1019                        // #1910: a fallback inside an `auto` read returns the
1020                        // bare file, bannerless.
1021                        let _auto_guard = (mode == "auto")
1022                            .then(crate::tools::ctx_read::render::AutoRequestGuard::new);
1023                        let (out, _) = crate::tools::ctx_read::process_mode_tuned(
1024                            &compute_content,
1025                            &resolved_mode,
1026                            &file_ref,
1027                            &short,
1028                            ext_s,
1029                            original_tokens,
1030                            crp_mode,
1031                            &path_owned,
1032                            task_ref,
1033                            tuning,
1034                        );
1035                        let out = if crate::tools::ctx_read::mode_allows_raw_cap(&resolved_mode) {
1036                            let ft = lean_ctx::core::tokens::count_tokens(&out);
1037                            crate::tools::ctx_read::cap_to_raw(
1038                                out,
1039                                ft,
1040                                &compute_content,
1041                                original_tokens,
1042                                &resolved_mode,
1043                            )
1044                        } else {
1045                            out
1046                        };
1047                        (out, resolved_mode)
1048                    };
1049
1050                    computed = lean_ctx::core::redaction::redact_text_if_enabled(&computed);
1051                    if let Some(note) =
1052                        lean_ctx::core::context_admission::note_for(&compute_content, &computed)
1053                    {
1054                        computed.push('\n');
1055                        computed.push_str(&note);
1056                    }
1057
1058                    if cancel_flag.load(Ordering::Relaxed) {
1059                        return;
1060                    }
1061
1062                    // Warming source bytes is acquisition; variants/full flags imply
1063                    // delivery and are committed only if this result is included.
1064                    // An uncached fallback (`F?` ref, #1925) never feeds the cache.
1065                    let compressed_variant = (cacheable
1066                        && crate::tools::ctx_read::is_cacheable_mode(&rmode))
1067                    .then(|| {
1068                        // #1910: an `auto` request's fallback is bannerless, an
1069                        // explicit one is not — they must never share a variant.
1070                        let key = crate::tools::ctx_read::request_scoped_key(
1071                            crate::tools::ctx_read::compressed_cache_key(
1072                                &rmode,
1073                                crp_mode,
1074                                task_ref,
1075                                tuning.aggressiveness,
1076                                tuning.protect,
1077                            ),
1078                            mode == "auto",
1079                        );
1080                        (key, computed.clone())
1081                    });
1082                    *pending_cache_worker
1083                        .lock()
1084                        .unwrap_or_else(std::sync::PoisonError::into_inner) =
1085                        PendingCacheDelivery {
1086                            identity: source_identity,
1087                            compressed_variant,
1088                            // A read the turn budget cuts is not a full delivery (#2172).
1089                            full_content: cacheable
1090                                && matches!(rmode.as_str(), "full" | "full-compact")
1091                                && !crate::tools::ctx_read::dedup_hook::exceeds_turn_budget(
1092                                    &computed,
1093                                ),
1094                            source_fingerprint: Some(source_fingerprint),
1095                            last_mode: cacheable.then(|| rmode.clone()),
1096                            record_bounce_read: true,
1097                            diff_baseline: None,
1098                        };
1099                    let _ = tx.send((
1100                        computed,
1101                        rmode,
1102                        original_tokens,
1103                        false,
1104                        Some(file_ref),
1105                        (0, 0),
1106                        reuse_outcome,
1107                    ));
1108                });
1109                if let Ok(result) = rx.recv_timeout(read_timeout) {
1110                    result
1111                } else {
1112                    cancelled.store(true, Ordering::Relaxed);
1113                    tracing::error!("ctx_read timed out after {read_timeout:?} for {path}");
1114                    let msg = format!(
1115                        "ERROR: ctx_read timed out after {}s reading {path}. \
1116                     The file may be very large or a blocking I/O issue occurred. \
1117                     Try mode=\"lines:1-100\" for a partial read.",
1118                        read_timeout.as_secs()
1119                    );
1120                    return Err(ErrorData::internal_error(msg, None));
1121                }
1122            } // end else (slow path)
1123        };
1124
1125        engine::reject_read_failure_if_enabled(
1126            engine_interface_v1,
1127            &resolved_mode,
1128            &ctx.project_root,
1129            path,
1130            &mut engine_policy_admission,
1131        )?;
1132        engine::require_aggressive(
1133            engine_interface_v1,
1134            &resolved_mode,
1135            &ctx.project_root,
1136            path,
1137            &mut engine_policy_admission,
1138        )?;
1139        if resolved_mode == "error" {
1140            return Err(ErrorData::invalid_params(output, None));
1141        }
1142
1143        let mut output = output;
1144        // Raw and receipt-backed contracts retain their exact payload. Only a
1145        // newly acquired source can produce a current optional Pro hint.
1146        let code_hint = if !engine_interface_v1 && resolved_mode != "raw" && !is_cache_hit {
1147            engine_snapshot.code_hint(path)
1148        } else {
1149            None
1150        };
1151        let engine_warning = engine_snapshot.record_if_enabled(
1152            &ctx.project_root,
1153            engine_policy_admission,
1154            &mut output,
1155        );
1156        let output_tokens = lean_ctx::core::tokens::count_tokens(&output);
1157
1158        let cache_delivery = std::mem::take(
1159            &mut *pending_cache
1160                .lock()
1161                .unwrap_or_else(std::sync::PoisonError::into_inner),
1162        );
1163        // A pre-read disk hash may describe older bytes. Relay only when it
1164        // agrees with the source that actually supplied this rendered result.
1165        let cross_agent = if is_cache_hit {
1166            None
1167        } else {
1168            delivery_metadata
1169                .filter(|fp| cache_delivery.source_fingerprint.as_ref() == Some(&fp.hash))
1170                .map(|fp| {
1171                    let relay_key = (matches!(resolved_mode.as_str(), "map" | "signatures")
1172                        && output.len() <= 8192)
1173                        .then(|| {
1174                            crate::tools::ctx_read::compressed_cache_key(
1175                                &resolved_mode,
1176                                ctx.crp_mode,
1177                                None,
1178                                aggressiveness,
1179                                &protect,
1180                            )
1181                        });
1182                    PendingCrossAgentDelivery {
1183                        hash: fp.hash,
1184                        mtime: fp.mtime,
1185                        line_count: fp.line_count,
1186                        output_tokens,
1187                        output: output.clone(),
1188                        relay_key,
1189                    }
1190                })
1191        };
1192
1193        // #1098: graph-related hints (callers/callees) are now computed AFTER the
1194        // cache lock is released. They involve SQLite queries (~50-200ms) that
1195        // previously blocked all parallel reads while holding the write lock.
1196        let graph_hint = if !is_cache_hit
1197            && !resolved_mode.starts_with("lines:")
1198            && lean_ctx::core::profiles::active_profile()
1199                .output_hints
1200                .related_hint()
1201        {
1202            crate::tools::ctx_read::graph_related_hint(path)
1203        } else {
1204            None
1205        };
1206
1207        // Cross-source hints: gated by profile `cross_source_hint` (default on).
1208        // When enabled, appends issue/PR/schema references from the property
1209        // graph. Skipped when graph.db doesn't exist (#682).
1210        // Legacy graph edges carry no source authority for protected reuse.
1211        // A project linked to an emspace space is indexed in the background
1212        // (detached, never waited for), so its page hints need no provider call.
1213        let hints_on = lean_ctx::core::profiles::active_profile()
1214            .output_hints
1215            .cross_source_hint()
1216            && !lean_ctx::core::policy::runtime::is_active();
1217        if hints_on && lean_ctx::core::providers::emspace::is_linked(&ctx.project_root) {
1218            lean_ctx::core::providers::emspace::refresh_index_if_due(&ctx.project_root);
1219        }
1220        let hints_suffix = if hints_on {
1221            let graph_db =
1222                lean_ctx::core::property_graph::graph_dir(&ctx.project_root).join("graph.db");
1223            let graph = graph_db
1224                .exists()
1225                .then(|| lean_ctx::core::property_graph::CodeGraph::open(&ctx.project_root))
1226                .transpose()
1227                .ok()
1228                .flatten();
1229            graph.map_or_else(String::new, |graph| {
1230                let edges = graph.all_cross_source_edges();
1231                if edges.is_empty() {
1232                    String::new()
1233                } else {
1234                    let ranges = scoped_read_ranges(&resolved_mode);
1235                    let relative_path =
1236                        lean_ctx::core::graph_index::graph_relative_key(path, &ctx.project_root);
1237                    let hints = lean_ctx::core::cross_source_hints::hints_for_file_matching(
1238                        path,
1239                        &edges,
1240                        &ctx.project_root,
1241                        |hint| {
1242                            // The far end of the edge is asked again (G04);
1243                            // the near end is the file this call just read.
1244                            graph.serves_file(&hint.source_uri)
1245                                && ranges.as_ref().is_none_or(|ranges| {
1246                                    hint_intersects_ranges(hint, ranges, &graph, &relative_path)
1247                                })
1248                        },
1249                    );
1250                    admitted_hints(&hints)
1251                }
1252            })
1253        } else {
1254            String::new()
1255        };
1256        // pz21 §1.8: a file this session downloaded through a matched
1257        // command is outside text, whatever its path.
1258        note_downloaded_read(path, &ctx.project_root);
1259
1260        // Rule injection (#1325): discover and append rules scoped to this file
1261        // path (CLAUDE.md, AGENTS.md, .cursor/rules, .claude/rules) so the agent
1262        // receives the same context it would get from the native Read tool.
1263        let rules_suffix = {
1264            let client_id = ctx
1265                .client_name
1266                .as_ref()
1267                .map(|c| c.blocking_read().clone())
1268                .unwrap_or_default();
1269            lean_ctx::core::rule_discovery::rules_suffix_for_read(
1270                path,
1271                &ctx.project_root,
1272                &client_id,
1273            )
1274        };
1275        let mut warnings: Vec<&str> = engine_warning.as_deref().into_iter().collect();
1276        if let Some(ref w) = budget_warning {
1277            warnings.push(w.as_str());
1278        }
1279        if let Some(ref w) = delta_explicit_note {
1280            warnings.push(w.as_str());
1281        }
1282        if let Some(ref w) = mode_override_note {
1283            warnings.push(w.as_str());
1284        }
1285        if let Some(ref w) = instruction_mode_note {
1286            warnings.push(w.as_str());
1287        }
1288        let file_summary = extract_file_summary(&output, path);
1289        let graph_suffix = graph_hint.map(|h| format!("\n{h}")).unwrap_or_default();
1290        // #977: notices (mode override, budget, delta) go BEFORE the
1291        // payload so client-side truncation of large outputs cannot hide them.
1292        let final_output = if !warnings.is_empty() {
1293            format!(
1294                "{}\n\n{output}{hints_suffix}{graph_suffix}{rules_suffix}",
1295                warnings.join("\n")
1296            )
1297        } else if hints_suffix.is_empty() && graph_suffix.is_empty() && rules_suffix.is_empty() {
1298            output
1299        } else {
1300            format!("{output}{hints_suffix}{graph_suffix}{rules_suffix}")
1301        };
1302        // Source lineage of this reply (G04). The requested file is published
1303        // even when the answer came from a cache or a stub. Everything the
1304        // reply carries from somewhere ELSE — graph neighbours, cross-source
1305        // hints, proactively expanded content — comes from stores whose
1306        // sources are not bound here: the call's source set is then not
1307        // complete and is recorded as such, never as "this file only".
1308        // Rule files are not in that list: `rule_discovery` appends a rule
1309        // only as the admitted rooted reader delivers it, and that reader
1310        // publishes the rule file into this call's source set (or marks the
1311        // set unbound itself when it cannot bind the file).
1312        crate::tools::ctx_read::publish_requested_source(path, &ctx.project_root, "ctx_read");
1313        let assembled_len = final_output.len();
1314        let foreign_content = !hints_suffix.is_empty() || !graph_suffix.is_empty();
1315
1316        // Proactive context: gated by profile `proactive_context` (default off).
1317        // When enabled, auto-expands previously compressed content that is
1318        // keyword-relevant to the current read (up to 2000 tokens).
1319        let final_output = if lean_ctx::core::profiles::active_profile()
1320            .output_hints
1321            .proactive_context()
1322        {
1323            let proactive_query = format!(
1324                "ctx_read path={path} mode={resolved_mode} task={}",
1325                task_ref.unwrap_or_default()
1326            );
1327            if let Some(block) = lean_ctx::core::relevance_tracker::proactive_context_for_path(
1328                &proactive_query,
1329                path,
1330            ) {
1331                format!("{final_output}{block}")
1332            } else {
1333                final_output
1334            }
1335        } else {
1336            final_output
1337        };
1338        if foreign_content || final_output.len() != assembled_len {
1339            lean_ctx::core::archive::authority::mark_unbound();
1340        }
1341
1342        // Keep paid annotations outside the pending source/relay cache payload;
1343        // an expired license must never recover an old paid analysis as fresh.
1344        let final_output = match code_hint {
1345            Some(note) => format!("{final_output}\n\n{note}"),
1346            None => final_output,
1347        };
1348
1349        // Monotonic guard (#1326): re-count tokens on the fully assembled output
1350        // (including hints, warnings, proactive context) and verify the compressed
1351        // result is actually smaller than the original. If annotations inflated the
1352        // output beyond the raw baseline, report zero savings so the ledger stays
1353        // honest. We keep the compressed form (it may still be more useful than raw)
1354        // but correct the accounting.
1355        let final_tokens = lean_ctx::core::tokens::count_tokens(&final_output);
1356        let verified_saved = original.saturating_sub(final_tokens);
1357
1358        let delivery = PendingReadDelivery {
1359            path: path.to_owned(),
1360            cache_delivery,
1361            cross_agent,
1362            file_ref,
1363            file_summary,
1364            resolved_mode: resolved_mode.clone(),
1365            original_tokens: original,
1366            reuse_outcome,
1367            is_cache_hit,
1368            agent_id: resolved_agent_id,
1369        };
1370        Ok(PreparedRead {
1371            delivery: Some(delivery),
1372            output: ToolOutput {
1373                text: final_output,
1374                original_tokens: original,
1375                saved_tokens: verified_saved,
1376                mode: Some(resolved_mode),
1377                path: Some(path.to_string()),
1378                changed: false,
1379                shell_outcome: None,
1380                content_blocks: None,
1381            },
1382        })
1383    }
1384}
1385
1386#[path = "ctx_read_engine.rs"]
1387mod engine;
1388// #660 LOC gate: the MCP tool definition lives in its own module.
1389#[path = "ctx_read_image.rs"]
1390mod image;
1391#[path = "ctx_read_schema.rs"]
1392mod schema;
1393#[path = "ctx_read_symbol.rs"]
1394mod symbol;
1395use image::read_image_file;
1396// #660 LOC gate: leaf helpers live in their own module.
1397#[path = "ctx_read_helpers.rs"]
1398mod helpers;
1399pub(crate) use helpers::task_intent_steers_read;
1400use helpers::{extract_file_summary, record_attribution_result};
1401
1402// #660 LOC gate: cache-lock deadline and the uncached fallback (#1925).
1403#[path = "ctx_read_uncached.rs"]
1404mod uncached;
1405use uncached::{PrepareOutcome, cache_lock_deadline, empty_file_notice, prepare_uncached};
1406
1407#[path = "ctx_read_window.rs"]
1408mod window;
1409// #1993: the batch read resolves `raw=true` the same way.
1410pub(super) use window::resolve_explicit_mode;
1411#[allow(unused_imports)]
1412// lines_mode + resolve_line_window used in #[cfg(test)] ctx_read_inline_tests
1413use window::{
1414    apply_line_window, hint_intersects_ranges, lines_mode, resolve_instruction_file_mode,
1415    resolve_line_window, resolve_raw_alias, scoped_read_ranges,
1416};
1417
1418// #660 LOC gate: inline tests split out to keep this file under the line cap.
1419#[cfg(test)]
1420#[path = "ctx_read_inline_tests.rs"]
1421mod tests;
1422
1423#[cfg(test)]
1424#[path = "ctx_read_security_tests.rs"]
1425mod security_tests;
1426
1427#[cfg(test)]
1428#[path = "ctx_read_gateway_tests.rs"]
1429mod gateway_tests;
1430
1431// #660 LOC gate: repo-param tests split out to keep this file under the line
1432// cap — see `ctx_read_repo_param_tests.rs`.
1433
1434#[cfg(test)]
1435#[path = "ctx_read_repo_param_tests.rs"]
1436mod repo_param_tests;
1437
1438#[cfg(test)]
1439#[path = "ctx_read_override_tests.rs"]
1440mod override_tests;
1441
1442#[path = "ctx_read_origin.rs"]
1443mod origin_parts;
1444use origin_parts::{admitted_hints, note_downloaded_read};