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 pub reclaim_headroom_pct: f32,
351 pub reclaim_enabled: bool,
355}
356
357impl Default for LifecyclePolicy {
358 fn default() -> Self {
359 Self {
360 decay_rate: 0.01,
361 low_confidence_threshold: 0.3,
362 stale_days: 30,
363 similarity_threshold: 0.85,
364 forgetting_model: "ebbinghaus".to_string(),
365 base_stability_days: crate::core::memory_lifecycle::DEFAULT_BASE_STABILITY_DAYS,
366 archetype_aware_decay: false,
367 prune_unretrieved_after_days: Some(90),
368 reclaim_headroom_pct: crate::core::memory_lifecycle::DEFAULT_RECLAIM_HEADROOM_PCT,
369 reclaim_enabled: true,
370 }
371 }
372}
373
374impl LifecyclePolicy {
375 fn apply_env_overrides(&mut self) {
376 if let Ok(v) = std::env::var("LEAN_CTX_LIFECYCLE_DECAY_RATE")
377 && let Ok(n) = v.parse()
378 {
379 self.decay_rate = n;
380 }
381 if let Ok(v) = std::env::var("LEAN_CTX_LIFECYCLE_LOW_CONFIDENCE_THRESHOLD")
382 && let Ok(n) = v.parse()
383 {
384 self.low_confidence_threshold = n;
385 }
386 if let Ok(v) = std::env::var("LEAN_CTX_LIFECYCLE_STALE_DAYS")
387 && let Ok(n) = v.parse()
388 {
389 self.stale_days = n;
390 }
391 if let Ok(v) = std::env::var("LEAN_CTX_LIFECYCLE_SIMILARITY_THRESHOLD")
392 && let Ok(n) = v.parse()
393 {
394 self.similarity_threshold = n;
395 }
396 if let Ok(v) = std::env::var("LEAN_CTX_LIFECYCLE_FORGETTING") {
397 self.forgetting_model = v;
398 }
399 if let Ok(v) = std::env::var("LEAN_CTX_LIFECYCLE_BASE_STABILITY_DAYS")
400 && let Ok(n) = v.parse()
401 {
402 self.base_stability_days = n;
403 }
404 if let Ok(v) = std::env::var("LEAN_CTX_LIFECYCLE_ARCHETYPE_AWARE") {
405 self.archetype_aware_decay = v == "1" || v.eq_ignore_ascii_case("true");
406 }
407 if let Ok(v) = std::env::var("LEAN_CTX_LIFECYCLE_PRUNE_UNRETRIEVED_DAYS") {
408 self.prune_unretrieved_after_days = match v.trim().to_lowercase().as_str() {
409 "" | "off" | "none" | "0" => None,
410 s => s.parse::<i64>().ok().filter(|&n| n > 0),
411 };
412 }
413 if let Ok(v) = std::env::var("LEAN_CTX_LIFECYCLE_RECLAIM_HEADROOM_PCT")
414 && let Ok(n) = v.parse()
415 {
416 self.reclaim_headroom_pct = n;
417 }
418 if let Ok(v) = std::env::var("LEAN_CTX_LIFECYCLE_RECLAIM_ENABLED") {
419 self.reclaim_enabled = !(v == "0" || v.eq_ignore_ascii_case("false"));
420 }
421 }
422
423 fn validate(&self) -> Result<(), String> {
424 if !(0.0..=1.0).contains(&self.decay_rate) {
425 return Err("memory.lifecycle.decay_rate must be in [0.0, 1.0]".to_string());
426 }
427 if !(0.0..=1.0).contains(&self.low_confidence_threshold) {
428 return Err(
429 "memory.lifecycle.low_confidence_threshold must be in [0.0, 1.0]".to_string(),
430 );
431 }
432 if self.stale_days < 0 {
433 return Err("memory.lifecycle.stale_days must be >= 0".to_string());
434 }
435 if !(0.0..=1.0).contains(&self.similarity_threshold) {
436 return Err("memory.lifecycle.similarity_threshold must be in [0.0, 1.0]".to_string());
437 }
438 if self.base_stability_days <= 0.0 {
439 return Err("memory.lifecycle.base_stability_days must be > 0".to_string());
440 }
441 if !(0.0..=0.95).contains(&self.reclaim_headroom_pct) {
442 return Err("memory.lifecycle.reclaim_headroom_pct must be in [0.0, 0.95]".to_string());
443 }
444 Ok(())
445 }
446
447 fn apply_overrides(&mut self, o: &LifecyclePolicyOverrides) {
448 if let Some(v) = o.decay_rate {
449 self.decay_rate = v;
450 }
451 if let Some(v) = o.low_confidence_threshold {
452 self.low_confidence_threshold = v;
453 }
454 if let Some(v) = o.stale_days {
455 self.stale_days = v;
456 }
457 if let Some(v) = o.similarity_threshold {
458 self.similarity_threshold = v;
459 }
460 if let Some(ref v) = o.forgetting_model {
461 self.forgetting_model.clone_from(v);
462 }
463 if let Some(v) = o.base_stability_days {
464 self.base_stability_days = v;
465 }
466 if let Some(v) = o.archetype_aware_decay {
467 self.archetype_aware_decay = v;
468 }
469 }
470}
471
472#[derive(Debug, Clone, Serialize, Deserialize)]
473#[serde(default)]
474pub struct EmbeddingsPolicy {
475 pub max_facts: usize,
476}
477
478impl Default for EmbeddingsPolicy {
479 fn default() -> Self {
480 Self { max_facts: 2000 }
481 }
482}
483
484impl EmbeddingsPolicy {
485 fn apply_env_overrides(&mut self) {
486 if let Ok(v) = std::env::var("LEAN_CTX_KNOWLEDGE_EMBEDDINGS_MAX_FACTS")
487 && let Ok(n) = v.parse()
488 {
489 self.max_facts = n;
490 }
491 }
492
493 fn validate(&self) -> Result<(), String> {
494 if self.max_facts == 0 {
495 return Err("memory.embeddings.max_facts must be > 0".to_string());
496 }
497 Ok(())
498 }
499}
500
501use std::collections::HashMap;
502
503#[derive(Debug, Clone, Serialize, Deserialize)]
504#[serde(default)]
505pub struct GotchaPolicy {
506 pub max_gotchas_per_project: usize,
507 pub retrieval_budget_per_room: usize,
508 pub default_decay_rate: f32,
509 pub category_decay_overrides: HashMap<String, f32>,
510 pub auto_expire_days: Option<i64>,
511}
512
513impl Default for GotchaPolicy {
514 fn default() -> Self {
515 Self {
516 max_gotchas_per_project: 100,
517 retrieval_budget_per_room: 10,
518 default_decay_rate: 0.03,
519 category_decay_overrides: HashMap::new(),
520 auto_expire_days: None,
521 }
522 }
523}
524
525impl GotchaPolicy {
526 fn apply_env_overrides(&mut self) {
527 if let Ok(v) = std::env::var("LEAN_CTX_GOTCHA_MAX_PER_PROJECT")
528 && let Ok(n) = v.parse()
529 {
530 self.max_gotchas_per_project = n;
531 }
532 if let Ok(v) = std::env::var("LEAN_CTX_GOTCHA_RETRIEVAL_BUDGET")
533 && let Ok(n) = v.parse()
534 {
535 self.retrieval_budget_per_room = n;
536 }
537 }
538
539 fn validate(&self) -> Result<(), String> {
540 if self.max_gotchas_per_project == 0 {
541 return Err("memory.gotcha.max_gotchas_per_project must be > 0".to_string());
542 }
543 if self.retrieval_budget_per_room == 0 {
544 return Err("memory.gotcha.retrieval_budget_per_room must be > 0".to_string());
545 }
546 if !(0.0..=1.0).contains(&self.default_decay_rate) {
547 return Err("memory.gotcha.default_decay_rate must be 0.0-1.0".to_string());
548 }
549 Ok(())
550 }
551
552 pub fn effective_decay_rate(&self, category: &str) -> f32 {
553 self.category_decay_overrides
554 .get(category)
555 .copied()
556 .unwrap_or(self.default_decay_rate)
557 }
558}
559
560#[derive(Debug, Clone, Serialize, Deserialize)]
566#[serde(default)]
567pub struct AdmissionPolicy {
568 pub enabled: bool,
571 pub auto_merge_similarity: f32,
577 pub min_salience: u32,
581}
582
583impl Default for AdmissionPolicy {
584 fn default() -> Self {
585 Self {
586 enabled: true,
587 auto_merge_similarity: 0.9,
588 min_salience: 0,
589 }
590 }
591}
592
593impl AdmissionPolicy {
594 fn apply_env_overrides(&mut self) {
595 if let Ok(v) = std::env::var("LEAN_CTX_ADMISSION_ENABLED") {
596 self.enabled = !(v == "0" || v.eq_ignore_ascii_case("false"));
597 }
598 if let Ok(v) = std::env::var("LEAN_CTX_ADMISSION_MERGE_SIMILARITY")
599 && let Ok(n) = v.parse()
600 {
601 self.auto_merge_similarity = n;
602 }
603 if let Ok(v) = std::env::var("LEAN_CTX_ADMISSION_MIN_SALIENCE")
604 && let Ok(n) = v.parse()
605 {
606 self.min_salience = n;
607 }
608 }
609
610 fn validate(&self) -> Result<(), String> {
611 if !(0.0..=1.0).contains(&self.auto_merge_similarity) {
612 return Err("memory.admission.auto_merge_similarity must be in [0.0, 1.0]".to_string());
613 }
614 Ok(())
615 }
616}
617
618#[derive(Debug, Clone, Serialize, Deserialize)]
626#[serde(default)]
627pub struct CompactionPolicy {
628 pub enabled: bool,
630 pub min_cluster: usize,
633 pub similarity: f32,
635 pub max_confidence: f32,
638 pub max_confirmations: u32,
641}
642
643impl Default for CompactionPolicy {
644 fn default() -> Self {
645 Self {
646 enabled: true,
647 min_cluster: 4,
648 similarity: 0.5,
649 max_confidence: 0.5,
650 max_confirmations: 1,
651 }
652 }
653}
654
655impl CompactionPolicy {
656 fn apply_env_overrides(&mut self) {
657 if let Ok(v) = std::env::var("LEAN_CTX_COMPACTION_ENABLED") {
658 self.enabled = !(v == "0" || v.eq_ignore_ascii_case("false"));
659 }
660 if let Ok(v) = std::env::var("LEAN_CTX_COMPACTION_MIN_CLUSTER")
661 && let Ok(n) = v.parse()
662 {
663 self.min_cluster = n;
664 }
665 if let Ok(v) = std::env::var("LEAN_CTX_COMPACTION_SIMILARITY")
666 && let Ok(n) = v.parse()
667 {
668 self.similarity = n;
669 }
670 if let Ok(v) = std::env::var("LEAN_CTX_COMPACTION_MAX_CONFIDENCE")
671 && let Ok(n) = v.parse()
672 {
673 self.max_confidence = n;
674 }
675 if let Ok(v) = std::env::var("LEAN_CTX_COMPACTION_MAX_CONFIRMATIONS")
676 && let Ok(n) = v.parse()
677 {
678 self.max_confirmations = n;
679 }
680 }
681
682 fn validate(&self) -> Result<(), String> {
683 if self.min_cluster < 2 {
684 return Err("memory.compaction.min_cluster must be >= 2".to_string());
685 }
686 if !(0.0..=1.0).contains(&self.similarity) {
687 return Err("memory.compaction.similarity must be in [0.0, 1.0]".to_string());
688 }
689 if !(0.0..=1.0).contains(&self.max_confidence) {
690 return Err("memory.compaction.max_confidence must be in [0.0, 1.0]".to_string());
691 }
692 Ok(())
693 }
694}
695
696#[cfg(test)]
697mod tests {
698 use super::*;
699
700 fn restore_env(key: &str, prev: Option<String>) {
701 match prev {
702 Some(v) => crate::test_env::set_var(key, v),
703 None => crate::test_env::remove_var(key),
704 }
705 }
706
707 #[test]
708 fn default_policy_is_valid() {
709 let p = MemoryPolicy::default();
710 p.validate().expect("default policy must be valid");
711 }
712
713 #[test]
714 fn memory_discipline_defaults_are_premium() {
715 let p = MemoryPolicy::default();
717 assert!(p.admission.enabled, "admission on by default");
718 assert_eq!(p.admission.min_salience, 0, "salience floor off (lossless)");
719 assert!(p.compaction.enabled, "cluster compaction on by default");
720 assert!(p.compaction.min_cluster >= 2);
721 assert_eq!(
722 p.lifecycle.prune_unretrieved_after_days,
723 Some(90),
724 "conservative recoverable prune default"
725 );
726 }
727
728 #[test]
729 fn admission_and_compaction_env_overrides_apply() {
730 let _lock = crate::core::data_dir::test_env_lock();
731
732 let prev = [
733 (
734 "LEAN_CTX_ADMISSION_ENABLED",
735 std::env::var("LEAN_CTX_ADMISSION_ENABLED").ok(),
736 ),
737 (
738 "LEAN_CTX_ADMISSION_MIN_SALIENCE",
739 std::env::var("LEAN_CTX_ADMISSION_MIN_SALIENCE").ok(),
740 ),
741 (
742 "LEAN_CTX_COMPACTION_MIN_CLUSTER",
743 std::env::var("LEAN_CTX_COMPACTION_MIN_CLUSTER").ok(),
744 ),
745 ];
746 crate::test_env::set_var("LEAN_CTX_ADMISSION_ENABLED", "0");
747 crate::test_env::set_var("LEAN_CTX_ADMISSION_MIN_SALIENCE", "42");
748 crate::test_env::set_var("LEAN_CTX_COMPACTION_MIN_CLUSTER", "7");
749
750 let mut p = MemoryPolicy::default();
751 p.apply_env_overrides();
752
753 assert!(!p.admission.enabled);
754 assert_eq!(p.admission.min_salience, 42);
755 assert_eq!(p.compaction.min_cluster, 7);
756
757 for (key, val) in prev {
758 restore_env(key, val);
759 }
760 }
761
762 #[test]
763 fn validate_rejects_invalid_compaction() {
764 let mut p = MemoryPolicy::default();
765 p.compaction.min_cluster = 1;
766 assert!(p.validate().is_err());
767
768 let mut p = MemoryPolicy::default();
769 p.admission.auto_merge_similarity = 1.5;
770 assert!(p.validate().is_err());
771 }
772
773 #[test]
774 fn env_overrides_apply() {
775 let _lock = crate::core::data_dir::test_env_lock();
776
777 let prev_facts = std::env::var("LEAN_CTX_KNOWLEDGE_MAX_FACTS").ok();
778 let prev_stale = std::env::var("LEAN_CTX_LIFECYCLE_STALE_DAYS").ok();
779 let prev_rep = std::env::var("LEAN_CTX_PROCEDURAL_MIN_REPETITIONS").ok();
780
781 crate::test_env::set_var("LEAN_CTX_KNOWLEDGE_MAX_FACTS", "123");
782 crate::test_env::set_var("LEAN_CTX_LIFECYCLE_STALE_DAYS", "7");
783 crate::test_env::set_var("LEAN_CTX_PROCEDURAL_MIN_REPETITIONS", "4");
784
785 let mut p = MemoryPolicy::default();
786 p.apply_env_overrides();
787
788 assert_eq!(p.knowledge.max_facts, 123);
789 assert_eq!(p.lifecycle.stale_days, 7);
790 assert_eq!(p.procedural.min_repetitions, 4);
791
792 restore_env("LEAN_CTX_KNOWLEDGE_MAX_FACTS", prev_facts);
793 restore_env("LEAN_CTX_LIFECYCLE_STALE_DAYS", prev_stale);
794 restore_env("LEAN_CTX_PROCEDURAL_MIN_REPETITIONS", prev_rep);
795 }
796
797 #[test]
798 fn validate_rejects_invalid_values() {
799 let mut p = MemoryPolicy::default();
800 p.knowledge.max_facts = 0;
801 assert!(p.validate().is_err());
802
803 let mut p = MemoryPolicy::default();
804 p.lifecycle.decay_rate = 2.0;
805 assert!(p.validate().is_err());
806
807 let mut p = MemoryPolicy::default();
808 p.procedural.min_sequence_len = 1;
809 assert!(p.validate().is_err());
810 }
811}