Skip to main content

microscope_memory/
config.rs

1use crate::types::ProjectId;
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::path::Path;
5
6#[derive(Debug, Serialize, Deserialize, Clone)]
7pub struct HooksConfig {
8    pub read_only: bool,
9    pub write_enabled: bool,
10}
11
12impl Default for HooksConfig {
13    fn default() -> Self {
14        Self {
15            read_only: true,
16            write_enabled: false,
17        }
18    }
19}
20
21#[derive(Debug, Serialize, Deserialize, Clone)]
22pub struct Config {
23    pub paths: Paths,
24    pub index: Index,
25    pub search: Search,
26    pub memory_layers: MemoryLayers,
27    pub performance: Performance,
28    pub logging: Logging,
29    #[serde(default)]
30    pub embedding: Embedding,
31    #[serde(default)]
32    pub server: Server,
33    #[serde(default)]
34    pub federation: Federation,
35    #[serde(default)]
36    pub hooks: HooksConfig,
37    #[serde(default = "default_project_id")]
38    pub project_id: ProjectId,
39    #[serde(default)]
40    pub epistemic: EpistemicConfig,
41}
42
43#[derive(Debug, Serialize, Deserialize, Clone)]
44pub struct Paths {
45    pub layers_dir: String,
46    pub output_dir: String,
47    pub temp_dir: String,
48}
49
50#[derive(Debug, Serialize, Deserialize, Clone)]
51pub struct Index {
52    pub max_depth: u8,
53    pub header_size: usize,
54    #[serde(default = "default_auto_rebuild")]
55    pub auto_rebuild: bool,
56    #[serde(default = "default_auto_rebuild_entries")]
57    pub auto_rebuild_entries: usize,
58    /// Maximum persisted entries per layer. Zero keeps the full history.
59    #[serde(default = "default_layer_retention_entries")]
60    pub layer_retention_entries: usize,
61    /// Maximum blocks in the index before low-score blocks are evicted.
62    /// Zero keeps the index unbounded.
63    #[serde(default = "default_max_blocks")]
64    pub max_blocks: usize,
65    /// Blocks with importance >= this value are never evicted.
66    #[serde(default = "default_protect_min_importance")]
67    pub protect_min_importance: u8,
68    /// Minimum Hebbian recall energy for automatic importance promotion.
69    #[serde(default = "default_promote_energy_threshold")]
70    pub promote_energy_threshold: f32,
71}
72
73fn default_auto_rebuild() -> bool {
74    true
75}
76
77fn default_layer_retention_entries() -> usize {
78    2000
79}
80
81fn default_max_blocks() -> usize {
82    0
83}
84
85fn default_protect_min_importance() -> u8 {
86    8
87}
88
89fn default_promote_energy_threshold() -> f32 {
90    0.35
91}
92
93fn default_auto_rebuild_entries() -> usize {
94    25
95}
96
97#[derive(Debug, Serialize, Deserialize, Clone)]
98pub struct Search {
99    pub default_k: usize,
100    pub zoom_weight: f32,
101    pub keyword_boost: f32,
102    #[serde(default)]
103    pub semantic_weight: f32,
104    /// How much the cosine counts in the *ranking*, as a distance bonus.
105    ///
106    /// Separate from `semantic_weight`, which is a 0..1 blend deciding how far a
107    /// query's spatial coordinates lean toward its embedding. Those are
108    /// different units with different meanings, and sharing one knob made the
109    /// two impossible to tell apart: setting it to 2.0 to test whether the
110    /// semantic term could lead also moved the coordinates, and the measurement
111    /// credited both effects to the same number.
112    ///
113    /// The default is the value both halves independently measured best at; see
114    /// docs/measurements/ for the split curves. The two were never measured
115    /// apart before this field existed.
116    #[serde(default = "default_semantic_rank_gain")]
117    pub semantic_rank_gain: f32,
118    #[serde(default)]
119    pub emotional_bias_weight: f32,
120    #[serde(default)]
121    pub emotion_21d_weight: f32,
122}
123
124#[derive(Debug, Serialize, Deserialize, Clone)]
125pub struct MemoryLayers {
126    pub layers: Vec<String>,
127}
128
129#[derive(Debug, Serialize, Deserialize, Clone)]
130pub struct Performance {
131    pub use_mmap: bool,
132    pub cache_size: usize,
133    pub build_workers: usize,
134    #[serde(default)]
135    pub use_gpu: bool,
136    #[serde(default)]
137    pub compression: bool,
138    #[serde(default = "default_cache_ttl")]
139    pub cache_ttl_secs: u64,
140}
141
142fn default_cache_ttl() -> u64 {
143    300
144}
145
146#[derive(Debug, Serialize, Deserialize, Clone)]
147pub struct Embedding {
148    #[serde(default = "default_provider")]
149    pub provider: String,
150    #[serde(default = "default_model")]
151    pub model: String,
152    #[serde(default = "default_dim")]
153    pub dim: usize,
154    #[serde(default = "default_max_depth")]
155    pub max_depth: u8,
156    #[serde(default)]
157    pub onnx_model_path: Option<String>,
158    #[serde(default)]
159    pub tokenizer_path: Option<String>,
160    #[serde(default)]
161    pub use_gpu: bool,
162}
163
164fn default_provider() -> String {
165    "mock".to_string()
166}
167fn default_model() -> String {
168    "sentence-transformers/all-MiniLM-L6-v2".to_string()
169}
170fn default_dim() -> usize {
171    384
172}
173fn default_max_depth() -> u8 {
174    // D5 holds 15..39 character statements (auto_depth), which is where short
175    // personal facts are stored. Embedding only to D4 left those facts without
176    // any vector, making them unreachable from the semantic path.
177    5
178}
179fn default_semantic_rank_gain() -> f32 {
180    // Set from the measured curve, not chosen: see WHITEPAPER 11.9. The
181    // knee is at 2.0 -- 1.0 costs about 1.7 points of R@1 and nothing above 2.0
182    // adds any.
183    2.0
184}
185
186impl Default for Embedding {
187    fn default() -> Self {
188        Self {
189            provider: default_provider(),
190            model: default_model(),
191            dim: default_dim(),
192            max_depth: default_max_depth(),
193            onnx_model_path: None,
194            tokenizer_path: None,
195            use_gpu: false,
196        }
197    }
198}
199
200#[derive(Debug, Serialize, Deserialize, Clone)]
201pub struct Server {
202    #[serde(default = "default_port")]
203    pub port: u16,
204    #[serde(default)]
205    pub cors_origin: Option<String>,
206    #[serde(default)]
207    pub openai_api_key: Option<String>,
208    #[serde(default)]
209    pub gemini_api_key: Option<String>,
210    /// Optional shared API key for inbound HTTP requests. Empty = no auth
211    /// (single-user localhost default). When set, every bridge request must
212    /// carry `X-API-Key: <key>`. Required when binding on a non-loopback host.
213    #[serde(default)]
214    pub api_key: Option<String>,
215}
216
217fn default_port() -> u16 {
218    6060
219}
220
221impl Default for Server {
222    fn default() -> Self {
223        Self {
224            port: default_port(),
225            cors_origin: None,
226            openai_api_key: None,
227            gemini_api_key: None,
228            api_key: None,
229        }
230    }
231}
232
233#[derive(Debug, Serialize, Deserialize, Clone)]
234pub struct Logging {
235    pub level: String,
236    pub file: Option<String>,
237}
238
239#[derive(Debug, Serialize, Deserialize, Clone, Default)]
240pub struct Federation {
241    #[serde(default)]
242    pub indices: Vec<FederatedIndex>,
243}
244
245#[derive(Debug, Serialize, Deserialize, Clone, Default)]
246pub struct EpistemicConfig {
247    /// Gate importance promotion for unsupported Inference/Hypothesis.
248    #[serde(default = "default_true")]
249    pub gate_promotion: bool,
250    /// Minimum independent sources needed for non-zero confidence.
251    #[serde(default = "default_min_sources")]
252    pub min_independent_sources: u32,
253    /// Cosine similarity threshold for echo detection.
254    #[serde(default = "default_sim_threshold")]
255    pub sim_threshold: f32,
256    /// Near-duplicate scan depth for link operations.
257    #[serde(default = "default_search_k")]
258    pub search_k: usize,
259    /// Confidence formula weights.
260    #[serde(default)]
261    pub confidence_weights: ConfidenceWeights,
262    /// Bias added to eviction score from confidence (0 = disabled).
263    #[serde(default)]
264    pub eviction_evidence_bias: f32,
265}
266
267fn default_true() -> bool {
268    true
269}
270fn default_min_sources() -> u32 {
271    1
272}
273fn default_sim_threshold() -> f32 {
274    0.85
275}
276fn default_search_k() -> usize {
277    8
278}
279
280#[derive(Debug, Serialize, Deserialize, Clone)]
281pub struct ConfidenceWeights {
282    #[serde(default = "default_obs_w")]
283    pub observation_w: i32,
284    #[serde(default = "default_src_w")]
285    pub source_w: i32,
286    #[serde(default = "default_refute_w")]
287    pub refute_w: i32,
288    #[serde(default = "default_age_pen")]
289    pub age_penalty: i32,
290}
291
292impl Default for ConfidenceWeights {
293    fn default() -> Self {
294        Self {
295            observation_w: 30,
296            source_w: 18,
297            refute_w: 25,
298            age_penalty: 5,
299        }
300    }
301}
302
303fn default_obs_w() -> i32 {
304    30
305}
306fn default_src_w() -> i32 {
307    18
308}
309fn default_refute_w() -> i32 {
310    25
311}
312fn default_age_pen() -> i32 {
313    5
314}
315
316#[derive(Debug, Serialize, Deserialize, Clone)]
317pub struct FederatedIndex {
318    pub name: String,
319    pub config_path: String,
320    #[serde(default = "default_weight")]
321    pub weight: f32,
322}
323
324fn default_weight() -> f32 {
325    1.0
326}
327
328fn default_project_id() -> ProjectId {
329    ProjectId::GLOBAL
330}
331
332impl Config {
333    /// Loads config from a TOML file.
334    pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, Box<dyn std::error::Error>> {
335        if !path.as_ref().exists() {
336            return Ok(Self::default());
337        }
338
339        let content = fs::read_to_string(path)?;
340        let config: Config = toml::from_str(&content)?;
341        Ok(config)
342    }
343
344    /// Saves the current config to a TOML file.
345    pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<(), Box<dyn std::error::Error>> {
346        let content = toml::to_string_pretty(self)?;
347        let tmp_path = path.as_ref().with_extension("toml.tmp");
348        fs::write(&tmp_path, &content)?;
349        fs::rename(&tmp_path, path.as_ref())?;
350        Ok(())
351    }
352}
353
354impl Default for Config {
355    fn default() -> Self {
356        Self {
357            paths: Paths {
358                layers_dir: "./layers".to_string(),
359                output_dir: "./output".to_string(),
360                temp_dir: "./tmp".to_string(),
361            },
362            index: Index {
363                max_depth: 8,
364                header_size: 50,
365                auto_rebuild: default_auto_rebuild(),
366                auto_rebuild_entries: default_auto_rebuild_entries(),
367                layer_retention_entries: default_layer_retention_entries(),
368                max_blocks: default_max_blocks(),
369                protect_min_importance: default_protect_min_importance(),
370                promote_energy_threshold: default_promote_energy_threshold(),
371            },
372            search: Search {
373                default_k: 10,
374                zoom_weight: 3.0,   // boost recent memories more
375                keyword_boost: 0.4, // tuned for better recall precision
376                semantic_weight: 0.0,
377                semantic_rank_gain: default_semantic_rank_gain(),
378                emotional_bias_weight: 0.0,
379                emotion_21d_weight: 0.0,
380            },
381            memory_layers: MemoryLayers {
382                layers: vec![
383                    "long_term".to_string(),
384                    "short_term".to_string(),
385                    "session".to_string(),
386                    "associative".to_string(),
387                    "echo_cache".to_string(),
388                ],
389            },
390            performance: Performance {
391                use_mmap: true,
392                cache_size: 64,
393                build_workers: 4,
394                use_gpu: false,
395                compression: false,
396                cache_ttl_secs: default_cache_ttl(),
397            },
398            logging: Logging {
399                level: "info".to_string(),
400                file: Some("microscope.log".to_string()),
401            },
402            embedding: Embedding::default(),
403            server: Server::default(),
404            federation: Federation::default(),
405            hooks: HooksConfig::default(),
406            project_id: ProjectId::GLOBAL,
407            epistemic: EpistemicConfig::default(),
408        }
409    }
410}