1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize, Default)]
4#[serde(default)]
5pub struct MemoryPolicy {
6 pub knowledge: KnowledgePolicy,
7 pub episodic: EpisodicPolicy,
8 pub procedural: ProceduralPolicy,
9 pub lifecycle: LifecyclePolicy,
10 pub embeddings: EmbeddingsPolicy,
11 pub gotcha: GotchaPolicy,
12 pub admission: AdmissionPolicy,
13 pub compaction: CompactionPolicy,
14}
15
16impl MemoryPolicy {
17 pub fn apply_env_overrides(&mut self) {
18 self.knowledge.apply_env_overrides();
19 self.episodic.apply_env_overrides();
20 self.procedural.apply_env_overrides();
21 self.lifecycle.apply_env_overrides();
22 self.embeddings.apply_env_overrides();
23 self.gotcha.apply_env_overrides();
24 self.admission.apply_env_overrides();
25 self.compaction.apply_env_overrides();
26 }
27
28 pub fn apply_overrides(&mut self, o: &MemoryPolicyOverrides) {
29 self.knowledge.apply_overrides(&o.knowledge);
30 self.lifecycle.apply_overrides(&o.lifecycle);
31 }
32
33 pub fn validate(&self) -> Result<(), String> {
34 self.knowledge.validate()?;
35 self.episodic.validate()?;
36 self.procedural.validate()?;
37 self.lifecycle.validate()?;
38 self.embeddings.validate()?;
39 self.gotcha.validate()?;
40 self.admission.validate()?;
41 self.compaction.validate()?;
42 Ok(())
43 }
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize, Default)]
47#[serde(default)]
48pub struct MemoryPolicyOverrides {
49 pub knowledge: KnowledgePolicyOverrides,
50 pub lifecycle: LifecyclePolicyOverrides,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize, Default)]
54#[serde(default)]
55pub struct KnowledgePolicyOverrides {
56 pub max_facts: Option<usize>,
57 pub max_patterns: Option<usize>,
58 pub max_history: Option<usize>,
59 pub contradiction_threshold: Option<f32>,
60 pub recall_facts_limit: Option<usize>,
61 pub rooms_limit: Option<usize>,
62 pub timeline_limit: Option<usize>,
63 pub relations_limit: Option<usize>,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize, Default)]
67#[serde(default)]
68pub struct LifecyclePolicyOverrides {
69 pub decay_rate: Option<f32>,
70 pub low_confidence_threshold: Option<f32>,
71 pub stale_days: Option<i64>,
72 pub similarity_threshold: Option<f32>,
73 pub forgetting_model: Option<String>,
74 pub base_stability_days: Option<f32>,
75 pub archetype_aware_decay: Option<bool>,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
79#[serde(default)]
80pub struct KnowledgePolicy {
81 pub max_facts: usize,
82 pub max_patterns: usize,
83 pub max_history: usize,
84 pub contradiction_threshold: f32,
85 pub recall_facts_limit: usize,
87 pub rooms_limit: usize,
89 pub timeline_limit: usize,
91 pub relations_limit: usize,
93}
94
95impl Default for KnowledgePolicy {
96 fn default() -> Self {
97 Self {
98 max_facts: 200,
99 max_patterns: 50,
100 max_history: 100,
101 contradiction_threshold: 0.5,
102 recall_facts_limit: crate::core::budgets::KNOWLEDGE_RECALL_FACTS_LIMIT,
103 rooms_limit: crate::core::budgets::KNOWLEDGE_ROOMS_LIMIT,
104 timeline_limit: crate::core::budgets::KNOWLEDGE_TIMELINE_LIMIT,
105 relations_limit: 40,
106 }
107 }
108}
109
110impl KnowledgePolicy {
111 fn apply_env_overrides(&mut self) {
112 if let Ok(v) = std::env::var("LEAN_CTX_KNOWLEDGE_MAX_FACTS")
113 && let Ok(n) = v.parse()
114 {
115 self.max_facts = n;
116 }
117 if let Ok(v) = std::env::var("LEAN_CTX_KNOWLEDGE_MAX_PATTERNS")
118 && let Ok(n) = v.parse()
119 {
120 self.max_patterns = n;
121 }
122 if let Ok(v) = std::env::var("LEAN_CTX_KNOWLEDGE_MAX_HISTORY")
123 && let Ok(n) = v.parse()
124 {
125 self.max_history = n;
126 }
127 if let Ok(v) = std::env::var("LEAN_CTX_KNOWLEDGE_CONTRADICTION_THRESHOLD")
128 && let Ok(n) = v.parse()
129 {
130 self.contradiction_threshold = n;
131 }
132 if let Ok(v) = std::env::var("LEAN_CTX_KNOWLEDGE_RECALL_FACTS_LIMIT")
133 && let Ok(n) = v.parse()
134 {
135 self.recall_facts_limit = n;
136 }
137 if let Ok(v) = std::env::var("LEAN_CTX_KNOWLEDGE_ROOMS_LIMIT")
138 && let Ok(n) = v.parse()
139 {
140 self.rooms_limit = n;
141 }
142 if let Ok(v) = std::env::var("LEAN_CTX_KNOWLEDGE_TIMELINE_LIMIT")
143 && let Ok(n) = v.parse()
144 {
145 self.timeline_limit = n;
146 }
147 if let Ok(v) = std::env::var("LEAN_CTX_KNOWLEDGE_RELATIONS_LIMIT")
148 && let Ok(n) = v.parse()
149 {
150 self.relations_limit = n;
151 }
152 }
153
154 fn validate(&self) -> Result<(), String> {
155 if self.max_facts == 0 {
156 return Err("memory.knowledge.max_facts must be > 0".to_string());
157 }
158 if self.max_patterns == 0 {
159 return Err("memory.knowledge.max_patterns must be > 0".to_string());
160 }
161 if self.max_history == 0 {
162 return Err("memory.knowledge.max_history must be > 0".to_string());
163 }
164 if !(0.0..=1.0).contains(&self.contradiction_threshold) {
165 return Err(
166 "memory.knowledge.contradiction_threshold must be in [0.0, 1.0]".to_string(),
167 );
168 }
169 if self.recall_facts_limit == 0 {
170 return Err("memory.knowledge.recall_facts_limit must be > 0".to_string());
171 }
172 if self.rooms_limit == 0 {
173 return Err("memory.knowledge.rooms_limit must be > 0".to_string());
174 }
175 if self.timeline_limit == 0 {
176 return Err("memory.knowledge.timeline_limit must be > 0".to_string());
177 }
178 if self.relations_limit == 0 {
179 return Err("memory.knowledge.relations_limit must be > 0".to_string());
180 }
181 Ok(())
182 }
183
184 fn apply_overrides(&mut self, o: &KnowledgePolicyOverrides) {
185 if let Some(v) = o.max_facts {
186 self.max_facts = v;
187 }
188 if let Some(v) = o.max_patterns {
189 self.max_patterns = v;
190 }
191 if let Some(v) = o.max_history {
192 self.max_history = v;
193 }
194 if let Some(v) = o.contradiction_threshold {
195 self.contradiction_threshold = v;
196 }
197 if let Some(v) = o.recall_facts_limit {
198 self.recall_facts_limit = v;
199 }
200 if let Some(v) = o.rooms_limit {
201 self.rooms_limit = v;
202 }
203 if let Some(v) = o.timeline_limit {
204 self.timeline_limit = v;
205 }
206 if let Some(v) = o.relations_limit {
207 self.relations_limit = v;
208 }
209 }
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize)]
213#[serde(default)]
214pub struct EpisodicPolicy {
215 pub max_episodes: usize,
216 pub max_actions_per_episode: usize,
217 pub summary_max_chars: usize,
218}
219
220impl Default for EpisodicPolicy {
221 fn default() -> Self {
222 Self {
223 max_episodes: 500,
224 max_actions_per_episode: 50,
225 summary_max_chars: 200,
226 }
227 }
228}
229
230impl EpisodicPolicy {
231 fn apply_env_overrides(&mut self) {
232 if let Ok(v) = std::env::var("LEAN_CTX_EPISODIC_MAX_EPISODES")
233 && let Ok(n) = v.parse()
234 {
235 self.max_episodes = n;
236 }
237 if let Ok(v) = std::env::var("LEAN_CTX_EPISODIC_MAX_ACTIONS_PER_EPISODE")
238 && let Ok(n) = v.parse()
239 {
240 self.max_actions_per_episode = n;
241 }
242 if let Ok(v) = std::env::var("LEAN_CTX_EPISODIC_SUMMARY_MAX_CHARS")
243 && let Ok(n) = v.parse()
244 {
245 self.summary_max_chars = n;
246 }
247 }
248
249 fn validate(&self) -> Result<(), String> {
250 if self.max_episodes == 0 {
251 return Err("memory.episodic.max_episodes must be > 0".to_string());
252 }
253 if self.max_actions_per_episode == 0 {
254 return Err("memory.episodic.max_actions_per_episode must be > 0".to_string());
255 }
256 if self.summary_max_chars < 40 {
257 return Err("memory.episodic.summary_max_chars must be >= 40".to_string());
258 }
259 Ok(())
260 }
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize)]
264#[serde(default)]
265pub struct ProceduralPolicy {
266 pub min_repetitions: usize,
267 pub min_sequence_len: usize,
268 pub max_procedures: usize,
269 pub max_window_size: usize,
270}
271
272impl Default for ProceduralPolicy {
273 fn default() -> Self {
274 Self {
275 min_repetitions: 3,
276 min_sequence_len: 2,
277 max_procedures: 100,
278 max_window_size: 10,
279 }
280 }
281}
282
283impl ProceduralPolicy {
284 fn apply_env_overrides(&mut self) {
285 if let Ok(v) = std::env::var("LEAN_CTX_PROCEDURAL_MIN_REPETITIONS")
286 && let Ok(n) = v.parse()
287 {
288 self.min_repetitions = n;
289 }
290 if let Ok(v) = std::env::var("LEAN_CTX_PROCEDURAL_MIN_SEQUENCE_LEN")
291 && let Ok(n) = v.parse()
292 {
293 self.min_sequence_len = n;
294 }
295 if let Ok(v) = std::env::var("LEAN_CTX_PROCEDURAL_MAX_PROCEDURES")
296 && let Ok(n) = v.parse()
297 {
298 self.max_procedures = n;
299 }
300 if let Ok(v) = std::env::var("LEAN_CTX_PROCEDURAL_MAX_WINDOW_SIZE")
301 && let Ok(n) = v.parse()
302 {
303 self.max_window_size = n;
304 }
305 }
306
307 fn validate(&self) -> Result<(), String> {
308 if self.min_repetitions == 0 {
309 return Err("memory.procedural.min_repetitions must be > 0".to_string());
310 }
311 if self.min_sequence_len < 2 {
312 return Err("memory.procedural.min_sequence_len must be >= 2".to_string());
313 }
314 if self.max_procedures == 0 {
315 return Err("memory.procedural.max_procedures must be > 0".to_string());
316 }
317 if self.max_window_size < self.min_sequence_len {
318 return Err(
319 "memory.procedural.max_window_size must be >= min_sequence_len".to_string(),
320 );
321 }
322 Ok(())
323 }
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize)]
327#[serde(default)]
328pub struct LifecyclePolicy {
329 pub decay_rate: f32,
330 pub low_confidence_threshold: f32,
331 pub stale_days: i64,
332 pub similarity_threshold: f32,
333 pub forgetting_model: String,
335 pub base_stability_days: f32,
337 pub archetype_aware_decay: bool,
340 pub prune_unretrieved_after_days: Option<i64>,
346}
347
348impl Default for LifecyclePolicy {
349 fn default() -> Self {
350 Self {
351 decay_rate: 0.01,
352 low_confidence_threshold: 0.3,
353 stale_days: 30,
354 similarity_threshold: 0.85,
355 forgetting_model: "ebbinghaus".to_string(),
356 base_stability_days: crate::core::memory_lifecycle::DEFAULT_BASE_STABILITY_DAYS,
357 archetype_aware_decay: false,
358 prune_unretrieved_after_days: Some(90),
359 }
360 }
361}
362
363impl LifecyclePolicy {
364 fn apply_env_overrides(&mut self) {
365 if let Ok(v) = std::env::var("LEAN_CTX_LIFECYCLE_DECAY_RATE")
366 && let Ok(n) = v.parse()
367 {
368 self.decay_rate = n;
369 }
370 if let Ok(v) = std::env::var("LEAN_CTX_LIFECYCLE_LOW_CONFIDENCE_THRESHOLD")
371 && let Ok(n) = v.parse()
372 {
373 self.low_confidence_threshold = n;
374 }
375 if let Ok(v) = std::env::var("LEAN_CTX_LIFECYCLE_STALE_DAYS")
376 && let Ok(n) = v.parse()
377 {
378 self.stale_days = n;
379 }
380 if let Ok(v) = std::env::var("LEAN_CTX_LIFECYCLE_SIMILARITY_THRESHOLD")
381 && let Ok(n) = v.parse()
382 {
383 self.similarity_threshold = n;
384 }
385 if let Ok(v) = std::env::var("LEAN_CTX_LIFECYCLE_FORGETTING") {
386 self.forgetting_model = v;
387 }
388 if let Ok(v) = std::env::var("LEAN_CTX_LIFECYCLE_BASE_STABILITY_DAYS")
389 && let Ok(n) = v.parse()
390 {
391 self.base_stability_days = n;
392 }
393 if let Ok(v) = std::env::var("LEAN_CTX_LIFECYCLE_ARCHETYPE_AWARE") {
394 self.archetype_aware_decay = v == "1" || v.eq_ignore_ascii_case("true");
395 }
396 if let Ok(v) = std::env::var("LEAN_CTX_LIFECYCLE_PRUNE_UNRETRIEVED_DAYS") {
397 self.prune_unretrieved_after_days = match v.trim().to_lowercase().as_str() {
398 "" | "off" | "none" | "0" => None,
399 s => s.parse::<i64>().ok().filter(|&n| n > 0),
400 };
401 }
402 }
403
404 fn validate(&self) -> Result<(), String> {
405 if !(0.0..=1.0).contains(&self.decay_rate) {
406 return Err("memory.lifecycle.decay_rate must be in [0.0, 1.0]".to_string());
407 }
408 if !(0.0..=1.0).contains(&self.low_confidence_threshold) {
409 return Err(
410 "memory.lifecycle.low_confidence_threshold must be in [0.0, 1.0]".to_string(),
411 );
412 }
413 if self.stale_days < 0 {
414 return Err("memory.lifecycle.stale_days must be >= 0".to_string());
415 }
416 if !(0.0..=1.0).contains(&self.similarity_threshold) {
417 return Err("memory.lifecycle.similarity_threshold must be in [0.0, 1.0]".to_string());
418 }
419 if self.base_stability_days <= 0.0 {
420 return Err("memory.lifecycle.base_stability_days must be > 0".to_string());
421 }
422 Ok(())
423 }
424
425 fn apply_overrides(&mut self, o: &LifecyclePolicyOverrides) {
426 if let Some(v) = o.decay_rate {
427 self.decay_rate = v;
428 }
429 if let Some(v) = o.low_confidence_threshold {
430 self.low_confidence_threshold = v;
431 }
432 if let Some(v) = o.stale_days {
433 self.stale_days = v;
434 }
435 if let Some(v) = o.similarity_threshold {
436 self.similarity_threshold = v;
437 }
438 if let Some(ref v) = o.forgetting_model {
439 self.forgetting_model.clone_from(v);
440 }
441 if let Some(v) = o.base_stability_days {
442 self.base_stability_days = v;
443 }
444 if let Some(v) = o.archetype_aware_decay {
445 self.archetype_aware_decay = v;
446 }
447 }
448}
449
450#[derive(Debug, Clone, Serialize, Deserialize)]
451#[serde(default)]
452pub struct EmbeddingsPolicy {
453 pub max_facts: usize,
454}
455
456impl Default for EmbeddingsPolicy {
457 fn default() -> Self {
458 Self { max_facts: 2000 }
459 }
460}
461
462impl EmbeddingsPolicy {
463 fn apply_env_overrides(&mut self) {
464 if let Ok(v) = std::env::var("LEAN_CTX_KNOWLEDGE_EMBEDDINGS_MAX_FACTS")
465 && let Ok(n) = v.parse()
466 {
467 self.max_facts = n;
468 }
469 }
470
471 fn validate(&self) -> Result<(), String> {
472 if self.max_facts == 0 {
473 return Err("memory.embeddings.max_facts must be > 0".to_string());
474 }
475 Ok(())
476 }
477}
478
479use std::collections::HashMap;
480
481#[derive(Debug, Clone, Serialize, Deserialize)]
482#[serde(default)]
483pub struct GotchaPolicy {
484 pub max_gotchas_per_project: usize,
485 pub retrieval_budget_per_room: usize,
486 pub default_decay_rate: f32,
487 pub category_decay_overrides: HashMap<String, f32>,
488 pub auto_expire_days: Option<i64>,
489}
490
491impl Default for GotchaPolicy {
492 fn default() -> Self {
493 Self {
494 max_gotchas_per_project: 100,
495 retrieval_budget_per_room: 10,
496 default_decay_rate: 0.03,
497 category_decay_overrides: HashMap::new(),
498 auto_expire_days: None,
499 }
500 }
501}
502
503impl GotchaPolicy {
504 fn apply_env_overrides(&mut self) {
505 if let Ok(v) = std::env::var("LEAN_CTX_GOTCHA_MAX_PER_PROJECT")
506 && let Ok(n) = v.parse()
507 {
508 self.max_gotchas_per_project = n;
509 }
510 if let Ok(v) = std::env::var("LEAN_CTX_GOTCHA_RETRIEVAL_BUDGET")
511 && let Ok(n) = v.parse()
512 {
513 self.retrieval_budget_per_room = n;
514 }
515 }
516
517 fn validate(&self) -> Result<(), String> {
518 if self.max_gotchas_per_project == 0 {
519 return Err("memory.gotcha.max_gotchas_per_project must be > 0".to_string());
520 }
521 if self.retrieval_budget_per_room == 0 {
522 return Err("memory.gotcha.retrieval_budget_per_room must be > 0".to_string());
523 }
524 if !(0.0..=1.0).contains(&self.default_decay_rate) {
525 return Err("memory.gotcha.default_decay_rate must be 0.0-1.0".to_string());
526 }
527 Ok(())
528 }
529
530 pub fn effective_decay_rate(&self, category: &str) -> f32 {
531 self.category_decay_overrides
532 .get(category)
533 .copied()
534 .unwrap_or(self.default_decay_rate)
535 }
536}
537
538#[derive(Debug, Clone, Serialize, Deserialize)]
544#[serde(default)]
545pub struct AdmissionPolicy {
546 pub enabled: bool,
549 pub auto_merge_similarity: f32,
555 pub min_salience: u32,
559}
560
561impl Default for AdmissionPolicy {
562 fn default() -> Self {
563 Self {
564 enabled: true,
565 auto_merge_similarity: 0.9,
566 min_salience: 0,
567 }
568 }
569}
570
571impl AdmissionPolicy {
572 fn apply_env_overrides(&mut self) {
573 if let Ok(v) = std::env::var("LEAN_CTX_ADMISSION_ENABLED") {
574 self.enabled = !(v == "0" || v.eq_ignore_ascii_case("false"));
575 }
576 if let Ok(v) = std::env::var("LEAN_CTX_ADMISSION_MERGE_SIMILARITY")
577 && let Ok(n) = v.parse()
578 {
579 self.auto_merge_similarity = n;
580 }
581 if let Ok(v) = std::env::var("LEAN_CTX_ADMISSION_MIN_SALIENCE")
582 && let Ok(n) = v.parse()
583 {
584 self.min_salience = n;
585 }
586 }
587
588 fn validate(&self) -> Result<(), String> {
589 if !(0.0..=1.0).contains(&self.auto_merge_similarity) {
590 return Err("memory.admission.auto_merge_similarity must be in [0.0, 1.0]".to_string());
591 }
592 Ok(())
593 }
594}
595
596#[derive(Debug, Clone, Serialize, Deserialize)]
604#[serde(default)]
605pub struct CompactionPolicy {
606 pub enabled: bool,
608 pub min_cluster: usize,
611 pub similarity: f32,
613 pub max_confidence: f32,
616 pub max_confirmations: u32,
619}
620
621impl Default for CompactionPolicy {
622 fn default() -> Self {
623 Self {
624 enabled: true,
625 min_cluster: 4,
626 similarity: 0.5,
627 max_confidence: 0.5,
628 max_confirmations: 1,
629 }
630 }
631}
632
633impl CompactionPolicy {
634 fn apply_env_overrides(&mut self) {
635 if let Ok(v) = std::env::var("LEAN_CTX_COMPACTION_ENABLED") {
636 self.enabled = !(v == "0" || v.eq_ignore_ascii_case("false"));
637 }
638 if let Ok(v) = std::env::var("LEAN_CTX_COMPACTION_MIN_CLUSTER")
639 && let Ok(n) = v.parse()
640 {
641 self.min_cluster = n;
642 }
643 if let Ok(v) = std::env::var("LEAN_CTX_COMPACTION_SIMILARITY")
644 && let Ok(n) = v.parse()
645 {
646 self.similarity = n;
647 }
648 if let Ok(v) = std::env::var("LEAN_CTX_COMPACTION_MAX_CONFIDENCE")
649 && let Ok(n) = v.parse()
650 {
651 self.max_confidence = n;
652 }
653 if let Ok(v) = std::env::var("LEAN_CTX_COMPACTION_MAX_CONFIRMATIONS")
654 && let Ok(n) = v.parse()
655 {
656 self.max_confirmations = n;
657 }
658 }
659
660 fn validate(&self) -> Result<(), String> {
661 if self.min_cluster < 2 {
662 return Err("memory.compaction.min_cluster must be >= 2".to_string());
663 }
664 if !(0.0..=1.0).contains(&self.similarity) {
665 return Err("memory.compaction.similarity must be in [0.0, 1.0]".to_string());
666 }
667 if !(0.0..=1.0).contains(&self.max_confidence) {
668 return Err("memory.compaction.max_confidence must be in [0.0, 1.0]".to_string());
669 }
670 Ok(())
671 }
672}
673
674#[cfg(test)]
675mod tests {
676 use super::*;
677
678 fn restore_env(key: &str, prev: Option<String>) {
679 match prev {
680 Some(v) => crate::test_env::set_var(key, v),
681 None => crate::test_env::remove_var(key),
682 }
683 }
684
685 #[test]
686 fn default_policy_is_valid() {
687 let p = MemoryPolicy::default();
688 p.validate().expect("default policy must be valid");
689 }
690
691 #[test]
692 fn memory_discipline_defaults_are_premium() {
693 let p = MemoryPolicy::default();
695 assert!(p.admission.enabled, "admission on by default");
696 assert_eq!(p.admission.min_salience, 0, "salience floor off (lossless)");
697 assert!(p.compaction.enabled, "cluster compaction on by default");
698 assert!(p.compaction.min_cluster >= 2);
699 assert_eq!(
700 p.lifecycle.prune_unretrieved_after_days,
701 Some(90),
702 "conservative recoverable prune default"
703 );
704 }
705
706 #[test]
707 fn admission_and_compaction_env_overrides_apply() {
708 let _lock = crate::core::data_dir::test_env_lock();
709
710 let prev = [
711 (
712 "LEAN_CTX_ADMISSION_ENABLED",
713 std::env::var("LEAN_CTX_ADMISSION_ENABLED").ok(),
714 ),
715 (
716 "LEAN_CTX_ADMISSION_MIN_SALIENCE",
717 std::env::var("LEAN_CTX_ADMISSION_MIN_SALIENCE").ok(),
718 ),
719 (
720 "LEAN_CTX_COMPACTION_MIN_CLUSTER",
721 std::env::var("LEAN_CTX_COMPACTION_MIN_CLUSTER").ok(),
722 ),
723 ];
724 crate::test_env::set_var("LEAN_CTX_ADMISSION_ENABLED", "0");
725 crate::test_env::set_var("LEAN_CTX_ADMISSION_MIN_SALIENCE", "42");
726 crate::test_env::set_var("LEAN_CTX_COMPACTION_MIN_CLUSTER", "7");
727
728 let mut p = MemoryPolicy::default();
729 p.apply_env_overrides();
730
731 assert!(!p.admission.enabled);
732 assert_eq!(p.admission.min_salience, 42);
733 assert_eq!(p.compaction.min_cluster, 7);
734
735 for (key, val) in prev {
736 restore_env(key, val);
737 }
738 }
739
740 #[test]
741 fn validate_rejects_invalid_compaction() {
742 let mut p = MemoryPolicy::default();
743 p.compaction.min_cluster = 1;
744 assert!(p.validate().is_err());
745
746 let mut p = MemoryPolicy::default();
747 p.admission.auto_merge_similarity = 1.5;
748 assert!(p.validate().is_err());
749 }
750
751 #[test]
752 fn env_overrides_apply() {
753 let _lock = crate::core::data_dir::test_env_lock();
754
755 let prev_facts = std::env::var("LEAN_CTX_KNOWLEDGE_MAX_FACTS").ok();
756 let prev_stale = std::env::var("LEAN_CTX_LIFECYCLE_STALE_DAYS").ok();
757 let prev_rep = std::env::var("LEAN_CTX_PROCEDURAL_MIN_REPETITIONS").ok();
758
759 crate::test_env::set_var("LEAN_CTX_KNOWLEDGE_MAX_FACTS", "123");
760 crate::test_env::set_var("LEAN_CTX_LIFECYCLE_STALE_DAYS", "7");
761 crate::test_env::set_var("LEAN_CTX_PROCEDURAL_MIN_REPETITIONS", "4");
762
763 let mut p = MemoryPolicy::default();
764 p.apply_env_overrides();
765
766 assert_eq!(p.knowledge.max_facts, 123);
767 assert_eq!(p.lifecycle.stale_days, 7);
768 assert_eq!(p.procedural.min_repetitions, 4);
769
770 restore_env("LEAN_CTX_KNOWLEDGE_MAX_FACTS", prev_facts);
771 restore_env("LEAN_CTX_LIFECYCLE_STALE_DAYS", prev_stale);
772 restore_env("LEAN_CTX_PROCEDURAL_MIN_REPETITIONS", prev_rep);
773 }
774
775 #[test]
776 fn validate_rejects_invalid_values() {
777 let mut p = MemoryPolicy::default();
778 p.knowledge.max_facts = 0;
779 assert!(p.validate().is_err());
780
781 let mut p = MemoryPolicy::default();
782 p.lifecycle.decay_rate = 2.0;
783 assert!(p.validate().is_err());
784
785 let mut p = MemoryPolicy::default();
786 p.procedural.min_sequence_len = 1;
787 assert!(p.validate().is_err());
788 }
789}