Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

### Changed

- Internal: `[tier]`, `[dream]` and `[lifecycle]` are read straight into the core types, #1515 part 1 (round 2 of the architecture deepening, #1523; the first step of closing #1355). `uteke_core::{TierConfig, DreamConfig, LifecycleConfig}` now implement `serde::Deserialize` (each key optional, the core default for the rest; negative tier days are still rejected), so the CLI config uses them as its section types and the server drops its three `Option` mirror structs and the field-by-field default chains. `[recall]` and `[embed_fallback]` follow in the next parts. No behaviour change.
- Internal, API: transports no longer reach into the store, #1521 (round 2 of the architecture deepening, #1523). `Uteke::store()` and `Uteke::graph_store()` are no longer public (a breaking change for crates that depend on `uteke-core`); `Uteke` offers `count_active`, `list_deprecated` and `graph()` (the knowledge graph) instead, and `store_for_tests()` exists behind the `test-support` feature for fixtures. The private module of the `Uteke` room operations is now `room_ops`, so it no longer shares a name with `memory::rooms`. No behaviour change.
- **`remember` with `detect_contradiction` writes like plain `remember` (#1514, part 1)** - the two now share one embedding step and one commit step. The contradiction path used to skip the memory type check (an unknown type was stored), embed JSON content as raw text instead of the flattened text, try the embedding once, and fail the whole write when the embedding failed. It now rejects an unknown type, embeds the flattened text, retries like `remember`, and on a persistent embedding failure stores the memory FTS5-only with a warning (the contradiction check is skipped without a vector, as the duplicate check is). Import and consolidation move onto the same commit step in later parts, once uteke-cloud#242 is answered.
- Internal: the redaction flag policy is its own module, #1520 (round 2 of the architecture deepening, #1523): `redaction_flags::resolve` decides which `redacted` / `redactions` flags a memory carries after an update as one pure function with a test per rule, and `update_memory_detailed` loses its inline five-arm `match`. The three flag helpers moved with it and are still reachable as `uteke_core::privacy::{with,without,carry}_redaction_flags`. No behaviour change.
Expand Down
136 changes: 7 additions & 129 deletions crates/uteke-cli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,27 +155,9 @@ impl EmbedFallbackConfig {
}
}

/// Tier configuration for hot/warm/cold memory tiers.
#[derive(serde::Deserialize, Clone)]
#[serde(default)]
pub struct TierConfig {
/// Days before memory moves from hot to warm.
pub hot_days: u32,
/// Days before memory moves from warm to cold.
pub warm_days: u32,
/// Score boost for hot memories.
pub hot_boost: f64,
}

impl Default for TierConfig {
fn default() -> Self {
Self {
hot_days: 7,
warm_days: 30,
hot_boost: 0.1,
}
}
}
/// Tier configuration for hot/warm/cold memory tiers: the core type, so the
/// keys and defaults live in one place (#1515).
pub type TierConfig = uteke_core::TierConfig;

/// Logging configuration.
#[derive(serde::Deserialize, Clone)]
Expand Down Expand Up @@ -331,115 +313,11 @@ impl Default for MaintenanceConfig {
}
}

/// Dream pipeline thresholds (#731). All phases in the dream cycle
/// (contradiction scan, dedup/consolidate, orphan detection) read from
/// these values instead of hardcoded constants.
#[derive(serde::Deserialize, Clone, Copy)]
#[serde(default)]
pub struct DreamConfig {
/// Cosine similarity threshold for contradiction scan. Memories with
/// similarity ABOVE this value are NOT contradictions. Default: 0.6.
pub contradict_similarity_threshold: f32,
/// Minimum Jaccard index for tag overlap to consider contradiction.
/// Default: 0.4 (up from original 0.3 to reduce false positives).
pub contradict_tag_jaccard_min: f32,
/// Maximum memories loaded for O(n²) contradiction scan. Default: 200.
pub contradict_max_memories: usize,
/// Cosine similarity threshold for dedup/consolidate. Memories with
/// similarity ABOVE this value are merge candidates. Default: 0.92.
pub dedup_threshold: f32,
/// Importance threshold for orphan detection. Memories below this AND
/// with no edges are flagged as orphans. Default: 0.15 (safer than 0.3).
pub orphan_importance_threshold: f64,
}
/// Dream pipeline thresholds (#731): the core type (#1515).
pub type DreamConfig = uteke_core::DreamConfig;

impl Default for DreamConfig {
fn default() -> Self {
Self {
contradict_similarity_threshold: 0.6,
contradict_tag_jaccard_min: 0.4,
contradict_max_memories: 200,
dedup_threshold: 0.92,
orphan_importance_threshold: 0.15,
}
}
}

