1use std::sync::atomic::Ordering;
2
3use super::server::{CepComputedStats, CrpMode, LeanCtxServer, ToolCallRecord};
4use super::startup::auto_consolidate_knowledge;
5use super::{ctx_compress, ctx_share};
6
7impl LeanCtxServer {
8 pub async fn record_call(
10 &self,
11 tool: &str,
12 original: usize,
13 saved: usize,
14 mode: Option<String>,
15 ) {
16 self.record_call_with_timing(tool, original, saved, mode, 0)
17 .await;
18 }
19
20 pub async fn record_call_with_path(
22 &self,
23 tool: &str,
24 original: usize,
25 saved: usize,
26 mode: Option<String>,
27 path: Option<&str>,
28 ) {
29 self.record_call_with_timing_inner(tool, original, saved, mode, 0, path)
30 .await;
31 }
32
33 pub async fn record_call_with_timing(
35 &self,
36 tool: &str,
37 original: usize,
38 saved: usize,
39 mode: Option<String>,
40 duration_ms: u64,
41 ) {
42 self.record_call_with_timing_inner(tool, original, saved, mode, duration_ms, None)
43 .await;
44 }
45
46 async fn record_call_with_timing_inner(
47 &self,
48 tool: &str,
49 original: usize,
50 saved: usize,
51 mode: Option<String>,
52 duration_ms: u64,
53 path: Option<&str>,
54 ) {
55 let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
56 let mut calls = self.tool_calls.write().await;
57 calls.push(ToolCallRecord {
58 tool: tool.to_string(),
59 original_tokens: original,
60 saved_tokens: saved,
61 mode: mode.clone(),
62 duration_ms,
63 timestamp: ts.clone(),
64 });
65
66 const MAX_TOOL_CALL_RECORDS: usize = 500;
67 if calls.len() > MAX_TOOL_CALL_RECORDS {
68 let excess = calls.len() - MAX_TOOL_CALL_RECORDS;
69 calls.drain(..excess);
70 }
71
72 if duration_ms > 0 {
73 Self::append_tool_call_log(tool, duration_ms, original, saved, mode.as_deref(), &ts);
74 }
75
76 crate::core::events::emit_tool_call(
77 tool,
78 original as u64,
79 saved as u64,
80 mode.clone(),
81 duration_ms,
82 path.map(ToString::to_string),
83 );
84
85 let output_tokens = original.saturating_sub(saved);
86 crate::core::stats::record(tool, original, output_tokens);
87
88 let mut session = self.session.write().await;
89 session.record_tool_call(saved as u64, original as u64);
90 if tool == "ctx_shell" {
91 session.record_command();
92 }
93 let pending_save = if session.should_save() {
94 session.prepare_save().ok()
95 } else {
96 None
97 };
98 drop(calls);
99 drop(session);
100
101 if let Some(prepared) = pending_save {
102 tokio::task::spawn_blocking(move || {
103 let _ = prepared.write_to_disk();
104 });
105 }
106
107 self.write_mcp_live_stats().await;
108 }
109
110 pub async fn is_prompt_cache_stale(&self) -> bool {
112 let last = *self.last_call.read().await;
113 last.elapsed().as_secs() > 3600
114 }
115
116 pub fn upgrade_mode_if_stale(mode: &str, stale: bool) -> &str {
118 if !stale {
119 return mode;
120 }
121 match mode {
122 "full" => "full",
123 "map" => "signatures",
124 m => m,
125 }
126 }
127
128 pub fn increment_and_check(&self) -> bool {
130 let count = self.call_count.fetch_add(1, Ordering::Relaxed) + 1;
131 let interval = Self::checkpoint_interval_effective();
132 interval > 0 && count.is_multiple_of(interval)
133 }
134
135 pub async fn auto_checkpoint(&self) -> Option<String> {
137 let cache = self.cache.read().await;
138 if cache.get_all_entries().is_empty() {
139 return None;
140 }
141 let complexity = crate::core::adaptive::classify_from_context(&cache);
142 let checkpoint = ctx_compress::handle(&cache, false, CrpMode::effective());
143 drop(cache);
144
145 let mut session = self.session.write().await;
146 let _ = session.save();
147 let session_summary = session.format_compact();
148 let has_insights = !session.findings.is_empty() || !session.decisions.is_empty();
149 let project_root = session.project_root.clone();
150 drop(session);
151
152 if has_insights {
153 if let Some(ref root) = project_root {
154 let root = root.clone();
155 std::thread::spawn(move || {
156 auto_consolidate_knowledge(&root);
157 });
158 }
159 }
160
161 let multi_agent_block = self
162 .auto_multi_agent_checkpoint(project_root.as_ref())
163 .await;
164
165 self.record_call("ctx_compress", 0, 0, Some("auto".to_string()))
166 .await;
167
168 self.record_cep_snapshot().await;
169
170 if !crate::core::protocol::meta_visible() {
171 return None;
172 }
173
174 Some(format!(
175 "{checkpoint}\n\n--- SESSION STATE ---\n{session_summary}\n\n{}{multi_agent_block}",
176 complexity.instruction_suffix()
177 ))
178 }
179
180 async fn auto_multi_agent_checkpoint(&self, project_root: Option<&String>) -> String {
181 let Some(root) = project_root else {
182 return String::new();
183 };
184
185 let registry = crate::core::agents::AgentRegistry::load_or_create();
186 let active = registry.list_active(Some(root));
187 if active.len() <= 1 {
188 return String::new();
189 }
190
191 let agent_id = self.agent_id.read().await;
192 let my_id = match agent_id.as_deref() {
193 Some(id) => id.to_string(),
194 None => return String::new(),
195 };
196 drop(agent_id);
197
198 let cache = self.cache.read().await;
199 let entries = cache.get_all_entries();
200 if !entries.is_empty() {
201 let mut by_access: Vec<_> = entries.iter().collect();
202 by_access.sort_by_key(|x| std::cmp::Reverse(x.1.read_count));
203 let top_paths: Vec<&str> = by_access
204 .iter()
205 .take(5)
206 .map(|(key, _)| key.as_str())
207 .collect();
208 let paths_csv = top_paths.join(",");
209
210 let _ = ctx_share::handle(
211 "push",
212 Some(&my_id),
213 None,
214 Some(&paths_csv),
215 None,
216 &cache,
217 root,
218 );
219 }
220 drop(cache);
221
222 let pending_count = registry
223 .scratchpad
224 .iter()
225 .filter(|e| !e.read_by.contains(&my_id) && e.from_agent != my_id)
226 .count();
227
228 let shared_dir = crate::core::data_dir::lean_ctx_data_dir()
229 .unwrap_or_default()
230 .join("agents")
231 .join("shared");
232 let shared_count = if shared_dir.exists() {
233 std::fs::read_dir(&shared_dir).map_or(0, std::iter::Iterator::count)
234 } else {
235 0
236 };
237
238 let agent_names: Vec<String> = active
239 .iter()
240 .map(|a| {
241 let role = a.role.as_deref().unwrap_or(&a.agent_type);
242 format!("{role}({})", &a.agent_id[..8.min(a.agent_id.len())])
243 })
244 .collect();
245
246 format!(
247 "\n\n--- MULTI-AGENT SYNC ---\nAgents: {} | Pending msgs: {} | Shared contexts: {}\nAuto-shared top-5 cached files.\n--- END SYNC ---",
248 agent_names.join(", "),
249 pending_count,
250 shared_count,
251 )
252 }
253
254 pub fn append_tool_call_log(
256 tool: &str,
257 duration_ms: u64,
258 original: usize,
259 saved: usize,
260 mode: Option<&str>,
261 timestamp: &str,
262 ) {
263 const MAX_LOG_LINES: usize = 50;
264 if let Ok(dir) = crate::core::data_dir::lean_ctx_data_dir() {
265 let log_path = dir.join("tool-calls.log");
266 let mode_str = mode.unwrap_or("-");
267 let slow = if duration_ms > 5000 { " **SLOW**" } else { "" };
268 let line = format!(
269 "{timestamp}\t{tool}\t{duration_ms}ms\torig={original}\tsaved={saved}\tmode={mode_str}{slow}\n"
270 );
271
272 let mut lines: Vec<String> = std::fs::read_to_string(&log_path)
273 .unwrap_or_default()
274 .lines()
275 .map(std::string::ToString::to_string)
276 .collect();
277
278 lines.push(line.trim_end().to_string());
279 if lines.len() > MAX_LOG_LINES {
280 lines.drain(0..lines.len() - MAX_LOG_LINES);
281 }
282
283 let _ = std::fs::write(&log_path, lines.join("\n") + "\n");
284 }
285 }
286
287 fn compute_cep_stats(
288 calls: &[ToolCallRecord],
289 stats: &crate::core::cache::CacheStats,
290 complexity: &crate::core::adaptive::TaskComplexity,
291 ) -> CepComputedStats {
292 let total_original: u64 = calls.iter().map(|c| c.original_tokens as u64).sum();
293 let total_saved: u64 = calls.iter().map(|c| c.saved_tokens as u64).sum();
294 let total_compressed = total_original.saturating_sub(total_saved);
295 let compression_rate = if total_original > 0 {
296 total_saved as f64 / total_original as f64
297 } else {
298 0.0
299 };
300
301 let modes_used: std::collections::HashSet<&str> =
302 calls.iter().filter_map(|c| c.mode.as_deref()).collect();
303 let mode_diversity = (modes_used.len() as f64 / 10.0).min(1.0);
304 let cache_util = stats.hit_rate() / 100.0;
305 let cep_score = cache_util * 0.3 + mode_diversity * 0.2 + compression_rate * 0.5;
306
307 let mut mode_counts: std::collections::HashMap<String, u64> =
308 std::collections::HashMap::new();
309 for call in calls {
310 if let Some(ref mode) = call.mode {
311 *mode_counts.entry(mode.clone()).or_insert(0) += 1;
312 }
313 }
314
315 CepComputedStats {
316 cep_score: (cep_score * 100.0).round() as u32,
317 cache_util: (cache_util * 100.0).round() as u32,
318 mode_diversity: (mode_diversity * 100.0).round() as u32,
319 compression_rate: (compression_rate * 100.0).round() as u32,
320 total_original,
321 total_compressed,
322 total_saved,
323 mode_counts,
324 complexity: format!("{complexity:?}"),
325 cache_hits: stats.cache_hits,
326 total_reads: stats.total_reads,
327 tool_call_count: calls.len() as u64,
328 }
329 }
330
331 async fn write_mcp_live_stats(&self) {
332 let count = self.call_count.load(Ordering::Relaxed);
333 if count > 1 && !count.is_multiple_of(5) {
334 return;
335 }
336
337 let cache = self.cache.read().await;
338 let calls = self.tool_calls.read().await;
339 let stats = cache.get_stats();
340 let complexity = crate::core::adaptive::classify_from_context(&cache);
341
342 let cs = Self::compute_cep_stats(&calls, stats, &complexity);
343 let started_at = calls
344 .first()
345 .map(|c| c.timestamp.clone())
346 .unwrap_or_default();
347
348 drop(cache);
349 drop(calls);
350 let live = serde_json::json!({
351 "cep_score": cs.cep_score,
352 "cache_utilization": cs.cache_util,
353 "mode_diversity": cs.mode_diversity,
354 "compression_rate": cs.compression_rate,
355 "task_complexity": cs.complexity,
356 "files_cached": cs.total_reads,
357 "total_reads": cs.total_reads,
358 "cache_hits": cs.cache_hits,
359 "tokens_saved": cs.total_saved,
360 "tokens_original": cs.total_original,
361 "tool_calls": cs.tool_call_count,
362 "started_at": started_at,
363 "updated_at": chrono::Local::now().to_rfc3339(),
364 });
365
366 if let Ok(dir) = crate::core::data_dir::lean_ctx_data_dir() {
367 let _ = std::fs::write(dir.join("mcp-live.json"), live.to_string());
368 }
369 }
370
371 pub async fn record_cep_snapshot(&self) {
373 let cache = self.cache.read().await;
374 let calls = self.tool_calls.read().await;
375 let stats = cache.get_stats();
376 let complexity = crate::core::adaptive::classify_from_context(&cache);
377
378 let cs = Self::compute_cep_stats(&calls, stats, &complexity);
379
380 drop(cache);
381 drop(calls);
382
383 crate::core::stats::record_cep_session(
384 cs.cep_score,
385 cs.cache_hits,
386 cs.total_reads,
387 cs.total_original,
388 cs.total_compressed,
389 &cs.mode_counts,
390 cs.tool_call_count,
391 &cs.complexity,
392 );
393 }
394}