zeph_config/memory/root.rs
1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Root memory configuration and vector-store backend selection.
5//!
6//! Hosts [`MemoryConfig`] — the central `[memory]` table aggregating every memory
7//! subsystem — and the [`VectorBackend`] selector for the embedding store.
8
9use crate::defaults::{default_sqlite_path_field, default_true};
10use crate::providers::ProviderName;
11use serde::{Deserialize, Serialize};
12use zeph_common::secret::Secret;
13
14use super::{
15 AdmissionConfig, AutoDreamConfig, CategoryConfig, CompressionConfig,
16 CompressionGuidelinesConfig, ConsentGateConfig, ConsolidationConfig, ContextStrategy,
17 CrossThreadStoreConfig, DigestConfig, DocumentConfig, EmGraphConfig,
18 EpisodicConsolidationConfig, EvictionConfig, FiveSignalConfig, ForgettingConfig, GraphConfig,
19 HebbianConfig, MemCotConfig, MicrocompactConfig, OpticalForgettingConfig, PersonaConfig,
20 ReasoningConfig, RetrievalConfig, RetrievalFailuresConfig, SemanticConfig, SessionsConfig,
21 SidequestConfig, StoreRoutingConfig, TierConfig, TieredRetrievalConfig, TrajectoryConfig,
22 TrajectoryRiskAccumulatorConfig, TreeConfig, TypeAwareComposeConfig, WriteQualityGateConfig,
23};
24
25fn default_sqlite_pool_size() -> u32 {
26 5
27}
28
29fn default_autosave_min_length() -> usize {
30 20
31}
32
33fn default_tool_call_cutoff() -> usize {
34 6
35}
36
37fn default_token_safety_margin() -> f32 {
38 1.0
39}
40
41fn default_redact_credentials() -> bool {
42 true
43}
44
45fn default_qdrant_url() -> String {
46 "http://localhost:6334".into()
47}
48
49fn default_qdrant_timeout_secs() -> u64 {
50 10
51}
52
53fn default_summarization_threshold() -> usize {
54 50
55}
56
57fn default_summarization_llm_timeout_secs() -> u64 {
58 60
59}
60
61fn default_context_budget_tokens() -> usize {
62 0
63}
64
65fn default_soft_compaction_threshold() -> f32 {
66 0.60
67}
68
69fn default_hard_compaction_threshold() -> f32 {
70 0.90
71}
72
73fn default_compaction_preserve_tail() -> usize {
74 6
75}
76
77fn default_compaction_cooldown_turns() -> u8 {
78 2
79}
80
81fn default_auto_budget() -> bool {
82 true
83}
84
85fn default_prune_protect_tokens() -> usize {
86 40_000
87}
88
89fn default_cross_session_score_threshold() -> f32 {
90 0.35
91}
92
93fn default_shutdown_summary() -> bool {
94 true
95}
96
97fn default_shutdown_summary_min_messages() -> usize {
98 4
99}
100
101fn default_shutdown_summary_max_messages() -> usize {
102 20
103}
104
105fn default_shutdown_summary_timeout_secs() -> u64 {
106 30
107}
108
109/// Vector backend selector for embedding storage.
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
111#[serde(rename_all = "lowercase")]
112#[non_exhaustive]
113pub enum VectorBackend {
114 Qdrant,
115 #[default]
116 Sqlite,
117}
118
119impl VectorBackend {
120 /// Return the lowercase identifier string for this backend.
121 ///
122 /// # Examples
123 ///
124 /// ```
125 /// use zeph_config::VectorBackend;
126 ///
127 /// assert_eq!(VectorBackend::Sqlite.as_str(), "sqlite");
128 /// assert_eq!(VectorBackend::Qdrant.as_str(), "qdrant");
129 /// ```
130 #[must_use]
131 pub fn as_str(&self) -> &'static str {
132 match self {
133 Self::Qdrant => "qdrant",
134 Self::Sqlite => "sqlite",
135 }
136 }
137}
138
139/// Memory subsystem configuration, nested under `[memory]` in TOML.
140///
141/// Controls `SQLite` and Qdrant storage, semantic recall, context compaction,
142/// multi-tier promotion, and all memory-related background tasks.
143///
144/// # Example (TOML)
145///
146/// ```toml
147/// [memory]
148/// sqlite_path = "~/.local/share/zeph/data/zeph.db"
149/// qdrant_url = "http://localhost:6334"
150/// history_limit = 50
151/// summarization_threshold = 50
152/// auto_budget = true
153/// ```
154#[derive(Debug, Deserialize, Serialize)]
155#[allow(clippy::struct_excessive_bools)] // config struct — boolean flags are idiomatic for TOML-deserialized configuration
156pub struct MemoryConfig {
157 #[serde(default)]
158 pub compression_guidelines: CompressionGuidelinesConfig,
159 #[serde(default = "default_sqlite_path_field")]
160 pub sqlite_path: String,
161 pub history_limit: u32,
162 #[serde(default = "default_qdrant_url")]
163 pub qdrant_url: String,
164 /// Optional API key for authenticating to a remote or managed Qdrant cluster.
165 ///
166 /// Required when `qdrant_url` points to a non-localhost host (e.g. Qdrant Cloud).
167 /// Leave `None` for local dev instances. The actual key is resolved from the vault:
168 /// `zeph vault set ZEPH_QDRANT_API_KEY "<key>"`.
169 ///
170 /// The value is wrapped in [`Secret`] to prevent accidental logging.
171 /// `skip_serializing` prevents the key from being written back to TOML on config save.
172 #[serde(default, skip_serializing)]
173 pub qdrant_api_key: Option<Secret>,
174 /// Per-call timeout applied to every Qdrant gRPC operation, in seconds.
175 ///
176 /// Bounds each call against a hung server or a stalled network path instead of blocking
177 /// the calling async task indefinitely. Default: `10`.
178 #[serde(default = "default_qdrant_timeout_secs")]
179 pub qdrant_timeout_secs: u64,
180 #[serde(default)]
181 pub semantic: SemanticConfig,
182 #[serde(default = "default_summarization_threshold")]
183 pub summarization_threshold: usize,
184 /// LLM call timeout for summarization, in seconds. Default: `60`.
185 #[serde(default = "default_summarization_llm_timeout_secs")]
186 pub summarization_llm_timeout_secs: u64,
187 #[serde(default = "default_context_budget_tokens")]
188 pub context_budget_tokens: usize,
189 #[serde(default = "default_soft_compaction_threshold")]
190 pub soft_compaction_threshold: f32,
191 #[serde(
192 default = "default_hard_compaction_threshold",
193 alias = "compaction_threshold"
194 )]
195 pub hard_compaction_threshold: f32,
196 #[serde(default = "default_compaction_preserve_tail")]
197 pub compaction_preserve_tail: usize,
198 #[serde(default = "default_compaction_cooldown_turns")]
199 pub compaction_cooldown_turns: u8,
200 #[serde(default = "default_auto_budget")]
201 pub auto_budget: bool,
202 #[serde(default = "default_prune_protect_tokens")]
203 pub prune_protect_tokens: usize,
204 #[serde(default = "default_cross_session_score_threshold")]
205 pub cross_session_score_threshold: f32,
206 #[serde(default)]
207 pub vector_backend: VectorBackend,
208 #[serde(default = "default_token_safety_margin")]
209 pub token_safety_margin: f32,
210 #[serde(default = "default_redact_credentials")]
211 pub redact_credentials: bool,
212 #[serde(default = "default_true")]
213 pub autosave_assistant: bool,
214 #[serde(default = "default_autosave_min_length")]
215 pub autosave_min_length: usize,
216 #[serde(default = "default_tool_call_cutoff")]
217 pub tool_call_cutoff: usize,
218 #[serde(default = "default_sqlite_pool_size")]
219 pub sqlite_pool_size: u32,
220 #[serde(default)]
221 pub sessions: SessionsConfig,
222 #[serde(default)]
223 pub documents: DocumentConfig,
224 #[serde(default)]
225 pub eviction: EvictionConfig,
226 #[serde(default)]
227 pub compression: CompressionConfig,
228 #[serde(default)]
229 pub sidequest: SidequestConfig,
230 #[serde(default)]
231 pub graph: GraphConfig,
232 /// Store a lightweight session summary to the vector store on shutdown when no session
233 /// summary exists yet for this conversation. Enables cross-session recall for short or
234 /// interrupted sessions that never triggered hard compaction. Default: `true`.
235 #[serde(default = "default_shutdown_summary")]
236 pub shutdown_summary: bool,
237 /// Minimum number of user-turn messages required before a shutdown summary is generated.
238 /// Sessions below this threshold are considered trivial and skipped. Default: `4`.
239 #[serde(default = "default_shutdown_summary_min_messages")]
240 pub shutdown_summary_min_messages: usize,
241 /// Maximum number of recent messages (user + assistant) sent to the LLM for shutdown
242 /// summarization. Caps token cost for long sessions that never triggered hard compaction.
243 /// Default: `20`.
244 #[serde(default = "default_shutdown_summary_max_messages")]
245 pub shutdown_summary_max_messages: usize,
246 /// Per-attempt timeout in seconds for each LLM call during shutdown summarization.
247 /// Applies independently to the structured call and to the plain-text fallback.
248 /// Default: `10`.
249 #[serde(default = "default_shutdown_summary_timeout_secs")]
250 pub shutdown_summary_timeout_secs: u64,
251 /// LLM provider used for shutdown summarization calls.
252 ///
253 /// Accepts a provider name from `[[llm.providers]]`. When empty, falls back to the primary
254 /// provider. Use a fast, cost-efficient model (e.g. `"fast"`) to minimise shutdown latency.
255 ///
256 /// Example:
257 /// ```toml
258 /// [memory]
259 /// shutdown_summary_provider = "fast"
260 /// ```
261 #[serde(default)]
262 pub shutdown_summary_provider: ProviderName,
263 /// LLM provider used for deferred tool-pair summarization (context compaction).
264 ///
265 /// Accepts a provider name from `[[llm.providers]]`. When empty, falls back to the primary
266 /// provider. A mid-tier model is usually sufficient for compaction summaries.
267 ///
268 /// Example:
269 /// ```toml
270 /// [memory]
271 /// compaction_provider = "fast"
272 /// ```
273 #[serde(default)]
274 pub compaction_provider: ProviderName,
275 /// Use structured anchored summaries for context compaction.
276 ///
277 /// When enabled, hard compaction requests a JSON schema from the LLM
278 /// instead of free-form prose. Falls back to prose if the LLM fails
279 /// to produce valid JSON. Default: `false`.
280 #[serde(default)]
281 pub structured_summaries: bool,
282 /// AOI three-layer memory tier promotion system.
283 ///
284 /// When `tiers.enabled = true`, a background sweep promotes frequently-accessed episodic
285 /// messages to a semantic tier by clustering near-duplicates and distilling via LLM.
286 #[serde(default)]
287 pub tiers: TierConfig,
288 /// A-MAC adaptive memory admission control.
289 ///
290 /// When `admission.enabled = true`, each message is evaluated before saving and rejected
291 /// if its composite admission score falls below the configured threshold.
292 #[serde(default)]
293 pub admission: AdmissionConfig,
294 /// Session digest generation at session end. Default: disabled.
295 #[serde(default)]
296 pub digest: DigestConfig,
297 /// Context assembly strategy. Default: `full_history` (current behavior).
298 #[serde(default)]
299 pub context_strategy: ContextStrategy,
300 /// Number of turns at which `Adaptive` strategy switches to `MemoryFirst`. Default: `20`.
301 #[serde(default = "default_crossover_turn_threshold")]
302 pub crossover_turn_threshold: u32,
303 /// All-Mem lifelong memory consolidation sweep.
304 ///
305 /// When `consolidation.enabled = true`, a background loop clusters semantically similar
306 /// messages and merges them into consolidated entries via LLM.
307 #[serde(default)]
308 pub consolidation: ConsolidationConfig,
309 /// `SleepGate` forgetting sweep (#2397).
310 ///
311 /// When `forgetting.enabled = true`, a background loop periodically decays importance
312 /// scores and prunes memories below the forgetting floor.
313 #[serde(default)]
314 pub forgetting: ForgettingConfig,
315 /// `PostgreSQL` connection URL.
316 ///
317 /// Used when the binary is compiled with `--features postgres`.
318 /// Can be overridden by the vault key `ZEPH_DATABASE_URL`.
319 /// Example: `postgres://user:pass@localhost:5432/zeph`
320 /// Default: `None` (uses `sqlite_path` instead).
321 ///
322 /// The value is wrapped in [`Secret`] to prevent accidental logging — it commonly
323 /// embeds a username and password. `skip_serializing` prevents it from being written
324 /// back to TOML on config save, matching [`qdrant_api_key`](Self::qdrant_api_key).
325 #[serde(default, skip_serializing)]
326 pub database_url: Option<Secret>,
327 /// Cost-sensitive store routing (#2444).
328 ///
329 /// When `store_routing.enabled = true`, query intent is classified and routed to
330 /// the cheapest sufficient backend instead of querying all stores on every turn.
331 #[serde(default)]
332 pub store_routing: StoreRoutingConfig,
333 /// Persona memory layer (#2461).
334 ///
335 /// When `persona.enabled = true`, user preferences and domain knowledge are extracted
336 /// from conversation history and injected into context after the system prompt.
337 #[serde(default)]
338 pub persona: PersonaConfig,
339 /// Trajectory-informed memory (#2498).
340 #[serde(default)]
341 pub trajectory: TrajectoryConfig,
342 /// Category-aware memory (#2428).
343 #[serde(default)]
344 pub category: CategoryConfig,
345 /// `TiMem` temporal-hierarchical memory tree (#2262).
346 #[serde(default)]
347 pub tree: TreeConfig,
348 /// Time-based microcompact (#2699).
349 ///
350 /// When `microcompact.enabled = true`, stale low-value tool outputs are cleared
351 /// from context when the session has been idle longer than `gap_threshold_minutes`.
352 #[serde(default)]
353 pub microcompact: MicrocompactConfig,
354 /// autoDream background memory consolidation (#2697).
355 ///
356 /// When `autodream.enabled = true`, a constrained consolidation subagent runs
357 /// after a session ends if both `min_sessions` and `min_hours` gates pass.
358 #[serde(default)]
359 pub autodream: AutoDreamConfig,
360 /// Cosine similarity threshold for deduplicating key facts in `zeph_key_facts` (#2717).
361 ///
362 /// Before inserting a new key fact, its nearest neighbour is looked up in the
363 /// `zeph_key_facts` collection. If the best score is ≥ this threshold the fact is
364 /// considered a near-duplicate and skipped. Set to a value greater than `1.0` (e.g.
365 /// `2.0`) to disable dedup entirely. Default: `0.95`.
366 #[serde(default = "default_key_facts_dedup_threshold")]
367 pub key_facts_dedup_threshold: f32,
368 /// Experience compression spectrum (#3305).
369 ///
370 /// Controls three-tier retrieval policy and background skill-promotion engine.
371 #[serde(default)]
372 pub compression_spectrum: crate::features::CompressionSpectrumConfig,
373 /// MemMachine-inspired retrieval-stage tuning (#3340).
374 ///
375 /// Controls ANN candidate depth, search-prompt formatting, and the shape of memory snippets
376 /// injected into agent context. Separate from `SemanticConfig` because these knobs apply
377 /// uniformly across graph, hybrid, and vector-only recall paths.
378 ///
379 /// # Example (TOML)
380 ///
381 /// ```toml
382 /// [memory.retrieval]
383 /// depth = 40
384 /// search_prompt_template = ""
385 /// context_format = "structured"
386 /// ```
387 #[serde(default)]
388 pub retrieval: RetrievalConfig,
389 /// `ReasoningBank`: distilled reasoning strategy memory (#3342).
390 ///
391 /// When `reasoning.enabled = true`, each completed agent turn is evaluated by a self-judge
392 /// LLM call; successful and failed reasoning chains are compressed into short, generalizable
393 /// strategy summaries stored in `reasoning_strategies` (`SQLite`) and a matching Qdrant
394 /// collection. Top-k strategies are retrieved by embedding similarity at context-build time
395 /// and injected before the LLM call.
396 #[serde(default)]
397 pub reasoning: ReasoningConfig,
398 /// Hebbian edge-weight reinforcement configuration (HL-F1/F2, #3344).
399 ///
400 /// When `enabled = true`, the weight of each `graph_edges` row is incremented
401 /// by `hebbian_lr` every time that edge is traversed during a recall. Default: disabled.
402 ///
403 /// # Example (TOML)
404 ///
405 /// ```toml
406 /// [memory.hebbian]
407 /// enabled = true
408 /// hebbian_lr = 0.1
409 /// ```
410 #[serde(default)]
411 pub hebbian: HebbianConfig,
412 /// `MemCoT` rolling semantic state configuration (#3574).
413 ///
414 /// When `enabled = true`, each completed assistant turn spawns a background distillation
415 /// task that compresses the response into a short semantic state buffer. The buffer is
416 /// prepended to graph recall queries so retrieval stays contextually relevant across long
417 /// multi-turn sessions.
418 ///
419 /// # Example (TOML)
420 ///
421 /// ```toml
422 /// [memory.memcot]
423 /// enabled = true
424 /// distill_provider = "fast"
425 /// min_assistant_chars = 200
426 /// max_distills_per_session = 50
427 /// ```
428 #[serde(default)]
429 pub memcot: MemCotConfig,
430 /// `OmniMem` retrieval failure tracking (issue #3576).
431 ///
432 /// When `enabled = true`, no-hit and low-confidence recall events are logged
433 /// asynchronously to `memory_retrieval_failures` for closed-loop parameter tuning.
434 ///
435 /// # Example (TOML)
436 ///
437 /// ```toml
438 /// [memory.retrieval_failures]
439 /// enabled = true
440 /// low_confidence_threshold = 0.3
441 /// retention_days = 90
442 /// ```
443 #[serde(default)]
444 pub retrieval_failures: RetrievalFailuresConfig,
445 /// Write quality gate (#3629).
446 ///
447 /// When `quality_gate.enabled = true`, each `remember()` call is scored and low-quality
448 /// writes are rejected before persistence. Evaluated after A-MAC admission control.
449 #[serde(default)]
450 pub quality_gate: WriteQualityGateConfig,
451 /// `MemFlow` tiered intent-driven retrieval (issue #3712).
452 ///
453 /// When `tiered_retrieval.enabled = true`, recall queries are classified by intent and
454 /// dispatched to the cheapest sufficient tier (`ProfileLookup` → `TargetedRetrieval` →
455 /// `DeepReasoning`) with optional validation and tier escalation.
456 #[serde(default)]
457 pub tiered_retrieval: TieredRetrievalConfig,
458 /// `MemGuard`-inspired type-aware retrieval composition (spec 004-16, issue #6086).
459 ///
460 /// When `type_aware_compose.enabled = true`, `schedule_context_fetchers` composes only the
461 /// functional memory types in the active set instead of unconditionally injecting all of
462 /// them. Retrieval-only: no new Qdrant collection, no write-path change. Default: disabled
463 /// (byte-for-byte no-op).
464 #[serde(default)]
465 pub type_aware_compose: TypeAwareComposeConfig,
466 /// `ScrapMem` optical forgetting (issue #3713).
467 ///
468 /// When `optical_forgetting.enabled = true`, a background sweep progressively compresses
469 /// old messages: `Full` → `Compressed` → `SummaryOnly`, saving token budget in context assembly.
470 #[serde(default)]
471 pub optical_forgetting: OpticalForgettingConfig,
472 /// EM-Graph episodic event extraction and causal linking (issue #3713).
473 ///
474 /// When `em_graph.enabled = true`, episodic events are extracted from conversation turns
475 /// and linked via causal relationships, enabling causal-chain retrieval.
476 #[serde(default)]
477 pub em_graph: EmGraphConfig,
478 /// Episodic-to-semantic consolidation daemon (issue #3799).
479 ///
480 /// When `episodic_consolidation.enabled = true`, a background loop periodically sweeps
481 /// mature `episodic_events`, extracts durable facts via LLM, deduplicates against existing
482 /// key facts, and promotes them to the semantic tier in `zeph_key_facts`.
483 #[serde(default)]
484 pub episodic_consolidation: EpisodicConsolidationConfig,
485 /// MAGE shadow memory trajectory risk accumulator (spec 004-19).
486 ///
487 /// Maintains a per-session rolling risk score fed by sanitizer audit signals.
488 /// When `shadow_memory.enabled = true`, tool execution is gated if cumulative
489 /// trajectory risk exceeds `risk_threshold`. When `false`, all code paths are
490 /// zero-cost no-ops.
491 ///
492 /// # Example (TOML)
493 ///
494 /// ```toml
495 /// [memory.shadow_memory]
496 /// enabled = true
497 /// risk_threshold = 0.75
498 /// risk_halflife_turns = 10
499 /// ```
500 #[serde(default)]
501 pub shadow_memory: TrajectoryRiskAccumulatorConfig,
502 /// Five-signal SYNAPSE retrieval (issue #4374).
503 ///
504 /// When `five_signal.enabled = true`, SYNAPSE recall weights five signals: recency,
505 /// relevance, access frequency, causal distance, and novelty. All new signals default
506 /// to weight `0.0`, preserving exact backward compatibility.
507 #[serde(default)]
508 pub five_signal: FiveSignalConfig,
509 /// Context-Adaptive Memory fidelity scoring (CAM Phase 1, #4547).
510 ///
511 /// When `fidelity.enabled = true`, the heuristic fidelity scorer runs after each
512 /// `apply_prepared_context()` call and assigns `Full / Compressed / Placeholder`
513 /// levels to historical messages. Default: disabled.
514 ///
515 /// # Example (TOML)
516 ///
517 /// ```toml
518 /// [memory.fidelity]
519 /// enabled = false
520 /// w_semantic = 0.3
521 /// w_temporal = 0.3
522 /// w_importance = 0.2
523 /// w_plan = 0.2
524 /// full_threshold = 0.7
525 /// compressed_threshold = 0.3
526 /// compressed_max_tokens = 50
527 /// regrade_threshold = 0.6
528 /// min_query_length = 8
529 /// max_scored_messages = 500
530 /// ```
531 #[serde(default, skip_serializing_if = "Option::is_none")]
532 pub fidelity: Option<crate::fidelity::FidelityConfig>,
533 /// Generic namespaced cross-thread key-value store (spec-080, #6363).
534 ///
535 /// When `store.enabled = true`, `zeph store {get,put,list,delete}` (CLI/slash command)
536 /// and `zeph-orchestration`'s `Command.update` handoff (via `zeph-core`) can read and
537 /// write rows addressed by `(owner_key, namespace, key)`. Default: disabled — zero
538 /// behavior change (FR-A-001).
539 #[serde(default)]
540 pub store: CrossThreadStoreConfig,
541 /// Write-time memory-consent gate (issue #6490, `MemGhost`).
542 ///
543 /// When `consent_gate.enabled = true`, memory writes derived from untrusted content
544 /// (tool output, web scrapes, MCP responses) are gated: the interactive `memory_save`
545 /// tool path requires `Channel::confirm` at or above `confirm_threshold`, and autonomous
546 /// background tool-output writes emit a visible in-turn disclosure note at or above
547 /// `disclose_threshold`. Every write is audit-logged with source attribution when
548 /// `audit_all = true`.
549 #[serde(default)]
550 pub consent_gate: ConsentGateConfig,
551}
552
553fn default_crossover_turn_threshold() -> u32 {
554 20
555}
556
557fn default_key_facts_dedup_threshold() -> f32 {
558 0.95
559}