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
2 changes: 2 additions & 0 deletions cortex.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ enabled = true
interval_seconds = 60 # How often to run
similarity_threshold = 0.75 # Min cosine similarity for auto-edges
max_edges_per_node = 20 # Cap outgoing similarity edges
decay_enabled = true # Set to false for legal/compliance (nothing fades)
decay_rate_per_day = 0.01 # Ignored when decay_enabled = false
entity_promote_every_n_cycles = 60 # How often to run entity promotion (cycles)
entity_promote_min_agents = 2 # Min agents referencing an entity before promotion

Expand Down
4 changes: 2 additions & 2 deletions crates/cortex-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
//!
//! # Example
//! ```rust,no_run
//! use cortex_client::CortexClient;
//! use cortex_proto::cortex::v1::CreateNodeRequest;
//! use cortex_memory_client::CortexClient;
//! use cortex_memory_client::proto::CreateNodeRequest;
//!
//! #[tokio::main]
//! async fn main() -> anyhow::Result<()> {
Expand Down
12 changes: 6 additions & 6 deletions crates/cortex-core/examples/auto_linker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
//! Run with: cargo run --example auto_linker
//! Note: First run downloads the embedding model (~30MB)

use cortex_core::graph::GraphEngineImpl;
use cortex_core::linker::{AutoLinker, AutoLinkerConfig};
use cortex_core::storage::{RedbStorage, Storage};
use cortex_core::types::*;
use cortex_core::vector::{EmbeddingService, FastEmbedService, HnswIndex, SimilarityConfig};
use cortex_memory_core::graph::GraphEngineImpl;
use cortex_memory_core::linker::{AutoLinker, AutoLinkerConfig};
use cortex_memory_core::storage::{RedbStorage, Storage};
use cortex_memory_core::types::*;
use cortex_memory_core::vector::{EmbeddingService, FastEmbedService, HnswIndex, SimilarityConfig};
use std::sync::{Arc, RwLock};
use tempfile::TempDir;

