Skip to main content

lean_ctx/tools/registered/
ctx_cache.rs

1use rmcp::ErrorData;
2use rmcp::model::Tool;
3use serde_json::{Map, Value, json};
4
5use crate::server::tool_trait::{McpTool, ToolContext, ToolOutput, get_str, require_resolved_path};
6use crate::tool_defs::tool_def;
7
8pub struct CtxCacheTool;
9
10fn append_content_dedup_stats(lines: &mut Vec<String>) {
11    let dedup = crate::tools::ctx_read::dedup_hook::summary();
12    if dedup.total_reads > 0 {
13        lines.push(format!(
14            "content dedup: {} hit(s)/{} read(s) ({:.1}%), {} tokens saved",
15            dedup.dedup_hits,
16            dedup.total_reads,
17            dedup.hit_rate * 100.0,
18            dedup.tokens_saved
19        ));
20    }
21}
22
23impl McpTool for CtxCacheTool {
24    fn name(&self) -> &'static str {
25        "ctx_cache"
26    }
27
28    fn tool_def(&self) -> Tool {
29        tool_def(
30            "ctx_cache",
31            "Cache operations — inspect, clear, or invalidate the read cache.\n\
32            Actions: status lists cached files; clear empties all (recover token budget);\n\
33            invalidate path=... refreshes a single entry.\n\
34            Use to diagnose stale content or recover budget after large reads.\n\
35            ANTIPATTERN: does NOT affect disk files — only cached read content.",
36            json!({
37                "type": "object",
38                "properties": {
39                    "action": {
40                        "type": "string",
41                        "enum": ["status", "clear", "invalidate"],
42                        "description": "status|clear|invalidate"
43                    },
44                    "path": {
45                        "type": "string",
46                        "description": "File path for invalidate action (only used with action=invalidate)"
47                    }
48                },
49                "allOf": [
50                    {
51                        "if": {
52                            "properties": { "action": { "const": "invalidate" } },
53                            "required": ["action"]
54                        },
55                        "then": { "required": ["action", "path"] }
56                    }
57                ],
58                "required": ["action"]
59            }),
60        )
61    }
62
63    fn handle(
64        &self,
65        args: &Map<String, Value>,
66        ctx: &ToolContext,
67    ) -> Result<ToolOutput, ErrorData> {
68        let action = get_str(args, "action")
69            .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
70
71        let invalidate_path = if action == "invalidate" {
72            Some(require_resolved_path(ctx, args, "path")?)
73        } else {
74            None
75        };
76
77        let cache = ctx
78            .cache
79            .as_ref()
80            .ok_or_else(|| ErrorData::internal_error("cache not available", None))?;
81        let Some(mut guard) = crate::server::bounded_lock::write(cache, "ctx_cache") else {
82            return Ok(ToolOutput::simple(
83                "[cache lock temporarily unavailable — retry in a moment]".to_string(),
84            ));
85        };
86
87        let result = match action.as_str() {
88            "status" => {
89                let entries = guard.get_all_entries();
90                let mut lines = if entries.is_empty() {
91                    vec!["Cache empty — no files tracked.".to_string()]
92                } else {
93                    let mut lines = vec![format!("Cache: {} file(s)", entries.len())];
94                    for (path, entry) in &entries {
95                        let fref = guard
96                            .file_ref_map()
97                            .get(*path)
98                            .map_or("F?", std::string::String::as_str);
99                        lines.push(format!(
100                            "  {fref}={} [{}L, {}t, read {}x]",
101                            crate::core::protocol::shorten_path(path),
102                            entry.line_count,
103                            entry.original_tokens,
104                            entry.read_count()
105                        ));
106                    }
107                    lines
108                };
109                append_content_dedup_stats(&mut lines);
110                // Re-delivery telemetry: forced full re-sends grouped by cause
111                // (diagnostic only — never enters a cacheable body, #498).
112                let t = crate::core::cache_telemetry::snapshot();
113                if t.total() > 0 {
114                    lines.push(format!(
115                        "re-deliveries forced: compaction={} idle={} eviction={} conversation={} (total {})",
116                        t.compaction, t.idle, t.eviction, t.conversation, t.total()
117                    ));
118                }
119                if t.raw_cap_count > 0 {
120                    lines.push(format!(
121                        "raw-cap fallbacks: {} (prevented {} tokens inflation)",
122                        t.raw_cap_count, t.raw_cap_prevented
123                    ));
124                }
125                lines.join("\n")
126            }
127            "clear" => {
128                let count = guard.clear();
129                format!(
130                    "Cache cleared — {count} file(s) removed. Next ctx_read will return full content."
131                )
132            }
133            "invalidate" => {
134                let Some(path) = invalidate_path else {
135                    return Ok(ToolOutput::simple(
136                        "Missing path for invalidate action.".to_string(),
137                    ));
138                };
139                if guard.invalidate(&path) {
140                    format!(
141                        "Invalidated cache for {}. Next ctx_read will return full content.",
142                        crate::core::protocol::shorten_path(&path)
143                    )
144                } else {
145                    format!(
146                        "{} was not in cache.",
147                        crate::core::protocol::shorten_path(&path)
148                    )
149                }
150            }
151            _ => "Unknown action. Use: status, clear, invalidate".to_string(),
152        };
153
154        Ok(ToolOutput {
155            text: result,
156            original_tokens: 0,
157            saved_tokens: 0,
158            mode: Some(action),
159            path: None,
160            changed: false,
161            shell_outcome: None,
162            content_blocks: None,
163        })
164    }
165}