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
7fn ocla_metric_points(
13 context: &crate::core::ocla::OclaRequestContext,
14 tool: &str,
15 original: usize,
16 saved: usize,
17 duration_ms: u64,
18) -> Vec<crate::core::ocla::MetricPoint> {
19 let mut dimensions = std::collections::BTreeMap::new();
20 dimensions.insert("tool".to_string(), tool.to_string());
21 let original = metric_milli(original as u64);
22 let saved = metric_milli(saved as u64);
23 let duration = metric_milli(duration_ms);
24 vec![
25 crate::core::ocla::MetricPoint {
26 context: context.clone(),
27 name: "mcp.tool.original_tokens".to_string(),
28 value_milli: original,
29 dimensions: dimensions.clone(),
30 },
31 crate::core::ocla::MetricPoint {
32 context: context.clone(),
33 name: "mcp.tool.saved_tokens".to_string(),
34 value_milli: saved,
35 dimensions: dimensions.clone(),
36 },
37 crate::core::ocla::MetricPoint {
38 context: context.clone(),
39 name: "mcp.tool.duration_ms".to_string(),
40 value_milli: duration,
41 dimensions,
42 },
43 ]
44}
45
46fn metric_milli(value: u64) -> i64 {
47 i64::try_from(value.saturating_mul(1_000)).unwrap_or(i64::MAX)
48}
49
50impl LeanCtxServer {
51 pub async fn record_call(
53 &self,
54 tool: &str,
55 original: usize,
56 saved: usize,
57 mode: Option<String>,
58 ) {
59 self.record_call_with_timing(tool, original, saved, mode, 0)
60 .await;
61 }
62
63 pub async fn record_call_with_path(
67 &self,
68 tool: &str,
69 original: usize,
70 saved: usize,
71 mode: Option<String>,
72 path: Option<&str>,
73 duration_ms: u64,
74 ) {
75 self.record_call_with_timing_inner(tool, original, saved, mode, duration_ms, path)
76 .await;
77 }
78
79 pub async fn record_call_with_timing(
81 &self,
82 tool: &str,
83 original: usize,
84 saved: usize,
85 mode: Option<String>,
86 duration_ms: u64,
87 ) {
88 self.record_call_with_timing_inner(tool, original, saved, mode, duration_ms, None)
89 .await;
90 }
91
92 async fn record_call_with_timing_inner(
93 &self,
94 tool: &str,
95 original: usize,
96 saved: usize,
97 mode: Option<String>,
98 duration_ms: u64,
99 path: Option<&str>,
100 ) {
101 if let Some(agent_id) = self.presence_agent_id.read().await.clone()
102 && let Err(error) = crate::core::agents::AgentRegistry::heartbeat_persistent(&agent_id)
103 {
104 tracing::warn!("lean-ctx: failed to update MCP agent heartbeat: {error}");
105 }
106 let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
107 let mut calls = self.tool_calls.write().await;
108 calls.push(ToolCallRecord {
109 tool: tool.to_string(),
110 original_tokens: original,
111 saved_tokens: saved,
112 mode: mode.clone(),
113 duration_ms,
114 timestamp: ts.clone(),
115 });
116
117 const MAX_TOOL_CALL_RECORDS: usize = 500;
118 if calls.len() > MAX_TOOL_CALL_RECORDS {
119 let excess = calls.len() - MAX_TOOL_CALL_RECORDS;
120 calls.drain(..excess);
121 }
122
123 if duration_ms > 0 || original > 0 {
128 Self::append_tool_call_log(tool, duration_ms, original, saved, mode.as_deref(), &ts);
129 }
130
131 crate::core::events::emit_tool_call(
132 tool,
133 original as u64,
134 saved as u64,
135 mode.clone(),
136 duration_ms,
137 path.map(ToString::to_string),
138 );
139
140 let output_tokens = original.saturating_sub(saved);
141 crate::core::stats::record_at_turn(
142 tool,
143 original,
144 output_tokens,
145 crate::core::context_overhead::observed_turns(),
146 );
147 if tool == "ctx_shell" {
154 crate::core::savings_ledger::record_tool_event(
155 tool,
156 original,
157 output_tokens,
158 None,
159 None,
160 );
161 }
162
163 {
166 let session_r = self.session.read().await;
167 let agent_id = self
168 .presence_agent_id
169 .read()
170 .await
171 .clone()
172 .unwrap_or_default();
173 let ctx = crate::core::ocla::OclaRequestContext {
174 request_id: format!("mcp-{}", self.call_count.load(Ordering::Relaxed)),
175 session_id: session_r.id.clone(),
176 agent_id,
177 content_ref: path.map_or_else(|| format!("tool:{tool}"), |p| format!("file:{p}")),
178 tenant_id: None,
179 trace_id: "tr-unit".into(),
180 };
181 drop(session_r);
182 let observation = crate::core::ocla::Observation {
183 context: ctx.clone(),
184 name: format!("tool_call:{tool}"),
185 attributes: std::collections::BTreeMap::from([
186 ("original_tokens".into(), original.to_string()),
187 ("saved_tokens".into(), saved.to_string()),
188 ("duration_ms".into(), duration_ms.to_string()),
189 ]),
190 };
191 let hook = crate::core::ocla::OclaRegistry::global()
192 .observation_hook
193 .as_ref();
194 let _ = hook.observe(observation).await;
195
196 let outcome = crate::core::ocla::Outcome {
197 context: ctx.clone(),
198 accepted: Some(saved > 0),
199 quality_score_milli: if original > 0 {
200 Some(((saved as u64 * 1000) / original as u64).min(1000) as u16)
201 } else {
202 None
203 },
204 outcome_ref: None,
205 };
206 let _ = crate::core::ocla::OclaRegistry::global()
207 .outcome_tracker
208 .record_outcome(outcome);
209
210 let metrics = ocla_metric_points(&ctx, tool, original, saved, duration_ms);
214 let _ = crate::core::ocla::OclaRegistry::global()
215 .metrics_exporter
216 .export_metrics(metrics)
217 .await;
218 }
219 let mut session = self.session.write().await;
220 session.record_tool_call(saved as u64, original as u64);
221 if tool == "ctx_shell" {
222 session.record_command();
223 }
224 let pending_save = if session.should_save() {
225 session.prepare_save().ok()
226 } else {
227 None
228 };
229 drop(calls);
230 drop(session);
231
232 if let Some(prepared) = pending_save {
233 tokio::task::spawn_blocking(move || {
234 let _ = prepared.write_to_disk();
235 });
236 }
237
238 self.write_mcp_live_stats().await;
239 }
240
241 pub fn increment_and_check(&self) -> bool {
243 let count = self.call_count.fetch_add(1, Ordering::Relaxed) + 1;
244 let interval = Self::checkpoint_interval_effective();
245 interval > 0 && count.is_multiple_of(interval)
246 }
247
248 pub async fn auto_checkpoint(&self) -> Option<String> {
250 let cache = self.cache.read().await;
251 if cache.get_all_entries().is_empty() {
252 return None;
253 }
254 let complexity = crate::core::adaptive::classify_from_context(&cache);
255 let checkpoint = ctx_compress::handle(&cache, false, CrpMode::effective());
256 drop(cache);
257
258 let mut session = self.session.write().await;
259 let _ = session.save();
260 let session_summary = session.format_compact();
261 let has_insights = !session.findings.is_empty() || !session.decisions.is_empty();
262 let project_root = session.project_root.clone();
263 let summary_candidate = crate::core::session_summary::build_candidate(&session);
265 drop(session);
266
267 if has_insights && let Some(ref root) = project_root {
268 let root = root.clone();
269 std::thread::spawn(move || {
270 auto_consolidate_knowledge(&root);
271 });
272 }
273
274 if let Some(ref root) = project_root {
276 let root = root.clone();
277 std::thread::spawn(move || {
278 let _ =
279 crate::core::session_summary::maybe_record_periodic(&root, summary_candidate);
280 });
281 }
282
283 let multi_agent_block = self
284 .auto_multi_agent_checkpoint(project_root.as_ref())
285 .await;
286
287 self.record_call("ctx_compress", 0, 0, Some("auto".to_string()))
288 .await;
289
290 self.record_cep_snapshot().await;
291
292 if !crate::core::protocol::meta_visible() {
293 return None;
294 }
295
296 let doc_reminder = {
297 let session = self.session.read().await;
298 let calls = self.tool_calls.read().await;
299 Self::activity_nudge(&session, &calls)
300 };
301
302 Some(format!(
303 "{checkpoint}\n\n--- SESSION STATE ---\n{session_summary}\n\n{}{multi_agent_block}{doc_reminder}",
304 complexity.instruction_suffix()
305 ))
306 }
307
308 async fn auto_multi_agent_checkpoint(&self, project_root: Option<&String>) -> String {
309 let Some(root) = project_root else {
310 return String::new();
311 };
312
313 let registry = crate::core::agents::AgentRegistry::load_or_create();
314 let active = registry.list_active(Some(root));
315 if active.len() <= 1 {
316 return String::new();
317 }
318
319 let agent_id = self.agent_id.read().await;
320 let my_id = match agent_id.as_deref() {
321 Some(id) => id.to_string(),
322 None => return String::new(),
323 };
324 drop(agent_id);
325
326 let cache = self.cache.read().await;
327 let entries = cache.get_all_entries();
328 if !entries.is_empty() {
329 let mut by_access: Vec<_> = entries.iter().collect();
330 by_access.sort_by_key(|x| std::cmp::Reverse(x.1.read_count()));
331 let top_paths: Vec<&str> = by_access
332 .iter()
333 .take(5)
334 .map(|(key, _)| key.as_str())
335 .collect();
336 let paths_csv = top_paths.join(",");
337
338 let _ = ctx_share::handle(
339 "push",
340 Some(&my_id),
341 None,
342 Some(&paths_csv),
343 None,
344 &cache,
345 root,
346 );
347 }
348 drop(cache);
349
350 let pending_count = registry
351 .scratchpad
352 .iter()
353 .filter(|e| !e.read_by.contains(&my_id) && e.from_agent != my_id)
354 .count();
355
356 let shared_dir = crate::core::data_dir::lean_ctx_data_dir()
357 .unwrap_or_default()
358 .join("agents")
359 .join("shared");
360 let shared_count = if shared_dir.exists() {
361 std::fs::read_dir(&shared_dir).map_or(0, std::iter::Iterator::count)
362 } else {
363 0
364 };
365
366 let agent_names: Vec<String> = active
367 .iter()
368 .map(|a| {
369 let role = a.role.as_deref().unwrap_or(&a.agent_type);
370 format!("{role}({})", &a.agent_id[..8.min(a.agent_id.len())])
371 })
372 .collect();
373
374 format!(
375 "\n\n--- MULTI-AGENT SYNC ---\nAgents: {} | Pending msgs: {} | Shared contexts: {}\nAuto-shared top-5 cached files.\n--- END SYNC ---",
376 agent_names.join(", "),
377 pending_count,
378 shared_count,
379 )
380 }
381
382 pub fn append_tool_call_log(
384 tool: &str,
385 duration_ms: u64,
386 original: usize,
387 saved: usize,
388 mode: Option<&str>,
389 timestamp: &str,
390 ) {
391 const MAX_LOG_LINES: usize = 50;
392 if let Ok(dir) = crate::core::paths::state_dir() {
393 let log_path = dir.join("tool-calls.log");
394 let mode_str = mode.unwrap_or("-");
395 let slow = if duration_ms > 5000 { " **SLOW**" } else { "" };
396 let line = format!(
397 "{timestamp}\t{tool}\t{duration_ms}ms\torig={original}\tsaved={saved}\tmode={mode_str}{slow}\n"
398 );
399
400 let mut lines: Vec<String> = std::fs::read_to_string(&log_path)
401 .unwrap_or_default()
402 .lines()
403 .map(std::string::ToString::to_string)
404 .collect();
405
406 lines.push(line.trim_end().to_string());
407 if lines.len() > MAX_LOG_LINES {
408 lines.drain(0..lines.len() - MAX_LOG_LINES);
409 }
410
411 let _ = std::fs::write(&log_path, lines.join("\n") + "\n");
412 }
413 }
414
415 fn compute_cep_stats(
416 calls: &[ToolCallRecord],
417 stats: &crate::core::cache::CacheStats,
418 complexity: &crate::core::adaptive::TaskComplexity,
419 ) -> CepComputedStats {
420 let total_original: u64 = calls.iter().map(|c| c.original_tokens as u64).sum();
421 let total_saved: u64 = calls.iter().map(|c| c.saved_tokens as u64).sum();
422 let total_compressed = total_original.saturating_sub(total_saved);
423 let compression_rate = if total_original > 0 {
424 total_saved as f64 / total_original as f64
425 } else {
426 0.0
427 };
428
429 let modes_used: std::collections::HashSet<&str> =
430 calls.iter().filter_map(|c| c.mode.as_deref()).collect();
431 let mode_diversity = (modes_used.len() as f64 / 10.0).min(1.0);
432 let cache_util = stats.hit_rate() / 100.0;
433 let output_efficiency = 1.0 - crate::core::output_echo::current_avg_ratio();
436 let cep_score = cache_util * 0.25
437 + mode_diversity * 0.15
438 + compression_rate * 0.45
439 + output_efficiency * 0.15;
440
441 let mut mode_counts: std::collections::HashMap<String, u64> =
442 std::collections::HashMap::new();
443 for call in calls {
444 if let Some(ref mode) = call.mode {
445 *mode_counts.entry(mode.clone()).or_insert(0) += 1;
446 }
447 }
448
449 CepComputedStats {
450 cep_score: (cep_score * 100.0).round() as u32,
451 cache_util: (cache_util * 100.0).round() as u32,
452 mode_diversity: (mode_diversity * 100.0).round() as u32,
453 compression_rate: (compression_rate * 100.0).round() as u32,
454 total_original,
455 total_compressed,
456 total_saved,
457 mode_counts,
458 complexity: format!("{complexity:?}"),
459 cache_hits: stats.cache_hits(),
460 total_reads: stats.total_reads(),
461 tool_call_count: calls.len() as u64,
462 }
463 }
464
465 async fn write_mcp_live_stats(&self) {
466 let count = self.call_count.load(Ordering::Relaxed);
467 if count > 1 && !count.is_multiple_of(5) {
468 return;
469 }
470
471 let cache = self.cache.read().await;
472 let calls = self.tool_calls.read().await;
473 let stats = cache.get_stats();
474 let complexity = crate::core::adaptive::classify_from_context(&cache);
475 let dedup = crate::tools::ctx_read::dedup_hook::summary();
476
477 let cs = Self::compute_cep_stats(&calls, stats, &complexity);
478 let started_at = calls
479 .first()
480 .map(|c| c.timestamp.clone())
481 .unwrap_or_default();
482
483 drop(cache);
484 drop(calls);
485
486 crate::core::stats::record_cep_session(
492 cs.cep_score,
493 cs.cache_hits,
494 cs.total_reads,
495 cs.total_original,
496 cs.total_compressed,
497 &cs.mode_counts,
498 cs.tool_call_count,
499 &cs.complexity,
500 );
501
502 let effective_hits = cs.cache_hits + dedup.dedup_hits as u64;
503 let effective_reads = std::cmp::max(cs.total_reads, dedup.total_reads as u64);
504
505 let source_snapshot = crate::core::auto_mode_resolver::source_counts();
506 let compressed_cache_hits = source_snapshot
507 .iter()
508 .find(|(k, _)| *k == "compressed_cache_hit")
509 .map_or(0, |(_, v)| *v);
510 let full_delivery_degraded = source_snapshot
511 .iter()
512 .find(|(k, _)| *k == "full_delivery_degraded")
513 .map_or(0, |(_, v)| *v);
514
515 let live = serde_json::json!({
516 "cep_score": cs.cep_score,
517 "cache_utilization": cs.cache_util,
518 "mode_diversity": cs.mode_diversity,
519 "compression_rate": cs.compression_rate,
520 "task_complexity": cs.complexity,
521 "files_cached": cs.total_reads,
522 "total_reads": cs.total_reads,
523 "cache_hits": cs.cache_hits,
524 "dedup_reads": dedup.total_reads,
525 "dedup_hits": dedup.dedup_hits,
526 "dedup_tokens_saved": dedup.tokens_saved,
527 "effective_cache_hits": effective_hits,
528 "effective_cache_reads": effective_reads,
529 "compressed_cache_hits": compressed_cache_hits,
530 "full_delivery_degraded": full_delivery_degraded,
531 "tokens_saved": cs.total_saved,
532 "tokens_original": cs.total_original,
533 "tool_calls": cs.tool_call_count,
534 "started_at": started_at,
535 "updated_at": chrono::Local::now().to_rfc3339(),
536 });
537
538 if let Ok(dir) = crate::core::paths::state_dir() {
539 let _ = std::fs::write(dir.join("mcp-live.json"), live.to_string());
540 }
541 }
542
543 pub async fn record_cep_snapshot(&self) {
545 let cache = self.cache.read().await;
546 let calls = self.tool_calls.read().await;
547 let stats = cache.get_stats();
548 let complexity = crate::core::adaptive::classify_from_context(&cache);
549
550 let cs = Self::compute_cep_stats(&calls, stats, &complexity);
551
552 drop(cache);
553 drop(calls);
554
555 crate::core::stats::record_cep_session(
556 cs.cep_score,
557 cs.cache_hits,
558 cs.total_reads,
559 cs.total_original,
560 cs.total_compressed,
561 &cs.mode_counts,
562 cs.tool_call_count,
563 &cs.complexity,
564 );
565 }
566
567 fn activity_nudge(
568 session: &crate::core::session::SessionState,
569 calls: &[ToolCallRecord],
570 ) -> &'static str {
571 let last_doc_ts = session
572 .progress
573 .last()
574 .map(|p| p.timestamp)
575 .or_else(|| session.decisions.last().map(|d| d.timestamp))
576 .or_else(|| session.findings.last().map(|f| f.timestamp));
577
578 if let Some(ts) = last_doc_ts {
579 let age = chrono::Utc::now() - ts;
580 if age.num_minutes() < 8 {
581 return "";
582 }
583 }
584
585 let (weighted_score, significant_tools, shell_heavy, edit_heavy) =
586 Self::compute_activity_score(calls, last_doc_ts);
587
588 if weighted_score < 20 || significant_tools < 5 {
589 if session.stats.total_tool_calls >= 30
590 && session.decisions.is_empty()
591 && session.progress.is_empty()
592 {
593 return "\n[CHECKPOINT: please document current progress via ctx_session(action=\"task\") or ctx_knowledge(action=\"remember\")]";
594 }
595 return "";
596 }
597
598 if shell_heavy {
599 "\n[CHECKPOINT: multiple shell commands executed — any test results or findings worth persisting via ctx_knowledge(action=\"remember\")?]"
600 } else if edit_heavy {
601 "\n[CHECKPOINT: several files modified — document the architecture decision or pattern via ctx_knowledge(action=\"remember\")?]"
602 } else {
603 "\n[CHECKPOINT: significant work detected — consider persisting decisions via ctx_knowledge(action=\"remember\")]"
604 }
605 }
606
607 fn compute_activity_score(
608 calls: &[ToolCallRecord],
609 last_doc_ts: Option<chrono::DateTime<chrono::Utc>>,
610 ) -> (u32, u32, bool, bool) {
611 let mut weighted_score: u32 = 0;
612 let mut significant_tools: u32 = 0;
613 let mut shell_count: u32 = 0;
614 let mut edit_count: u32 = 0;
615
616 let since_doc: Vec<&ToolCallRecord> = if let Some(ts) = last_doc_ts {
617 let ts_str = ts.format("%Y-%m-%d %H:%M:%S").to_string();
618 calls.iter().filter(|c| c.timestamp > ts_str).collect()
619 } else {
620 calls.iter().collect()
621 };
622
623 for call in &since_doc {
624 let tool = call.tool.as_str();
625 let is_knowledge = tool == "ctx_knowledge" || tool == "ctx_session";
626 if is_knowledge {
627 weighted_score = 0;
628 significant_tools = 0;
629 shell_count = 0;
630 edit_count = 0;
631 continue;
632 }
633
634 let (weight, significant) = match tool {
635 "edit" | "write" | "str_replace" => {
636 edit_count += 1;
637 (4u32, true)
638 }
639 "ctx_shell" => {
640 shell_count += 1;
641 let is_test_or_build = call
642 .mode
643 .as_deref()
644 .is_some_and(|m| m.contains("test") || m.contains("build"));
645 if is_test_or_build {
646 (3, true)
647 } else {
648 (2, true)
649 }
650 }
651 "ctx_read" => {
652 let is_cache_hit = call.saved_tokens > 0
653 && call.original_tokens > 0
654 && call.saved_tokens == call.original_tokens;
655 if is_cache_hit { (0, false) } else { (1, false) }
656 }
657 _ => (1, false),
658 };
659
660 weighted_score = weighted_score.saturating_add(weight);
661 if significant {
662 significant_tools += 1;
663 }
664 }
665
666 let shell_heavy = shell_count >= 3 && shell_count > edit_count;
667 let edit_heavy = edit_count >= 3 && edit_count >= shell_count;
668
669 (weighted_score, significant_tools, shell_heavy, edit_heavy)
670 }
671}
672
673#[cfg(test)]
674mod activity_score_tests {
675 use super::*;
676
677 fn make_call(tool: &str, mode: Option<&str>) -> ToolCallRecord {
678 ToolCallRecord {
679 tool: tool.to_string(),
680 original_tokens: 100,
681 saved_tokens: 50,
682 mode: mode.map(String::from),
683 duration_ms: 10,
684 timestamp: "2026-01-01 12:00:00".to_string(),
685 }
686 }
687
688 fn make_cache_hit() -> ToolCallRecord {
689 ToolCallRecord {
690 tool: "ctx_read".to_string(),
691 original_tokens: 100,
692 saved_tokens: 100,
693 mode: Some("full".to_string()),
694 duration_ms: 1,
695 timestamp: "2026-01-01 12:00:00".to_string(),
696 }
697 }
698
699 #[test]
700 fn empty_calls_zero_score() {
701 let (score, sig, _, _) = LeanCtxServer::compute_activity_score(&[], None);
702 assert_eq!(score, 0);
703 assert_eq!(sig, 0);
704 }
705
706 #[test]
707 fn edits_have_highest_weight() {
708 let calls = vec![
709 make_call("edit", None),
710 make_call("edit", None),
711 make_call("edit", None),
712 ];
713 let (score, sig, _, edit_heavy) = LeanCtxServer::compute_activity_score(&calls, None);
714 assert_eq!(score, 12);
715 assert_eq!(sig, 3);
716 assert!(edit_heavy);
717 }
718
719 #[test]
720 fn shell_test_build_weight_three() {
721 let calls = vec![
722 make_call("ctx_shell", Some("test")),
723 make_call("ctx_shell", Some("build")),
724 make_call("ctx_shell", Some("test")),
725 ];
726 let (score, sig, shell_heavy, _) = LeanCtxServer::compute_activity_score(&calls, None);
727 assert_eq!(score, 9);
728 assert_eq!(sig, 3);
729 assert!(shell_heavy);
730 }
731
732 #[test]
733 fn cache_hits_zero_weight() {
734 let calls = vec![make_cache_hit(), make_cache_hit(), make_cache_hit()];
735 let (score, sig, _, _) = LeanCtxServer::compute_activity_score(&calls, None);
736 assert_eq!(score, 0);
737 assert_eq!(sig, 0);
738 }
739
740 #[test]
741 fn knowledge_call_resets_score() {
742 let calls = vec![
743 make_call("edit", None),
744 make_call("edit", None),
745 make_call("ctx_knowledge", None),
746 make_call("ctx_read", None),
747 ];
748 let (score, sig, _, _) = LeanCtxServer::compute_activity_score(&calls, None);
749 assert_eq!(score, 1);
750 assert_eq!(sig, 0);
751 }
752
753 #[test]
754 fn mixed_workflow_scoring() {
755 let calls = vec![
756 make_call("ctx_read", None),
757 make_call("ctx_read", None),
758 make_call("edit", None),
759 make_call("edit", None),
760 make_call("ctx_shell", Some("test output")),
761 make_call("ctx_shell", None),
762 ];
763 let (score, sig, _, _) = LeanCtxServer::compute_activity_score(&calls, None);
764 assert_eq!(score, 2 + 4 + 4 + 3 + 2);
765 assert_eq!(sig, 4);
766 }
767
768 #[test]
769 fn ocla_metrics_are_payload_free_and_saturating() {
770 let context = crate::core::ocla::OclaRequestContext {
771 request_id: "mcp-1".into(),
772 session_id: "session-1".into(),
773 agent_id: "agent-1".into(),
774 content_ref: "tool:ctx_read".into(),
775 tenant_id: None,
776 trace_id: "tr-unit".into(),
777 };
778 let points = super::ocla_metric_points(&context, "ctx_read", 750, 125, 42);
779 assert_eq!(points.len(), 3);
780 assert_eq!(points[0].name, "mcp.tool.original_tokens");
781 assert_eq!(points[0].value_milli, 750_000);
782 assert_eq!(points[1].name, "mcp.tool.saved_tokens");
783 assert_eq!(points[1].value_milli, 125_000);
784 assert_eq!(points[2].name, "mcp.tool.duration_ms");
785 assert_eq!(points[2].value_milli, 42_000);
786 assert_eq!(points[0].dimensions["tool"], "ctx_read");
787 assert_eq!(points[0].context, context);
788 assert!(!points[0].dimensions.contains_key("path"));
789
790 let saturated = super::ocla_metric_points(
791 &points[0].context,
792 "ctx_shell",
793 usize::MAX,
794 usize::MAX,
795 u64::MAX,
796 );
797 assert!(saturated.iter().all(|point| point.value_milli == i64::MAX));
798 }
799}