Expand Down Expand Up @@ -160,7 +160,7 @@ fn main() {
println!("Sample edges created:");
// Collect edges by iterating over all nodes' outgoing edges
let all_nodes = storage
.list_nodes(cortex_core::storage::NodeFilter::new())
.list_nodes(cortex_memory_core::storage::NodeFilter::new())
.unwrap();
let all_edges: Vec<_> = all_nodes
.iter()
Expand Down
2 changes: 1 addition & 1 deletion crates/cortex-core/examples/basic_usage.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use cortex_core::{
use cortex_memory_core::{
Edge, EdgeProvenance, Node, NodeFilter, NodeKind, RedbStorage, Relation, Source, Storage,
};

Expand Down
2 changes: 1 addition & 1 deletion crates/cortex-core/examples/graph_queries.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use cortex_core::{
use cortex_memory_core::{
Edge, EdgeProvenance, GraphEngine, GraphEngineImpl, Node, NodeKind, PathRequest, RedbStorage,
Relation, Source, Storage, TraversalDirection, TraversalRequest, TraversalStrategy,
};
Expand Down
6 changes: 3 additions & 3 deletions crates/cortex-core/examples/vector_search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
//! Run with: cargo run --example vector_search
//! Note: First run downloads the embedding model (~30MB)

use cortex_core::storage::{RedbStorage, Storage};
use cortex_core::types::*;
use cortex_core::vector::{
use cortex_memory_core::storage::{RedbStorage, Storage};
use cortex_memory_core::types::*;
use cortex_memory_core::vector::{
embedding_input, EmbeddingService, FastEmbedService, HnswIndex, VectorIndex,
};
use tempfile::TempDir;
Expand Down
2 changes: 1 addition & 1 deletion crates/cortex-core/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ impl Default for LibraryConfig {
///
/// # Example
/// ```rust,no_run
/// use cortex_core::{Cortex, LibraryConfig};
/// use cortex_memory_core::{Cortex, LibraryConfig};
///
/// let cortex = Cortex::open("./memory.redb", LibraryConfig::default()).unwrap();
/// cortex.store(Cortex::fact("The API uses JWT auth", 0.7)).unwrap();
Expand Down
4 changes: 4 additions & 0 deletions crates/cortex-core/src/linker/auto_linker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ impl<S: Storage, E: EmbeddingService, V: VectorIndex, G: GraphEngine> AutoLinker
let contradiction_detector =
ContradictionDetector::new(config.similarity.contradiction_threshold);

if !config.decay.enabled {
log::info!("Edge decay is DISABLED. Edges will never fade or be deleted due to age.");
}

Ok(Self {
storage,
graph_engine,
Expand Down
12 changes: 12 additions & 0 deletions crates/cortex-core/src/linker/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,12 @@ impl AutoLinkerConfig {
/// Configuration for edge decay
#[derive(Debug, Clone)]
pub struct DecayConfig {
/// Set to false to skip edge decay entirely.
/// When disabled, edge weights never change, edges are never pruned or deleted
/// due to age. Use for legal, compliance, or archival deployments.
/// Default: true.
pub enabled: bool,

/// Base decay rate per day. Default: 0.01 (1% per day).
pub daily_decay_rate: f32,

Expand All @@ -208,6 +214,7 @@ pub struct DecayConfig {
impl Default for DecayConfig {
fn default() -> Self {
Self {
enabled: true,
daily_decay_rate: 0.01,
prune_threshold: 0.1,
delete_threshold: 0.05,
Expand All @@ -223,6 +230,11 @@ impl DecayConfig {
Self::default()
}

pub fn with_enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}

pub fn with_daily_decay_rate(mut self, rate: f32) -> Self {
self.daily_decay_rate = rate;
self
Expand Down
126 changes: 126 additions & 0 deletions crates/cortex-core/src/linker/decay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ impl<S: Storage> DecayEngine<S> {
/// Apply decay to all edges in the graph
/// Returns (pruned_count, deleted_count)
pub fn apply_decay(&self, now: DateTime<Utc>) -> Result<(u64, u64)> {
if !self.config.enabled {
return Ok((0, 0));
}

let mut pruned_count = 0;
let mut deleted_count = 0;

Expand Down Expand Up @@ -337,6 +341,128 @@ mod tests {
let reinforced_edge = storage.get_edge(edge.id).unwrap().unwrap();
assert!(reinforced_edge.updated_at > old_time);
}

#[test]
fn test_decay_skipped_when_disabled() {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("no_decay.redb");
let storage = Arc::new(RedbStorage::open(&db_path).unwrap());

let node1 = Node::new(
NodeKind::new("fact").unwrap(),
"Deposition transcript".into(),
"Key witness testimony from month one".into(),
Source {
agent: "legal".into(),
session: None,
channel: None,
},
0.5,
);
let node2 = Node::new(
NodeKind::new("fact").unwrap(),
"Filing deadline".into(),
"Response due in 30 days".into(),
Source {
agent: "legal".into(),
session: None,
channel: None,
},
0.5,
);
storage.put_node(&node1).unwrap();
storage.put_node(&node2).unwrap();

let mut edge = Edge::new(
node1.id,
node2.id,
Relation::new("related_to").unwrap(),
0.8,
EdgeProvenance::AutoSimilarity { score: 0.8 },
);
// Edge is 365 days old
edge.updated_at = Utc::now() - Duration::days(365);
storage.put_edge(&edge).unwrap();

// Decay DISABLED
let config = DecayConfig {
enabled: false,
..DecayConfig::default()
};
let engine = DecayEngine::new(storage.clone(), config);
let (pruned, deleted) = engine.apply_decay(Utc::now()).unwrap();

assert_eq!(pruned, 0);
assert_eq!(deleted, 0);

// Edge weight unchanged after a full year
let unchanged = storage.get_edge(edge.id).unwrap().unwrap();
assert_eq!(
unchanged.weight, 0.8,
"Edge weight must not change when decay is disabled"
);
}

#[test]
fn test_decay_still_works_when_enabled() {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("enabled_decay.redb");
let storage = Arc::new(RedbStorage::open(&db_path).unwrap());

let node1 = Node::new(
NodeKind::new("fact").unwrap(),
"Node 1".into(),
"Body 1".into(),
Source {
agent: "test".into(),
session: None,
channel: None,
},
0.5,
);
let node2 = Node::new(
NodeKind::new("fact").unwrap(),
"Node 2".into(),
"Body 2".into(),
Source {
agent: "test".into(),
session: None,
channel: None,
},
0.5,
);
storage.put_node(&node1).unwrap();
storage.put_node(&node2).unwrap();

let mut edge = Edge::new(
node1.id,
node2.id,
Relation::new("related_to").unwrap(),
0.8,
EdgeProvenance::AutoSimilarity { score: 0.8 },
);
edge.updated_at = Utc::now() - Duration::days(365);
storage.put_edge(&edge).unwrap();

// Decay ENABLED (default)
let config = DecayConfig::default();
assert!(config.enabled);
let engine = DecayEngine::new(storage.clone(), config);
engine.apply_decay(Utc::now()).unwrap();

// Year-old edge should have decayed significantly
let updated = storage.get_edge(edge.id).unwrap();
// With default rate 0.01/day over 365 days, weight should be near zero or deleted
// Either the edge was deleted or its weight dropped well below 0.8
match updated {
None => {} // deleted — expected for a year-old edge
Some(e) => assert!(
e.weight < 0.8,
"Edge weight should have decayed, got {}",
e.weight
),
}
}
}

#[cfg(test)]
Expand Down
60 changes: 60 additions & 0 deletions crates/cortex-server/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@ pub struct AutoLinkerTomlConfig {
pub interval_seconds: u64,
pub similarity_threshold: f32,
pub dedup_threshold: f32,
/// Set to false to disable edge weight decay entirely.
/// Edges will never fade, weaken, or be deleted due to age.
/// Use for legal, compliance, or archival deployments.
#[serde(default = "default_true")]
pub decay_enabled: bool,
pub decay_rate_per_day: f32,
pub max_edges_per_node: usize,
/// Whether to run legacy hardcoded structural rules.
Expand All @@ -154,6 +159,10 @@ pub struct AutoLinkerTomlConfig {
pub entity_promote_min_agents: usize,
}

fn default_true() -> bool {
true
}

fn default_entity_promote_every_n_cycles() -> u64 {
60
}
Expand All @@ -169,6 +178,7 @@ impl Default for AutoLinkerTomlConfig {
interval_seconds: 60,
similarity_threshold: 0.75,
dedup_threshold: 0.92,
decay_enabled: true,
decay_rate_per_day: 0.01,
max_edges_per_node: 50,
legacy_rules_enabled: None,
Expand Down Expand Up @@ -388,6 +398,7 @@ impl CortexConfig {
)
.with_decay(
cortex_core::DecayConfig::new()
.with_enabled(self.auto_linker.decay_enabled)
.with_daily_decay_rate(self.auto_linker.decay_rate_per_day),
)
.with_embedding_model(self.embedding.model.clone())
Expand Down Expand Up @@ -504,4 +515,53 @@ enabled = true
let errors = config.validate();
assert!(errors.is_empty());
}

#[test]
fn test_decay_enabled_false_parses_from_toml() {
let toml_str = r#"
[auto_linker]
decay_enabled = false
decay_rate_per_day = 0.0
"#;
let config: CortexConfig = toml::from_str(toml_str).unwrap();
assert!(!config.auto_linker.decay_enabled);
assert_eq!(config.auto_linker.decay_rate_per_day, 0.0);
}

#[test]
fn test_decay_enabled_defaults_true_when_missing() {
let toml_str = r#"
[auto_linker]
enabled = true
"#;
let config: CortexConfig = toml::from_str(toml_str).unwrap();
assert!(
config.auto_linker.decay_enabled,
"decay_enabled must default to true for backward compatibility"
);
}

#[test]
fn test_decay_enabled_wired_to_auto_linker_config() {
let toml_str = r#"
[auto_linker]
decay_enabled = false
"#;
let config: CortexConfig = toml::from_str(toml_str).unwrap();
let linker_config = config.auto_linker_config();
assert!(
!linker_config.decay.enabled,
"decay_enabled=false in TOML must propagate to DecayConfig.enabled"
);
}

#[test]
fn test_decay_enabled_true_wired_to_auto_linker_config() {
let config = CortexConfig::default();
let linker_config = config.auto_linker_config();
assert!(
linker_config.decay.enabled,
"Default config must have decay enabled"
);
}
}
3 changes: 0 additions & 3 deletions crates/cortex-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@ mod migration;
mod observability;
mod serve;

#[cfg(feature = "warren")]
mod nats;

use clap::Parser;
use cli::{Cli, Commands};
use config::CortexConfig;
Expand Down
Loading
Loading