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}
13
14impl MemoryPolicy {
15 pub fn apply_env_overrides(&mut self) {
16 self.knowledge.apply_env_overrides();
17 self.episodic.apply_env_overrides();
18 self.procedural.apply_env_overrides();
19 self.lifecycle.apply_env_overrides();
20 self.embeddings.apply_env_overrides();
21 self.gotcha.apply_env_overrides();
22 }
23
24 pub fn apply_overrides(&mut self, o: &MemoryPolicyOverrides) {
25 self.knowledge.apply_overrides(&o.knowledge);
26 self.lifecycle.apply_overrides(&o.lifecycle);
27 }
28
29 pub fn validate(&self) -> Result<(), String> {
30 self.knowledge.validate()?;
31 self.episodic.validate()?;
32 self.procedural.validate()?;
33 self.lifecycle.validate()?;
34 self.embeddings.validate()?;
35 self.gotcha.validate()?;
36 Ok(())
37 }
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize, Default)]
41#[serde(default)]
42pub struct MemoryPolicyOverrides {
43 pub knowledge: KnowledgePolicyOverrides,
44 pub lifecycle: LifecyclePolicyOverrides,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize, Default)]
48#[serde(default)]
49pub struct KnowledgePolicyOverrides {
50 pub max_facts: Option<usize>,
51 pub max_patterns: Option<usize>,
52 pub max_history: Option<usize>,
53 pub contradiction_threshold: Option<f32>,
54 pub recall_facts_limit: Option<usize>,
55 pub rooms_limit: Option<usize>,
56 pub timeline_limit: Option<usize>,
57 pub relations_limit: Option<usize>,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize, Default)]
61#[serde(default)]
62pub struct LifecyclePolicyOverrides {
63 pub decay_rate: Option<f32>,
64 pub low_confidence_threshold: Option<f32>,
65 pub stale_days: Option<i64>,
66 pub similarity_threshold: Option<f32>,
67 pub forgetting_model: Option<String>,
68 pub base_stability_days: Option<f32>,
69 pub archetype_aware_decay: Option<bool>,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
73#[serde(default)]
74pub struct KnowledgePolicy {
75 pub max_facts: usize,
76 pub max_patterns: usize,
77 pub max_history: usize,
78 pub contradiction_threshold: f32,
79 pub recall_facts_limit: usize,
81 pub rooms_limit: usize,
83 pub timeline_limit: usize,
85 pub relations_limit: usize,
87}
88
89impl Default for KnowledgePolicy {
90 fn default() -> Self {
91 Self {
92 max_facts: 200,
93 max_patterns: 50,
94 max_history: 100,
95 contradiction_threshold: 0.5,
96 recall_facts_limit: crate::core::budgets::KNOWLEDGE_RECALL_FACTS_LIMIT,
97 rooms_limit: crate::core::budgets::KNOWLEDGE_ROOMS_LIMIT,
98 timeline_limit: crate::core::budgets::KNOWLEDGE_TIMELINE_LIMIT,
99 relations_limit: 40,
100 }
101 }
102}
103
104impl KnowledgePolicy {
105 fn apply_env_overrides(&mut self) {
106 if let Ok(v) = std::env::var("LEAN_CTX_KNOWLEDGE_MAX_FACTS")
107 && let Ok(n) = v.parse()
108 {
109 self.max_facts = n;
110 }
111 if let Ok(v) = std::env::var("LEAN_CTX_KNOWLEDGE_MAX_PATTERNS")
112 && let Ok(n) = v.parse()
113 {
114 self.max_patterns = n;
115 }
116 if let Ok(v) = std::env::var("LEAN_CTX_KNOWLEDGE_MAX_HISTORY")
117 && let Ok(n) = v.parse()
118 {
119 self.max_history = n;
120 }
121 if let Ok(v) = std::env::var("LEAN_CTX_KNOWLEDGE_CONTRADICTION_THRESHOLD")
122 && let Ok(n) = v.parse()
123 {
124 self.contradiction_threshold = n;
125 }
126 if let Ok(v) = std::env::var("LEAN_CTX_KNOWLEDGE_RECALL_FACTS_LIMIT")
127 && let Ok(n) = v.parse()
128 {
129 self.recall_facts_limit = n;
130 }
131 if let Ok(v) = std::env::var("LEAN_CTX_KNOWLEDGE_ROOMS_LIMIT")
132 && let Ok(n) = v.parse()
133 {
134 self.rooms_limit = n;
135 }
136 if let Ok(v) = std::env::var("LEAN_CTX_KNOWLEDGE_TIMELINE_LIMIT")
137 && let Ok(n) = v.parse()
138 {
139 self.timeline_limit = n;
140 }
141 if let Ok(v) = std::env::var("LEAN_CTX_KNOWLEDGE_RELATIONS_LIMIT")
142 && let Ok(n) = v.parse()
143 {
144 self.relations_limit = n;
145 }
146 }
147
148 fn validate(&self) -> Result<(), String> {
149 if self.max_facts == 0 {
150 return Err("memory.knowledge.max_facts must be > 0".to_string());
151 }
152 if self.max_patterns == 0 {
153 return Err("memory.knowledge.max_patterns must be > 0".to_string());
154 }
155 if self.max_history == 0 {
156 return Err("memory.knowledge.max_history must be > 0".to_string());
157 }
158 if !(0.0..=1.0).contains(&self.contradiction_threshold) {
159 return Err(
160 "memory.knowledge.contradiction_threshold must be in [0.0, 1.0]".to_string(),
161 );
162 }
163 if self.recall_facts_limit == 0 {
164 return Err("memory.knowledge.recall_facts_limit must be > 0".to_string());
165 }
166 if self.rooms_limit == 0 {
167 return Err("memory.knowledge.rooms_limit must be > 0".to_string());
168 }
169 if self.timeline_limit == 0 {
170 return Err("memory.knowledge.timeline_limit must be > 0".to_string());
171 }
172 if self.relations_limit == 0 {
173 return Err("memory.knowledge.relations_limit must be > 0".to_string());
174 }
175 Ok(())
176 }
177
178 fn apply_overrides(&mut self, o: &KnowledgePolicyOverrides) {
179 if let Some(v) = o.max_facts {
180 self.max_facts = v;
181 }
182 if let Some(v) = o.max_patterns {
183 self.max_patterns = v;
184 }
185 if let Some(v) = o.max_history {
186 self.max_history = v;
187 }
188 if let Some(v) = o.contradiction_threshold {
189 self.contradiction_threshold = v;
190 }
191 if let Some(v) = o.recall_facts_limit {
192 self.recall_facts_limit = v;
193 }
194 if let Some(v) = o.rooms_limit {
195 self.rooms_limit = v;
196 }
197 if let Some(v) = o.timeline_limit {
198 self.timeline_limit = v;
199 }
200 if let Some(v) = o.relations_limit {
201 self.relations_limit = v;
202 }
203 }
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize)]
207#[serde(default)]
208pub struct EpisodicPolicy {
209 pub max_episodes: usize,
210 pub max_actions_per_episode: usize,
211 pub summary_max_chars: usize,
212}
213
214impl Default for EpisodicPolicy {
215 fn default() -> Self {
216 Self {
217 max_episodes: 500,
218 max_actions_per_episode: 50,
219 summary_max_chars: 200,
220 }
221 }
222}
223
224impl EpisodicPolicy {
225 fn apply_env_overrides(&mut self) {
226 if let Ok(v) = std::env::var("LEAN_CTX_EPISODIC_MAX_EPISODES")
227 && let Ok(n) = v.parse()
228 {
229 self.max_episodes = n;
230 }
231 if let Ok(v) = std::env::var("LEAN_CTX_EPISODIC_MAX_ACTIONS_PER_EPISODE")
232 && let Ok(n) = v.parse()
233 {
234 self.max_actions_per_episode = n;
235 }
236 if let Ok(v) = std::env::var("LEAN_CTX_EPISODIC_SUMMARY_MAX_CHARS")
237 && let Ok(n) = v.parse()
238 {
239 self.summary_max_chars = n;
240 }
241 }
242
243 fn validate(&self) -> Result<(), String> {
244 if self.max_episodes == 0 {
245 return Err("memory.episodic.max_episodes must be > 0".to_string());
246 }
247 if self.max_actions_per_episode == 0 {
248 return Err("memory.episodic.max_actions_per_episode must be > 0".to_string());
249 }
250 if self.summary_max_chars < 40 {
251 return Err("memory.episodic.summary_max_chars must be >= 40".to_string());
252 }
253 Ok(())
254 }
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize)]
258#[serde(default)]
259pub struct ProceduralPolicy {
260 pub min_repetitions: usize,
261 pub min_sequence_len: usize,
262 pub max_procedures: usize,
263 pub max_window_size: usize,
264}
265
266impl Default for ProceduralPolicy {
267 fn default() -> Self {
268 Self {
269 min_repetitions: 3,
270 min_sequence_len: 2,
271 max_procedures: 100,
272 max_window_size: 10,
273 }
274 }
275}
276
277impl ProceduralPolicy {
278 fn apply_env_overrides(&mut self) {
279 if let Ok(v) = std::env::var("LEAN_CTX_PROCEDURAL_MIN_REPETITIONS")
280 && let Ok(n) = v.parse()
281 {
282 self.min_repetitions = n;
283 }
284 if let Ok(v) = std::env::var("LEAN_CTX_PROCEDURAL_MIN_SEQUENCE_LEN")
285 && let Ok(n) = v.parse()
286 {
287 self.min_sequence_len = n;
288 }
289 if let Ok(v) = std::env::var("LEAN_CTX_PROCEDURAL_MAX_PROCEDURES")
290 && let Ok(n) = v.parse()
291 {
292 self.max_procedures = n;
293 }
294 if let Ok(v) = std::env::var("LEAN_CTX_PROCEDURAL_MAX_WINDOW_SIZE")
295 && let Ok(n) = v.parse()
296 {
297 self.max_window_size = n;
298 }
299 }
300
301 fn validate(&self) -> Result<(), String> {
302 if self.min_repetitions == 0 {
303 return Err("memory.procedural.min_repetitions must be > 0".to_string());
304 }
305 if self.min_sequence_len < 2 {
306 return Err("memory.procedural.min_sequence_len must be >= 2".to_string());
307 }
308 if self.max_procedures == 0 {
309 return Err("memory.procedural.max_procedures must be > 0".to_string());
310 }
311 if self.max_window_size < self.min_sequence_len {
312 return Err(
313 "memory.procedural.max_window_size must be >= min_sequence_len".to_string(),
314 );
315 }
316 Ok(())
317 }
318}
319
320#[derive(Debug, Clone, Serialize, Deserialize)]
321#[serde(default)]
322pub struct LifecyclePolicy {
323 pub decay_rate: f32,
324 pub low_confidence_threshold: f32,
325 pub stale_days: i64,
326 pub similarity_threshold: f32,
327 pub forgetting_model: String,
329 pub base_stability_days: f32,
331 pub archetype_aware_decay: bool,
334}
335
336impl Default for LifecyclePolicy {
337 fn default() -> Self {
338 Self {
339 decay_rate: 0.01,
340 low_confidence_threshold: 0.3,
341 stale_days: 30,
342 similarity_threshold: 0.85,
343 forgetting_model: "ebbinghaus".to_string(),
344 base_stability_days: crate::core::memory_lifecycle::DEFAULT_BASE_STABILITY_DAYS,
345 archetype_aware_decay: false,
346 }
347 }
348}
349
350impl LifecyclePolicy {
351 fn apply_env_overrides(&mut self) {
352 if let Ok(v) = std::env::var("LEAN_CTX_LIFECYCLE_DECAY_RATE")
353 && let Ok(n) = v.parse()
354 {
355 self.decay_rate = n;
356 }
357 if let Ok(v) = std::env::var("LEAN_CTX_LIFECYCLE_LOW_CONFIDENCE_THRESHOLD")
358 && let Ok(n) = v.parse()
359 {
360 self.low_confidence_threshold = n;
361 }
362 if let Ok(v) = std::env::var("LEAN_CTX_LIFECYCLE_STALE_DAYS")
363 && let Ok(n) = v.parse()
364 {
365 self.stale_days = n;
366 }
367 if let Ok(v) = std::env::var("LEAN_CTX_LIFECYCLE_SIMILARITY_THRESHOLD")
368 && let Ok(n) = v.parse()
369 {
370 self.similarity_threshold = n;
371 }
372 if let Ok(v) = std::env::var("LEAN_CTX_LIFECYCLE_FORGETTING") {
373 self.forgetting_model = v;
374 }
375 if let Ok(v) = std::env::var("LEAN_CTX_LIFECYCLE_BASE_STABILITY_DAYS")
376 && let Ok(n) = v.parse()
377 {
378 self.base_stability_days = n;
379 }
380 if let Ok(v) = std::env::var("LEAN_CTX_LIFECYCLE_ARCHETYPE_AWARE") {
381 self.archetype_aware_decay = v == "1" || v.eq_ignore_ascii_case("true");
382 }
383 }
384
385 fn validate(&self) -> Result<(), String> {
386 if !(0.0..=1.0).contains(&self.decay_rate) {
387 return Err("memory.lifecycle.decay_rate must be in [0.0, 1.0]".to_string());
388 }
389 if !(0.0..=1.0).contains(&self.low_confidence_threshold) {
390 return Err(
391 "memory.lifecycle.low_confidence_threshold must be in [0.0, 1.0]".to_string(),
392 );
393 }
394 if self.stale_days < 0 {
395 return Err("memory.lifecycle.stale_days must be >= 0".to_string());
396 }
397 if !(0.0..=1.0).contains(&self.similarity_threshold) {
398 return Err("memory.lifecycle.similarity_threshold must be in [0.0, 1.0]".to_string());
399 }
400 if self.base_stability_days <= 0.0 {
401 return Err("memory.lifecycle.base_stability_days must be > 0".to_string());
402 }
403 Ok(())
404 }
405
406 fn apply_overrides(&mut self, o: &LifecyclePolicyOverrides) {
407 if let Some(v) = o.decay_rate {
408 self.decay_rate = v;
409 }
410 if let Some(v) = o.low_confidence_threshold {
411 self.low_confidence_threshold = v;
412 }
413 if let Some(v) = o.stale_days {
414 self.stale_days = v;
415 }
416 if let Some(v) = o.similarity_threshold {
417 self.similarity_threshold = v;
418 }
419 if let Some(ref v) = o.forgetting_model {
420 self.forgetting_model.clone_from(v);
421 }
422 if let Some(v) = o.base_stability_days {
423 self.base_stability_days = v;
424 }
425 if let Some(v) = o.archetype_aware_decay {
426 self.archetype_aware_decay = v;
427 }
428 }
429}
430
431#[derive(Debug, Clone, Serialize, Deserialize)]
432#[serde(default)]
433pub struct EmbeddingsPolicy {
434 pub max_facts: usize,
435}
436
437impl Default for EmbeddingsPolicy {
438 fn default() -> Self {
439 Self { max_facts: 2000 }
440 }
441}
442
443impl EmbeddingsPolicy {
444 fn apply_env_overrides(&mut self) {
445 if let Ok(v) = std::env::var("LEAN_CTX_KNOWLEDGE_EMBEDDINGS_MAX_FACTS")
446 && let Ok(n) = v.parse()
447 {
448 self.max_facts = n;
449 }
450 }
451
452 fn validate(&self) -> Result<(), String> {
453 if self.max_facts == 0 {
454 return Err("memory.embeddings.max_facts must be > 0".to_string());
455 }
456 Ok(())
457 }
458}
459
460use std::collections::HashMap;
461
462#[derive(Debug, Clone, Serialize, Deserialize)]
463#[serde(default)]
464pub struct GotchaPolicy {
465 pub max_gotchas_per_project: usize,
466 pub retrieval_budget_per_room: usize,
467 pub default_decay_rate: f32,
468 pub category_decay_overrides: HashMap<String, f32>,
469 pub auto_expire_days: Option<i64>,
470}
471
472impl Default for GotchaPolicy {
473 fn default() -> Self {
474 Self {
475 max_gotchas_per_project: 100,
476 retrieval_budget_per_room: 10,
477 default_decay_rate: 0.03,
478 category_decay_overrides: HashMap::new(),
479 auto_expire_days: None,
480 }
481 }
482}
483
484impl GotchaPolicy {
485 fn apply_env_overrides(&mut self) {
486 if let Ok(v) = std::env::var("LEAN_CTX_GOTCHA_MAX_PER_PROJECT")
487 && let Ok(n) = v.parse()
488 {
489 self.max_gotchas_per_project = n;
490 }
491 if let Ok(v) = std::env::var("LEAN_CTX_GOTCHA_RETRIEVAL_BUDGET")
492 && let Ok(n) = v.parse()
493 {
494 self.retrieval_budget_per_room = n;
495 }
496 }
497
498 fn validate(&self) -> Result<(), String> {
499 if self.max_gotchas_per_project == 0 {
500 return Err("memory.gotcha.max_gotchas_per_project must be > 0".to_string());
501 }
502 if self.retrieval_budget_per_room == 0 {
503 return Err("memory.gotcha.retrieval_budget_per_room must be > 0".to_string());
504 }
505 if !(0.0..=1.0).contains(&self.default_decay_rate) {
506 return Err("memory.gotcha.default_decay_rate must be 0.0-1.0".to_string());
507 }
508 Ok(())
509 }
510
511 pub fn effective_decay_rate(&self, category: &str) -> f32 {
512 self.category_decay_overrides
513 .get(category)
514 .copied()
515 .unwrap_or(self.default_decay_rate)
516 }
517}
518
519#[cfg(test)]
520mod tests {
521 use super::*;
522
523 fn restore_env(key: &str, prev: Option<String>) {
524 match prev {
525 Some(v) => crate::test_env::set_var(key, v),
526 None => crate::test_env::remove_var(key),
527 }
528 }
529
530 #[test]
531 fn default_policy_is_valid() {
532 let p = MemoryPolicy::default();
533 p.validate().expect("default policy must be valid");
534 }
535
536 #[test]
537 fn env_overrides_apply() {
538 let _lock = crate::core::data_dir::test_env_lock();
539
540 let prev_facts = std::env::var("LEAN_CTX_KNOWLEDGE_MAX_FACTS").ok();
541 let prev_stale = std::env::var("LEAN_CTX_LIFECYCLE_STALE_DAYS").ok();
542 let prev_rep = std::env::var("LEAN_CTX_PROCEDURAL_MIN_REPETITIONS").ok();
543
544 crate::test_env::set_var("LEAN_CTX_KNOWLEDGE_MAX_FACTS", "123");
545 crate::test_env::set_var("LEAN_CTX_LIFECYCLE_STALE_DAYS", "7");
546 crate::test_env::set_var("LEAN_CTX_PROCEDURAL_MIN_REPETITIONS", "4");
547
548 let mut p = MemoryPolicy::default();
549 p.apply_env_overrides();
550
551 assert_eq!(p.knowledge.max_facts, 123);
552 assert_eq!(p.lifecycle.stale_days, 7);
553 assert_eq!(p.procedural.min_repetitions, 4);
554
555 restore_env("LEAN_CTX_KNOWLEDGE_MAX_FACTS", prev_facts);
556 restore_env("LEAN_CTX_LIFECYCLE_STALE_DAYS", prev_stale);
557 restore_env("LEAN_CTX_PROCEDURAL_MIN_REPETITIONS", prev_rep);
558 }
559
560 #[test]
561 fn validate_rejects_invalid_values() {
562 let mut p = MemoryPolicy::default();
563 p.knowledge.max_facts = 0;
564 assert!(p.validate().is_err());
565
566 let mut p = MemoryPolicy::default();
567 p.lifecycle.decay_rate = 2.0;
568 assert!(p.validate().is_err());
569
570 let mut p = MemoryPolicy::default();
571 p.procedural.min_sequence_len = 1;
572 assert!(p.validate().is_err());
573 }
574}