Skip to main content

mentedb_core/
config.rs

1//! Configuration types for MenteDB.
2
3use serde::Deserialize;
4use std::path::Path;
5
6use crate::error::{MenteError, MenteResult};
7
8/// Top-level configuration for a MenteDB instance.
9#[derive(Debug, Clone, Deserialize, Default)]
10pub struct MenteConfig {
11    /// Storage engine configuration.
12    #[serde(default)]
13    pub storage: StorageConfig,
14    /// Index layer configuration.
15    #[serde(default)]
16    pub index: IndexConfig,
17    /// Context assembly configuration.
18    #[serde(default)]
19    pub context: ContextConfig,
20    /// Cognitive engine configuration.
21    #[serde(default)]
22    pub cognitive: CognitiveConfig,
23    /// Memory consolidation configuration.
24    #[serde(default)]
25    pub consolidation: ConsolidationConfig,
26    /// Server configuration.
27    #[serde(default)]
28    pub server: ServerConfig,
29}
30
31/// Storage engine settings.
32#[derive(Debug, Clone, Deserialize)]
33pub struct StorageConfig {
34    /// Directory for data files.
35    pub data_dir: String,
36    /// Bytes of decoded memory records the storage engine keeps in memory per
37    /// open database. Each record is charged its encoded size (about 5 KB for
38    /// a memory with a 1024 dimension embedding). A host with spare memory
39    /// can set this to the database size so every scan after the first is
40    /// served from memory instead of the file system.
41    #[serde(default = "default_cache_bytes")]
42    pub cache_bytes: u64,
43    /// The WAL is checkpointed (record file synced, record map persisted, log
44    /// truncated) once it grows past this many bytes.
45    #[serde(default = "default_wal_checkpoint_bytes")]
46    pub wal_checkpoint_bytes: u64,
47    /// Largest encoded memory record accepted; bigger writes fail with
48    /// `CapacityExceeded`.
49    #[serde(default = "default_max_record_bytes")]
50    pub max_record_bytes: u64,
51}
52
53fn default_cache_bytes() -> u64 {
54    64 * 1024 * 1024
55}
56fn default_wal_checkpoint_bytes() -> u64 {
57    8 * 1024 * 1024
58}
59fn default_max_record_bytes() -> u64 {
60    16 * 1024 * 1024
61}
62
63impl Default for StorageConfig {
64    fn default() -> Self {
65        Self {
66            data_dir: "data".to_string(),
67            cache_bytes: default_cache_bytes(),
68            wal_checkpoint_bytes: default_wal_checkpoint_bytes(),
69            max_record_bytes: default_max_record_bytes(),
70        }
71    }
72}
73
74/// HNSW index settings.
75#[derive(Debug, Clone, Deserialize)]
76pub struct IndexConfig {
77    /// Number of bidirectional links per node.
78    #[serde(default = "default_hnsw_m")]
79    pub hnsw_m: usize,
80    /// Size of the dynamic candidate list during construction.
81    #[serde(default = "default_hnsw_ef_construction")]
82    pub hnsw_ef_construction: usize,
83    /// Size of the dynamic candidate list during search.
84    #[serde(default = "default_hnsw_ef_search")]
85    pub hnsw_ef_search: usize,
86}
87
88fn default_hnsw_m() -> usize {
89    16
90}
91fn default_hnsw_ef_construction() -> usize {
92    200
93}
94fn default_hnsw_ef_search() -> usize {
95    50
96}
97
98impl Default for IndexConfig {
99    fn default() -> Self {
100        Self {
101            hnsw_m: default_hnsw_m(),
102            hnsw_ef_construction: default_hnsw_ef_construction(),
103            hnsw_ef_search: default_hnsw_ef_search(),
104        }
105    }
106}
107
108/// Context assembly settings.
109#[derive(Debug, Clone, Deserialize)]
110pub struct ContextConfig {
111    /// Default token budget for context windows.
112    #[serde(default = "default_token_budget")]
113    pub default_token_budget: usize,
114    /// Multiplier for estimating token counts from word counts.
115    #[serde(default = "default_token_multiplier")]
116    pub token_multiplier: f32,
117    /// Fraction of budget for the system zone.
118    #[serde(default = "default_zone_system_pct")]
119    pub zone_system_pct: f32,
120    /// Fraction of budget for the critical zone.
121    #[serde(default = "default_zone_critical_pct")]
122    pub zone_critical_pct: f32,
123    /// Fraction of budget for the primary zone.
124    #[serde(default = "default_zone_primary_pct")]
125    pub zone_primary_pct: f32,
126    /// Fraction of budget for the supporting zone.
127    #[serde(default = "default_zone_supporting_pct")]
128    pub zone_supporting_pct: f32,
129    /// Fraction of budget for the reference zone.
130    #[serde(default = "default_zone_reference_pct")]
131    pub zone_reference_pct: f32,
132}
133
134fn default_token_budget() -> usize {
135    4096
136}
137fn default_token_multiplier() -> f32 {
138    1.3
139}
140fn default_zone_system_pct() -> f32 {
141    0.10
142}
143fn default_zone_critical_pct() -> f32 {
144    0.25
145}
146fn default_zone_primary_pct() -> f32 {
147    0.35
148}
149fn default_zone_supporting_pct() -> f32 {
150    0.20
151}
152fn default_zone_reference_pct() -> f32 {
153    0.10
154}
155
156impl Default for ContextConfig {
157    fn default() -> Self {
158        Self {
159            default_token_budget: default_token_budget(),
160            token_multiplier: default_token_multiplier(),
161            zone_system_pct: default_zone_system_pct(),
162            zone_critical_pct: default_zone_critical_pct(),
163            zone_primary_pct: default_zone_primary_pct(),
164            zone_supporting_pct: default_zone_supporting_pct(),
165            zone_reference_pct: default_zone_reference_pct(),
166        }
167    }
168}
169
170/// Cognitive engine settings.
171#[derive(Debug, Clone, Deserialize)]
172pub struct CognitiveConfig {
173    /// Similarity threshold above which memories are considered contradictory.
174    #[serde(default = "default_contradiction_threshold")]
175    pub contradiction_threshold: f32,
176    /// Minimum similarity for memories to be considered related.
177    #[serde(default = "default_related_threshold_min")]
178    pub related_threshold_min: f32,
179    /// Maximum similarity for the "related" band (above this is near-duplicate).
180    #[serde(default = "default_related_threshold_max")]
181    pub related_threshold_max: f32,
182    /// Similarity threshold for interference detection.
183    #[serde(default = "default_interference_threshold")]
184    pub interference_threshold: f32,
185    /// Number of entries in the speculative pre-assembly cache.
186    #[serde(default = "default_speculative_cache_size")]
187    pub speculative_cache_size: usize,
188    /// Hit rate threshold for the speculative cache to remain active.
189    #[serde(default = "default_speculative_hit_threshold")]
190    pub speculative_hit_threshold: f32,
191    /// Maximum number of turns to track in a trajectory.
192    #[serde(default = "default_max_trajectory_turns")]
193    pub max_trajectory_turns: usize,
194    /// Maximum number of active pain signal warnings.
195    #[serde(default = "default_max_pain_warnings")]
196    pub max_pain_warnings: usize,
197    /// Maximum number of active phantom memory warnings.
198    #[serde(default = "default_max_phantom_warnings")]
199    pub max_phantom_warnings: usize,
200}
201
202fn default_contradiction_threshold() -> f32 {
203    0.95
204}
205fn default_related_threshold_min() -> f32 {
206    0.6
207}
208fn default_related_threshold_max() -> f32 {
209    0.85
210}
211fn default_interference_threshold() -> f32 {
212    0.8
213}
214fn default_speculative_cache_size() -> usize {
215    10
216}
217fn default_speculative_hit_threshold() -> f32 {
218    0.5
219}
220fn default_max_trajectory_turns() -> usize {
221    100
222}
223fn default_max_pain_warnings() -> usize {
224    5
225}
226fn default_max_phantom_warnings() -> usize {
227    5
228}
229
230impl Default for CognitiveConfig {
231    fn default() -> Self {
232        Self {
233            contradiction_threshold: default_contradiction_threshold(),
234            related_threshold_min: default_related_threshold_min(),
235            related_threshold_max: default_related_threshold_max(),
236            interference_threshold: default_interference_threshold(),
237            speculative_cache_size: default_speculative_cache_size(),
238            speculative_hit_threshold: default_speculative_hit_threshold(),
239            max_trajectory_turns: default_max_trajectory_turns(),
240            max_pain_warnings: default_max_pain_warnings(),
241            max_phantom_warnings: default_max_phantom_warnings(),
242        }
243    }
244}
245
246/// Memory consolidation settings.
247#[derive(Debug, Clone, Deserialize)]
248pub struct ConsolidationConfig {
249    /// Half-life for temporal salience decay, in hours.
250    #[serde(default = "default_decay_half_life_hours")]
251    pub decay_half_life_hours: f64,
252    /// Minimum salience before a memory is eligible for archival.
253    #[serde(default = "default_min_salience")]
254    pub min_salience: f32,
255    /// Minimum age in days before a memory can be archived.
256    #[serde(default = "default_archival_min_age_days")]
257    pub archival_min_age_days: u64,
258    /// Maximum salience for archival eligibility.
259    #[serde(default = "default_archival_max_salience")]
260    pub archival_max_salience: f32,
261}
262
263fn default_decay_half_life_hours() -> f64 {
264    168.0
265}
266fn default_min_salience() -> f32 {
267    0.01
268}
269fn default_archival_min_age_days() -> u64 {
270    30
271}
272fn default_archival_max_salience() -> f32 {
273    0.05
274}
275
276impl Default for ConsolidationConfig {
277    fn default() -> Self {
278        Self {
279            decay_half_life_hours: default_decay_half_life_hours(),
280            min_salience: default_min_salience(),
281            archival_min_age_days: default_archival_min_age_days(),
282            archival_max_salience: default_archival_max_salience(),
283        }
284    }
285}
286
287/// Server settings.
288#[derive(Debug, Clone, Deserialize)]
289pub struct ServerConfig {
290    /// Bind address.
291    #[serde(default = "default_host")]
292    pub host: String,
293    /// Listen port.
294    #[serde(default = "default_port")]
295    pub port: u16,
296}
297
298fn default_host() -> String {
299    "0.0.0.0".to_string()
300}
301fn default_port() -> u16 {
302    6677
303}
304
305impl Default for ServerConfig {
306    fn default() -> Self {
307        Self {
308            host: default_host(),
309            port: default_port(),
310        }
311    }
312}
313
314impl MenteConfig {
315    /// Load configuration from a JSON file.
316    pub fn from_file(path: &Path) -> MenteResult<Self> {
317        let contents = std::fs::read_to_string(path).map_err(MenteError::Io)?;
318        serde_json::from_str(&contents).map_err(|e| MenteError::Serialization(e.to_string()))
319    }
320}
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325
326    #[test]
327    fn test_defaults() {
328        let cfg = MenteConfig::default();
329
330        assert_eq!(cfg.storage.data_dir, "data");
331        assert_eq!(cfg.storage.cache_bytes, 64 * 1024 * 1024);
332        assert_eq!(cfg.storage.wal_checkpoint_bytes, 8 * 1024 * 1024);
333        assert_eq!(cfg.storage.max_record_bytes, 16 * 1024 * 1024);
334
335        assert_eq!(cfg.index.hnsw_m, 16);
336        assert_eq!(cfg.index.hnsw_ef_construction, 200);
337        assert_eq!(cfg.index.hnsw_ef_search, 50);
338
339        assert_eq!(cfg.context.default_token_budget, 4096);
340        assert!((cfg.context.token_multiplier - 1.3).abs() < f32::EPSILON);
341        assert!((cfg.context.zone_system_pct - 0.10).abs() < f32::EPSILON);
342        assert!((cfg.context.zone_critical_pct - 0.25).abs() < f32::EPSILON);
343        assert!((cfg.context.zone_primary_pct - 0.35).abs() < f32::EPSILON);
344        assert!((cfg.context.zone_supporting_pct - 0.20).abs() < f32::EPSILON);
345        assert!((cfg.context.zone_reference_pct - 0.10).abs() < f32::EPSILON);
346
347        assert!((cfg.cognitive.contradiction_threshold - 0.95).abs() < f32::EPSILON);
348        assert!((cfg.cognitive.related_threshold_min - 0.6).abs() < f32::EPSILON);
349        assert!((cfg.cognitive.related_threshold_max - 0.85).abs() < f32::EPSILON);
350        assert!((cfg.cognitive.interference_threshold - 0.8).abs() < f32::EPSILON);
351        assert_eq!(cfg.cognitive.speculative_cache_size, 10);
352        assert!((cfg.cognitive.speculative_hit_threshold - 0.5).abs() < f32::EPSILON);
353        assert_eq!(cfg.cognitive.max_trajectory_turns, 100);
354        assert_eq!(cfg.cognitive.max_pain_warnings, 5);
355        assert_eq!(cfg.cognitive.max_phantom_warnings, 5);
356
357        assert!((cfg.consolidation.decay_half_life_hours - 168.0).abs() < f64::EPSILON);
358        assert!((cfg.consolidation.min_salience - 0.01).abs() < f32::EPSILON);
359        assert_eq!(cfg.consolidation.archival_min_age_days, 30);
360        assert!((cfg.consolidation.archival_max_salience - 0.05).abs() < f32::EPSILON);
361
362        assert_eq!(cfg.server.host, "0.0.0.0");
363        assert_eq!(cfg.server.port, 6677);
364    }
365
366    #[test]
367    fn test_from_file() {
368        let dir = std::env::temp_dir().join("mentedb_config_test");
369        std::fs::create_dir_all(&dir).unwrap();
370        let path = dir.join("config.json");
371
372        let json = r#"{
373            "storage": {
374                "data_dir": "/var/mentedb",
375                "cache_bytes": 2147483648,
376                "buffer_pool_size": 2048
377            },
378            "index": {
379                "hnsw_m": 32,
380                "hnsw_ef_construction": 400,
381                "hnsw_ef_search": 100
382            },
383            "server": {
384                "host": "127.0.0.1",
385                "port": 9999
386            }
387        }"#;
388        std::fs::write(&path, json).unwrap();
389
390        let cfg = MenteConfig::from_file(&path).unwrap();
391
392        assert_eq!(cfg.storage.data_dir, "/var/mentedb");
393        assert_eq!(cfg.storage.cache_bytes, 2 * 1024 * 1024 * 1024);
394        // Retired keys from older config files are ignored, not rejected.
395        assert_eq!(cfg.storage.max_record_bytes, 16 * 1024 * 1024);
396        assert_eq!(cfg.index.hnsw_m, 32);
397        assert_eq!(cfg.index.hnsw_ef_construction, 400);
398        assert_eq!(cfg.index.hnsw_ef_search, 100);
399        assert_eq!(cfg.server.host, "127.0.0.1");
400        assert_eq!(cfg.server.port, 9999);
401
402        // Sections not provided should use defaults.
403        assert_eq!(cfg.context.default_token_budget, 4096);
404        assert!((cfg.cognitive.contradiction_threshold - 0.95).abs() < f32::EPSILON);
405        assert!((cfg.consolidation.decay_half_life_hours - 168.0).abs() < f64::EPSILON);
406
407        std::fs::remove_dir_all(&dir).ok();
408    }
409
410    #[test]
411    fn test_from_file_empty_object() {
412        let dir = std::env::temp_dir().join("mentedb_config_test_empty");
413        std::fs::create_dir_all(&dir).unwrap();
414        let path = dir.join("config.json");
415        std::fs::write(&path, "{}").unwrap();
416
417        let cfg = MenteConfig::from_file(&path).unwrap();
418        let defaults = MenteConfig::default();
419
420        assert_eq!(cfg.storage.cache_bytes, defaults.storage.cache_bytes);
421        assert_eq!(cfg.index.hnsw_m, defaults.index.hnsw_m);
422        assert_eq!(cfg.server.port, defaults.server.port);
423
424        std::fs::remove_dir_all(&dir).ok();
425    }
426
427    #[test]
428    fn test_from_file_not_found() {
429        let result = MenteConfig::from_file(Path::new("/nonexistent/config.json"));
430        assert!(result.is_err());
431    }
432
433    #[test]
434    fn test_from_file_invalid_json() {
435        let dir = std::env::temp_dir().join("mentedb_config_test_invalid");
436        std::fs::create_dir_all(&dir).unwrap();
437        let path = dir.join("config.json");
438        std::fs::write(&path, "not json at all").unwrap();
439
440        let result = MenteConfig::from_file(&path);
441        assert!(result.is_err());
442
443        std::fs::remove_dir_all(&dir).ok();
444    }
445}