/// Memory lifecycle configuration (#928).
///
/// Controls how memories transition through their lifecycle:
/// `ACTIVE → DEPRECATED (hidden, restorable) → PRUNED (hard delete)`.
///
/// All destructive operations route through soft-delete when
/// `soft_delete_only = true` (the default).
#[derive(serde::Deserialize, Clone)]
#[serde(default)]
pub struct LifecycleConfig {
/// Route all destructive ops through `deprecate()` instead of `delete()`.
/// Default: true. Set to `false` to restore pre-v0.13 hard-delete behavior.
pub soft_delete_only: bool,
/// Whether auto lifecycle cycles are enabled. Default: true.
pub auto_aging_enabled: bool,
/// Hours between automatic lifecycle cycles (server mode). Default: 168 (weekly).
pub auto_aging_interval_hours: u64,
/// Minimum age in days before eligible for deprecation. Default: 90.
pub min_age_days: u32,
/// Maximum access count for "cold" eligibility. Default: 3.
pub max_access_count: u32,
/// Max percentage of total memories deprecable per cycle. Default: 1.0.
pub max_deprecate_percent: f64,
/// Minimum deprecations per cycle. Default: 1.
pub min_deprecate_per_cycle: usize,
/// Hard ceiling on deprecations per cycle. Default: 50.
pub max_deprecate_per_cycle: usize,
/// Days after deprecation before eligible for pruning (hard delete). Default: 30.
pub deprecated_ttl_days: u32,
/// Auto-prune expired deprecated memories during cycles. Default: true.
pub auto_prune_enabled: bool,
/// Dream dedup phase uses soft-delete. Default: true.
pub dream_dedup_soft_delete: bool,
/// Dream compact phase uses soft-delete. Default: true.
pub dream_compact_soft_delete: bool,
}

impl Default for LifecycleConfig {
fn default() -> Self {
Self {
soft_delete_only: true,
auto_aging_enabled: true,
auto_aging_interval_hours: 168,
min_age_days: 90,
max_access_count: 3,
max_deprecate_percent: 1.0,
min_deprecate_per_cycle: 1,
max_deprecate_per_cycle: 50,
deprecated_ttl_days: 30,
auto_prune_enabled: true,
dream_dedup_soft_delete: true,
dream_compact_soft_delete: true,
}
}
}

impl LifecycleConfig {
/// Convert CLI config to core config.
pub fn to_core(&self) -> uteke_core::LifecycleConfig {
uteke_core::LifecycleConfig {
soft_delete_only: self.soft_delete_only,
auto_aging_enabled: self.auto_aging_enabled,
auto_aging_interval_hours: self.auto_aging_interval_hours,
min_age_days: self.min_age_days,
max_access_count: self.max_access_count,
max_deprecate_percent: self.max_deprecate_percent,
min_deprecate_per_cycle: self.min_deprecate_per_cycle,
max_deprecate_per_cycle: self.max_deprecate_per_cycle,
deprecated_ttl_days: self.deprecated_ttl_days,
auto_prune_enabled: self.auto_prune_enabled,
dream_dedup_soft_delete: self.dream_dedup_soft_delete,
dream_compact_soft_delete: self.dream_compact_soft_delete,
}
}
}
/// Memory lifecycle configuration (#928): the core type (#1515).
pub type LifecycleConfig = uteke_core::LifecycleConfig;

