Skip to main content

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}