/// Full uteke configuration, loaded from `uteke.toml`.
#[derive(serde::Deserialize, Clone)]
Expand Down
16 changes: 3 additions & 13 deletions crates/uteke-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,11 +217,7 @@ fn main() {
model: config.embedding.model.clone(),
dims: config.embedding.dims,
},
uteke_core::TierConfig {
hot_days: config.tier.hot_days as i64,
warm_days: config.tier.warm_days as i64,
hot_boost: config.tier.hot_boost,
},
config.tier,
uteke_core::RecallConfig {
min_score: config.recall.min_score as f32,
},
Expand All @@ -238,15 +234,9 @@ fn main() {
// #719: apply Jaccard weight from config
u.set_jaccard_weight(config.recall.jaccard_weight);
// #731: apply dream pipeline thresholds from config
u.set_dream_config(uteke_core::DreamConfig {
contradict_similarity_threshold: config.dream.contradict_similarity_threshold,
contradict_tag_jaccard_min: config.dream.contradict_tag_jaccard_min,
contradict_max_memories: config.dream.contradict_max_memories,
dedup_threshold: config.dream.dedup_threshold,
orphan_importance_threshold: config.dream.orphan_importance_threshold,
});
u.set_dream_config(config.dream);
// #928: apply lifecycle configuration from config
u.set_lifecycle_config(config.lifecycle.to_core());
u.set_lifecycle_config(config.lifecycle);
// #1464: secret filter on the write path ([privacy] in uteke.toml)
u.set_privacy_config(uteke_core::privacy::load_config());
u
Expand Down
70 changes: 70 additions & 0 deletions crates/uteke-core/src/engine_settings_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//! The `[tier]`, `[dream]` and `[lifecycle]` sections of `uteke.toml` are read
//! straight into the core types (#1515): one schema for the CLI and the server.

use crate::{DreamConfig, LifecycleConfig, TierConfig};

fn parse<T: serde::de::DeserializeOwned>(toml_text: &str) -> Result<T, toml::de::Error> {
toml::from_str(toml_text)
}

#[test]
fn an_empty_section_is_the_documented_default() {
let t: TierConfig = parse("").unwrap();
assert_eq!((t.hot_days, t.warm_days), (7, 30));
assert!((t.hot_boost - 0.1).abs() < f64::EPSILON);

let d: DreamConfig = parse("").unwrap();
assert_eq!(d.contradict_max_memories, 200);
assert!((d.contradict_similarity_threshold - 0.6).abs() < f32::EPSILON);
assert!((d.contradict_tag_jaccard_min - 0.4).abs() < f32::EPSILON);
assert!((d.dedup_threshold - 0.92).abs() < f32::EPSILON);
assert!((d.orphan_importance_threshold - 0.15).abs() < f64::EPSILON);

let l: LifecycleConfig = parse("").unwrap();
assert!(l.soft_delete_only && l.auto_aging_enabled && l.auto_prune_enabled);
assert!(l.dream_dedup_soft_delete && l.dream_compact_soft_delete);
assert_eq!(
(
l.auto_aging_interval_hours,
l.min_age_days,
l.max_access_count
),
(168, 90, 3)
);
assert_eq!(
(
l.min_deprecate_per_cycle,
l.max_deprecate_per_cycle,
l.deprecated_ttl_days
),
(1, 50, 30)
);
assert!((l.max_deprecate_percent - 1.0).abs() < f64::EPSILON);
}

#[test]
fn a_partial_section_keeps_the_default_for_the_keys_it_omits() {
let t: TierConfig = parse("hot_days = 3").unwrap();
assert_eq!((t.hot_days, t.warm_days), (3, 30));
let d: DreamConfig = parse("dedup_threshold = 0.8").unwrap();
assert!((d.dedup_threshold - 0.8).abs() < f32::EPSILON);
assert_eq!(d.contradict_max_memories, 200);
let l: LifecycleConfig = parse("soft_delete_only = false\ndeprecated_ttl_days = 5").unwrap();
assert!(!l.soft_delete_only);
assert_eq!(l.deprecated_ttl_days, 5);
assert_eq!(l.min_age_days, 90);
}

#[test]
fn negative_tier_days_are_rejected_as_before() {
assert!(parse::<TierConfig>("hot_days = -1").is_err());
assert!(parse::<TierConfig>("warm_days = -30").is_err());
// Zero is a number of days.
assert_eq!(parse::<TierConfig>("hot_days = 0").unwrap().hot_days, 0);
}

#[test]
fn a_wrong_type_is_an_error_not_a_silent_default() {
assert!(parse::<LifecycleConfig>("min_age_days = \"ninety\"").is_err());
assert!(parse::<DreamConfig>("contradict_max_memories = -5").is_err());
}
28 changes: 25 additions & 3 deletions crates/uteke-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ pub mod consolidation_plan;
pub mod dream;
mod edges;
mod embed;
#[cfg(test)]
mod engine_settings_tests;
mod error;
#[cfg(test)]
mod error_class_tests;
Expand Down Expand Up @@ -211,15 +213,33 @@ use std::os::unix::fs::PermissionsExt;

/// Configuration for memory tier thresholds.
///
/// A number of days read from `uteke.toml`: zero or more, as the `u32` the
/// config sections used before they shared this type.
fn non_negative_days<'de, D>(deserializer: D) -> Result<i64, D::Error>
where
D: serde::Deserializer<'de>,
{
let days = <i64 as serde::Deserialize>::deserialize(deserializer)?;
if days < 0 {
return Err(serde::de::Error::custom(
"days must be zero or more (a negative value is not a number of days)",
));
}
Ok(days)
}

/// Controls how memories are classified into hot/warm/cold tiers
/// and how hot memories are boosted in recall scoring.
///
/// Defaults match the hardcoded values used before config wiring (#127).
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, serde::Deserialize)]
#[serde(default)]
pub struct TierConfig {
/// Days before memory moves from hot → warm.
#[serde(deserialize_with = "non_negative_days")]
pub hot_days: i64,
/// Days before memory moves from warm → cold.
#[serde(deserialize_with = "non_negative_days")]
pub warm_days: i64,
/// Score boost added to hot memories during recall.
pub hot_boost: f64,
Expand Down Expand Up @@ -251,7 +271,8 @@ impl Default for RecallConfig {
/// Dream pipeline thresholds (#731). All phases in the dream cycle
/// (contradiction scan, dedup/consolidate, orphan detection) read from
/// these values instead of hardcoded constants.
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy, Debug, serde::Deserialize)]
#[serde(default)]
pub struct DreamConfig {
/// Cosine similarity threshold for contradiction scan. Memories with
/// similarity ABOVE this value are NOT contradictions. Default: 0.6.
Expand Down Expand Up @@ -307,7 +328,8 @@ impl Default for DreamConfig {
/// dream_dedup_soft_delete = true # dream dedup → deprecate, not delete
/// dream_compact_soft_delete = true # dream compact → deprecate, not delete
/// ```
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy, Debug, serde::Deserialize)]
#[serde(default)]
pub struct LifecycleConfig {
/// When `true`, all destructive operations route through `deprecate()`
/// instead of `delete()`. Set to `false` to restore pre-v0.13 hard-delete
Expand Down
Loading
Loading