From dc625ee39761965f199804c18d646ecfcd4ff5fe Mon Sep 17 00:00:00 2001 From: Mike Date: Sat, 11 Apr 2026 19:18:36 +0100 Subject: [PATCH 1/5] feat: add explicit decay disable flag (spec 29) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `enabled` field to `DecayConfig` and `decay_enabled` to TOML config so legal/compliance/archival deployments can explicitly disable edge weight decay. When disabled, `apply_decay()` returns immediately — no iteration, no weight changes, no pruning. - DecayConfig.enabled (default true) with with_enabled() builder - Early return in DecayEngine::apply_decay() when disabled - decay_enabled field in AutoLinkerTomlConfig, wired through - Legal template (cortex init --template legal) disables decay, score decay, and retention TTL - Startup log when decay is disabled - cortex.example.toml and docs/concepts/decay-and-memory.md updated - 11 new tests: unit, config parsing, integration, legal template Co-Authored-By: Claude Opus 4.6 (1M context) --- cortex.example.toml | 2 + crates/cortex-core/src/linker/auto_linker.rs | 4 + crates/cortex-core/src/linker/config.rs | 12 ++ crates/cortex-core/src/linker/decay.rs | 126 +++++++++++++++++ crates/cortex-server/src/cli/init.rs | 73 +++++++++- crates/cortex-server/src/config.rs | 60 +++++++++ .../cortex-server/tests/integration_test.rs | 127 ++++++++++++++++++ docs/concepts/decay-and-memory.md | 28 ++++ 8 files changed, 428 insertions(+), 4 deletions(-) diff --git a/cortex.example.toml b/cortex.example.toml index 0a57dfa..ea162d9 100644 --- a/cortex.example.toml +++ b/cortex.example.toml @@ -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 diff --git a/crates/cortex-core/src/linker/auto_linker.rs b/crates/cortex-core/src/linker/auto_linker.rs index e906f60..0aba4de 100644 --- a/crates/cortex-core/src/linker/auto_linker.rs +++ b/crates/cortex-core/src/linker/auto_linker.rs @@ -74,6 +74,10 @@ impl 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, diff --git a/crates/cortex-core/src/linker/config.rs b/crates/cortex-core/src/linker/config.rs index 300bcf0..676812b 100644 --- a/crates/cortex-core/src/linker/config.rs +++ b/crates/cortex-core/src/linker/config.rs @@ -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, @@ -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, @@ -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 diff --git a/crates/cortex-core/src/linker/decay.rs b/crates/cortex-core/src/linker/decay.rs index a090019..b8c0468 100644 --- a/crates/cortex-core/src/linker/decay.rs +++ b/crates/cortex-core/src/linker/decay.rs @@ -19,6 +19,10 @@ impl DecayEngine { /// Apply decay to all edges in the graph /// Returns (pruned_count, deleted_count) pub fn apply_decay(&self, now: DateTime) -> Result<(u64, u64)> { + if !self.config.enabled { + return Ok((0, 0)); + } + let mut pruned_count = 0; let mut deleted_count = 0; @@ -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)] diff --git a/crates/cortex-server/src/cli/init.rs b/crates/cortex-server/src/cli/init.rs index a0fa95b..10a8d1a 100644 --- a/crates/cortex-server/src/cli/init.rs +++ b/crates/cortex-server/src/cli/init.rs @@ -1,6 +1,7 @@ use crate::config::{ AutoLinkerTomlConfig, BriefingTomlConfig, CortexConfig, EmbeddingConfig, IngestConfig, - ObservabilityConfig, RetentionConfig, SchemaConfig, SecurityConfig, ServerConfig, + ObservabilityConfig, RetentionConfig, ScoreDecayConfig, SchemaConfig, SecurityConfig, + ServerConfig, }; use anyhow::Result; use cortex_core::briefing::BriefingRoleConfig; @@ -73,7 +74,7 @@ fn default_config_rules() -> Vec { ] } -fn roles_for_template(template: &str) -> BriefingRoleConfig { +pub(crate) fn roles_for_template(template: &str) -> BriefingRoleConfig { match template { "coding" => BriefingRoleConfig { identity: vec!["agent".into()], @@ -99,6 +100,26 @@ fn roles_for_template(template: &str) -> BriefingRoleConfig { reviewable: vec!["pattern".into(), "site-profile".into()], superseding: vec!["fact".into(), "screenshot".into()], }, + "legal" => BriefingRoleConfig { + identity: vec!["agent".into()], + persistent: vec![ + "legal-precedent".into(), + "regulation".into(), + "client-profile".into(), + ], + trackable: vec!["case".into(), "filing".into(), "deadline".into()], + temporal: vec![ + "hearing".into(), + "deposition".into(), + "correspondence".into(), + ], + reviewable: vec!["pattern".into(), "argument".into(), "citation".into()], + superseding: vec![ + "case-status".into(), + "client-statement".into(), + "ruling".into(), + ], + }, _ => BriefingRoleConfig::default(), } } @@ -177,6 +198,7 @@ pub async fn run(template: Option<&str>) -> Result<()> { let nats_enabled = ingest_choice == "NATS"; + let is_legal = template == Some("legal"); let roles = roles_for_template(template.unwrap_or("default")); let config = CortexConfig { @@ -195,6 +217,8 @@ pub async fn run(template: Option<&str>) -> Result<()> { auto_linker: AutoLinkerTomlConfig { enabled: autolinker, interval_seconds: autolinker_interval, + decay_enabled: !is_legal, + decay_rate_per_day: if is_legal { 0.0 } else { 0.01 }, rules: default_config_rules(), ..AutoLinkerTomlConfig::default() }, @@ -205,7 +229,12 @@ pub async fn run(template: Option<&str>) -> Result<()> { }, ingest: IngestConfig::default(), observability: ObservabilityConfig::default(), - retention: { + retention: if is_legal { + RetentionConfig { + default_ttl_days: 0, + ..RetentionConfig::default() + } + } else { let mut r = RetentionConfig::default(); r.by_kind.insert( "observation".to_string(), @@ -220,7 +249,14 @@ pub async fn run(template: Option<&str>) -> Result<()> { webhooks: vec![], plugins: vec![], prompt_rollback: Default::default(), - score_decay: Default::default(), + score_decay: if is_legal { + ScoreDecayConfig { + enabled: false, + ..Default::default() + } + } else { + Default::default() + }, write_gate: Default::default(), schemas: Default::default(), trust: None, @@ -241,3 +277,32 @@ pub async fn run(template: Option<&str>) -> Result<()> { Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_legal_template_roles() { + let roles = roles_for_template("legal"); + assert!(roles.persistent.contains(&"legal-precedent".to_string())); + assert!(roles.persistent.contains(&"regulation".to_string())); + assert!(roles.persistent.contains(&"client-profile".to_string())); + assert!(roles.trackable.contains(&"case".to_string())); + assert!(roles.trackable.contains(&"filing".to_string())); + assert!(roles.trackable.contains(&"deadline".to_string())); + assert!(roles.temporal.contains(&"hearing".to_string())); + assert!(roles.temporal.contains(&"deposition".to_string())); + assert!(roles.reviewable.contains(&"citation".to_string())); + assert!(roles.superseding.contains(&"ruling".to_string())); + } + + #[test] + fn test_legal_template_unknown_falls_back_to_default() { + let legal = roles_for_template("legal"); + let default = roles_for_template("unknown"); + // Legal has specific roles, default does not + assert!(legal.persistent.contains(&"regulation".to_string())); + assert!(!default.persistent.contains(&"regulation".to_string())); + } +} diff --git a/crates/cortex-server/src/config.rs b/crates/cortex-server/src/config.rs index b242311..b11ada3 100644 --- a/crates/cortex-server/src/config.rs +++ b/crates/cortex-server/src/config.rs @@ -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. @@ -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 } @@ -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, @@ -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()) @@ -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" + ); + } } diff --git a/crates/cortex-server/tests/integration_test.rs b/crates/cortex-server/tests/integration_test.rs index aeaade9..209d86e 100644 --- a/crates/cortex-server/tests/integration_test.rs +++ b/crates/cortex-server/tests/integration_test.rs @@ -686,12 +686,139 @@ fn test_auto_linker_config_defaults_are_sane() { #[test] fn test_decay_config_defaults_are_sane() { let config = DecayConfig::default(); + assert!(config.enabled, "Decay must be enabled by default"); assert!(config.daily_decay_rate > 0.0 && config.daily_decay_rate <= 1.0); assert!(config.prune_threshold > config.delete_threshold); assert!(config.exempt_manual); assert!(config.validate().is_ok()); } +#[test] +fn test_decay_config_with_enabled_builder() { + let config = DecayConfig::new().with_enabled(false); + assert!(!config.enabled); + // Other fields keep defaults + assert_eq!(config.daily_decay_rate, 0.01); + assert!(config.exempt_manual); + assert!(config.validate().is_ok()); +} + +#[test] +fn test_decay_disabled_full_graph_integration() { + let dir = tempdir().unwrap(); + let db_path = dir.path().join("decay_disabled.redb"); + let storage = Arc::new(RedbStorage::open(&db_path).unwrap()); + + // Create two nodes and an old edge + let n1 = Node::new( + NodeKind::new("fact").unwrap(), + "Deposition transcript".into(), + "Key witness testimony".into(), + make_source("legal-agent"), + 0.5, + ); + let n2 = Node::new( + NodeKind::new("fact").unwrap(), + "Exhibit A".into(), + "Primary evidence document".into(), + make_source("legal-agent"), + 0.5, + ); + storage.put_node(&n1).unwrap(); + storage.put_node(&n2).unwrap(); + + let mut edge = Edge::new( + n1.id, + n2.id, + Relation::new("related_to").unwrap(), + 0.9, + EdgeProvenance::AutoSimilarity { score: 0.9 }, + ); + // 18 months old — typical litigation timeline + edge.updated_at = chrono::Utc::now() - chrono::Duration::days(540); + storage.put_edge(&edge).unwrap(); + + // Run decay with enabled=false + let config = DecayConfig::new().with_enabled(false); + let engine = DecayEngine::new(storage.clone(), config); + let (pruned, deleted) = engine.apply_decay(chrono::Utc::now()).unwrap(); + + assert_eq!(pruned, 0, "No edges should be pruned when decay is disabled"); + assert_eq!(deleted, 0, "No edges should be deleted when decay is disabled"); + + let after = storage.get_edge(edge.id).unwrap().unwrap(); + assert_eq!( + after.weight, 0.9, + "Edge weight must be exactly preserved when decay is disabled" + ); + assert_eq!( + after.updated_at, edge.updated_at, + "Edge timestamp must not change when decay is disabled" + ); + + // Now run with enabled=true — same edge should decay or be deleted + let config_enabled = DecayConfig::default(); + let engine_enabled = DecayEngine::new(storage.clone(), config_enabled); + engine_enabled.apply_decay(chrono::Utc::now()).unwrap(); + + let after_enabled = storage.get_edge(edge.id).unwrap(); + match after_enabled { + None => {} // deleted after 540 days — expected + Some(e) => assert!( + e.weight < 0.9, + "Edge should have decayed with enabled=true, got {}", + e.weight + ), + } +} + +#[test] +fn test_decay_disabled_multiple_cycles_no_change() { + let dir = tempdir().unwrap(); + let db_path = dir.path().join("multi_cycle.redb"); + let storage = Arc::new(RedbStorage::open(&db_path).unwrap()); + + let n1 = Node::new( + NodeKind::new("fact").unwrap(), + "Regulation".into(), + "SOX compliance requirement".into(), + make_source("compliance"), + 0.8, + ); + let n2 = Node::new( + NodeKind::new("fact").unwrap(), + "Filing".into(), + "Annual filing".into(), + make_source("compliance"), + 0.8, + ); + storage.put_node(&n1).unwrap(); + storage.put_node(&n2).unwrap(); + + let mut edge = Edge::new( + n1.id, + n2.id, + Relation::new("related_to").unwrap(), + 0.75, + EdgeProvenance::AutoSimilarity { score: 0.75 }, + ); + edge.updated_at = chrono::Utc::now() - chrono::Duration::days(180); + storage.put_edge(&edge).unwrap(); + + let config = DecayConfig::new().with_enabled(false); + let engine = DecayEngine::new(storage.clone(), config); + + // Run decay 10 times — nothing should change + for _ in 0..10 { + let (p, d) = engine.apply_decay(chrono::Utc::now()).unwrap(); + assert_eq!(p, 0); + assert_eq!(d, 0); + } + + let after = storage.get_edge(edge.id).unwrap().unwrap(); + assert_eq!(after.weight, 0.75); +} + // ── Write Gate Schema Validation ──────────────────────────────────────────── #[test] diff --git a/docs/concepts/decay-and-memory.md b/docs/concepts/decay-and-memory.md index ded8579..32d671b 100644 --- a/docs/concepts/decay-and-memory.md +++ b/docs/concepts/decay-and-memory.md @@ -78,6 +78,34 @@ cortex node create --kind fact --title "Sprint goal: fix auth bug" \ The retention engine sweeps nodes past their `expires_at` during each cycle. +## Disabling Decay + +For domains where knowledge must never fade (legal, compliance, medical, archival), +disable decay entirely: + +```toml +[auto_linker] +decay_enabled = false + +[score_decay] +enabled = false + +[retention] +default_ttl_days = 0 +``` + +Or use the legal template: `cortex init --template legal` + +When decay is disabled: +- Edge weights never change due to age +- Edges are never pruned or deleted due to low weight +- Search results are ranked by pure relevance, not recency +- Nodes are never expired by the retention engine (when TTL is 0) +- A deposition from month one carries the same weight at trial 18 months later + +The auto-linker still runs (creating new edges, detecting contradictions, +promoting entities). Only the decay pass is skipped. + ## Retention Policies Hard retention limits are separate from decay. See [configuration](../getting-started/configuration.md) for `[retention]` settings. From 28dfa5a3098821ba4076ada45560120c3e157f90 Mon Sep 17 00:00:00 2001 From: Mike Date: Sat, 11 Apr 2026 19:21:09 +0100 Subject: [PATCH 2/5] fix: cargo fmt formatting Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/cortex-server/src/cli/init.rs | 2 +- crates/cortex-server/tests/integration_test.rs | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/cortex-server/src/cli/init.rs b/crates/cortex-server/src/cli/init.rs index 10a8d1a..a786527 100644 --- a/crates/cortex-server/src/cli/init.rs +++ b/crates/cortex-server/src/cli/init.rs @@ -1,6 +1,6 @@ use crate::config::{ AutoLinkerTomlConfig, BriefingTomlConfig, CortexConfig, EmbeddingConfig, IngestConfig, - ObservabilityConfig, RetentionConfig, ScoreDecayConfig, SchemaConfig, SecurityConfig, + ObservabilityConfig, RetentionConfig, SchemaConfig, ScoreDecayConfig, SecurityConfig, ServerConfig, }; use anyhow::Result; diff --git a/crates/cortex-server/tests/integration_test.rs b/crates/cortex-server/tests/integration_test.rs index 209d86e..a5d4c3e 100644 --- a/crates/cortex-server/tests/integration_test.rs +++ b/crates/cortex-server/tests/integration_test.rs @@ -743,8 +743,14 @@ fn test_decay_disabled_full_graph_integration() { let engine = DecayEngine::new(storage.clone(), config); let (pruned, deleted) = engine.apply_decay(chrono::Utc::now()).unwrap(); - assert_eq!(pruned, 0, "No edges should be pruned when decay is disabled"); - assert_eq!(deleted, 0, "No edges should be deleted when decay is disabled"); + assert_eq!( + pruned, 0, + "No edges should be pruned when decay is disabled" + ); + assert_eq!( + deleted, 0, + "No edges should be deleted when decay is disabled" + ); let after = storage.get_edge(edge.id).unwrap().unwrap(); assert_eq!( From a90802287d38cb85b364e1d238999176eb849336 Mon Sep 17 00:00:00 2001 From: Mike Date: Sat, 11 Apr 2026 19:30:54 +0100 Subject: [PATCH 3/5] fix: remove dead warren feature flag and NATS adapter wrapper The warren feature was removed from dependencies but cfg guards remained, causing clippy to fail. Remove the warren feature flag, the src/nats/ warren adapter wrapper module, and the warren-gated NATS startup code in serve.rs. The generic ingest/nats adapter is unaffected. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/cortex-server/src/main.rs | 3 -- crates/cortex-server/src/nats/ingest.rs | 33 ----------------- crates/cortex-server/src/nats/mod.rs | 5 --- crates/cortex-server/src/serve.rs | 47 +++---------------------- 4 files changed, 5 insertions(+), 83 deletions(-) delete mode 100644 crates/cortex-server/src/nats/ingest.rs delete mode 100644 crates/cortex-server/src/nats/mod.rs diff --git a/crates/cortex-server/src/main.rs b/crates/cortex-server/src/main.rs index f55e737..9931da4 100644 --- a/crates/cortex-server/src/main.rs +++ b/crates/cortex-server/src/main.rs @@ -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; diff --git a/crates/cortex-server/src/nats/ingest.rs b/crates/cortex-server/src/nats/ingest.rs deleted file mode 100644 index 37116f3..0000000 --- a/crates/cortex-server/src/nats/ingest.rs +++ /dev/null @@ -1,33 +0,0 @@ -use cortex_core::*; -use std::sync::atomic::AtomicU64; -use std::sync::Arc; -use std::sync::RwLock as StdRwLock; - -/// Thin wrapper around WarrenNatsAdapter for backward compatibility. -pub struct NatsIngest { - inner: warren_adapter::WarrenNatsAdapter, -} - -impl NatsIngest { - pub fn new( - client: async_nats::Client, - storage: Arc, - embedding_service: Arc, - vector_index: Arc>, - graph_version: Arc, - ) -> Self { - Self { - inner: warren_adapter::WarrenNatsAdapter::new( - client, - storage, - embedding_service, - vector_index, - graph_version, - ), - } - } - - pub async fn start(&self) -> Result<()> { - self.inner.start().await - } -} diff --git a/crates/cortex-server/src/nats/mod.rs b/crates/cortex-server/src/nats/mod.rs deleted file mode 100644 index ca43d98..0000000 --- a/crates/cortex-server/src/nats/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! Warren NATS integration — delegates to the `warren-adapter` crate. -//! This module exists for backward compatibility during transition. -mod ingest; - -pub use ingest::NatsIngest; diff --git a/crates/cortex-server/src/serve.rs b/crates/cortex-server/src/serve.rs index f32f2a0..0832504 100644 --- a/crates/cortex-server/src/serve.rs +++ b/crates/cortex-server/src/serve.rs @@ -431,48 +431,11 @@ pub async fn run(config: CortexConfig) -> anyhow::Result<()> { }) }; - // Optionally start NATS consumer - let nats_enabled = config.server.nats_enabled; - let nats_url = config.server.nats_url.clone(); - - let nats_task: Option> = if nats_enabled { - info!("Connecting to NATS at {}...", nats_url); - - #[cfg(feature = "warren")] - { - match async_nats::connect(&nats_url).await { - Ok(client) => { - info!("NATS connected (Warren adapter)"); - let nats_ingest = crate::nats::NatsIngest::new( - client, - storage.clone(), - embedding_service.clone(), - vector_index.clone(), - graph_version.clone(), - ); - Some(tokio::spawn(async move { - if let Err(e) = nats_ingest.start().await { - error!("NATS ingest failed: {}", e); - } - })) - } - Err(e) => { - error!("Failed to connect to NATS: {}", e); - error!("Continuing without NATS consumer"); - None - } - } - } - - #[cfg(not(feature = "warren"))] - { - info!("NATS consumer not available (warren feature disabled)"); - None - } - } else { - info!("NATS consumer disabled"); - None - }; + // NATS consumer (reserved for future generic ingest adapter) + let nats_task: Option> = None; + if config.server.nats_enabled { + info!("NATS ingest configured but no adapter available"); + } info!("Cortex server ready"); From f522cb1f1fc1a4364b4dcc4d48aa998a838cda1f Mon Sep 17 00:00:00 2001 From: Mike Date: Sat, 11 Apr 2026 19:37:05 +0100 Subject: [PATCH 4/5] refactor: remove hardcoded legal template, keep decay flag generic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The decay disable feature is purely config-driven (decay_enabled=false in cortex.toml). Remove the legal-specific template with hardcoded briefing roles — users configure their own roles and decay settings without domain-specific coupling. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/cortex-server/src/cli/init.rs | 73 ++-------------------------- docs/concepts/decay-and-memory.md | 2 - 2 files changed, 4 insertions(+), 71 deletions(-) diff --git a/crates/cortex-server/src/cli/init.rs b/crates/cortex-server/src/cli/init.rs index a786527..a0fa95b 100644 --- a/crates/cortex-server/src/cli/init.rs +++ b/crates/cortex-server/src/cli/init.rs @@ -1,7 +1,6 @@ use crate::config::{ AutoLinkerTomlConfig, BriefingTomlConfig, CortexConfig, EmbeddingConfig, IngestConfig, - ObservabilityConfig, RetentionConfig, SchemaConfig, ScoreDecayConfig, SecurityConfig, - ServerConfig, + ObservabilityConfig, RetentionConfig, SchemaConfig, SecurityConfig, ServerConfig, }; use anyhow::Result; use cortex_core::briefing::BriefingRoleConfig; @@ -74,7 +73,7 @@ fn default_config_rules() -> Vec { ] } -pub(crate) fn roles_for_template(template: &str) -> BriefingRoleConfig { +fn roles_for_template(template: &str) -> BriefingRoleConfig { match template { "coding" => BriefingRoleConfig { identity: vec!["agent".into()], @@ -100,26 +99,6 @@ pub(crate) fn roles_for_template(template: &str) -> BriefingRoleConfig { reviewable: vec!["pattern".into(), "site-profile".into()], superseding: vec!["fact".into(), "screenshot".into()], }, - "legal" => BriefingRoleConfig { - identity: vec!["agent".into()], - persistent: vec![ - "legal-precedent".into(), - "regulation".into(), - "client-profile".into(), - ], - trackable: vec!["case".into(), "filing".into(), "deadline".into()], - temporal: vec![ - "hearing".into(), - "deposition".into(), - "correspondence".into(), - ], - reviewable: vec!["pattern".into(), "argument".into(), "citation".into()], - superseding: vec![ - "case-status".into(), - "client-statement".into(), - "ruling".into(), - ], - }, _ => BriefingRoleConfig::default(), } } @@ -198,7 +177,6 @@ pub async fn run(template: Option<&str>) -> Result<()> { let nats_enabled = ingest_choice == "NATS"; - let is_legal = template == Some("legal"); let roles = roles_for_template(template.unwrap_or("default")); let config = CortexConfig { @@ -217,8 +195,6 @@ pub async fn run(template: Option<&str>) -> Result<()> { auto_linker: AutoLinkerTomlConfig { enabled: autolinker, interval_seconds: autolinker_interval, - decay_enabled: !is_legal, - decay_rate_per_day: if is_legal { 0.0 } else { 0.01 }, rules: default_config_rules(), ..AutoLinkerTomlConfig::default() }, @@ -229,12 +205,7 @@ pub async fn run(template: Option<&str>) -> Result<()> { }, ingest: IngestConfig::default(), observability: ObservabilityConfig::default(), - retention: if is_legal { - RetentionConfig { - default_ttl_days: 0, - ..RetentionConfig::default() - } - } else { + retention: { let mut r = RetentionConfig::default(); r.by_kind.insert( "observation".to_string(), @@ -249,14 +220,7 @@ pub async fn run(template: Option<&str>) -> Result<()> { webhooks: vec![], plugins: vec![], prompt_rollback: Default::default(), - score_decay: if is_legal { - ScoreDecayConfig { - enabled: false, - ..Default::default() - } - } else { - Default::default() - }, + score_decay: Default::default(), write_gate: Default::default(), schemas: Default::default(), trust: None, @@ -277,32 +241,3 @@ pub async fn run(template: Option<&str>) -> Result<()> { Ok(()) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_legal_template_roles() { - let roles = roles_for_template("legal"); - assert!(roles.persistent.contains(&"legal-precedent".to_string())); - assert!(roles.persistent.contains(&"regulation".to_string())); - assert!(roles.persistent.contains(&"client-profile".to_string())); - assert!(roles.trackable.contains(&"case".to_string())); - assert!(roles.trackable.contains(&"filing".to_string())); - assert!(roles.trackable.contains(&"deadline".to_string())); - assert!(roles.temporal.contains(&"hearing".to_string())); - assert!(roles.temporal.contains(&"deposition".to_string())); - assert!(roles.reviewable.contains(&"citation".to_string())); - assert!(roles.superseding.contains(&"ruling".to_string())); - } - - #[test] - fn test_legal_template_unknown_falls_back_to_default() { - let legal = roles_for_template("legal"); - let default = roles_for_template("unknown"); - // Legal has specific roles, default does not - assert!(legal.persistent.contains(&"regulation".to_string())); - assert!(!default.persistent.contains(&"regulation".to_string())); - } -} diff --git a/docs/concepts/decay-and-memory.md b/docs/concepts/decay-and-memory.md index 32d671b..68e96e5 100644 --- a/docs/concepts/decay-and-memory.md +++ b/docs/concepts/decay-and-memory.md @@ -94,8 +94,6 @@ enabled = false default_ttl_days = 0 ``` -Or use the legal template: `cortex init --template legal` - When decay is disabled: - Edge weights never change due to age - Edges are never pruned or deleted due to low weight From 7a636bad6a73f619ed89d761d8acd643f635a212 Mon Sep 17 00:00:00 2001 From: Mike Date: Sat, 11 Apr 2026 19:47:54 +0100 Subject: [PATCH 5/5] fix: update crate references after rename, remove legal template - Fix examples to use cortex_memory_core instead of cortex_core - Fix doctests in api.rs and client lib.rs for renamed crates - Remove hardcoded legal template from cortex init (decay disable is generic config, not domain-specific) Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/cortex-client/src/lib.rs | 4 ++-- crates/cortex-core/examples/auto_linker.rs | 12 ++++++------ crates/cortex-core/examples/basic_usage.rs | 2 +- crates/cortex-core/examples/graph_queries.rs | 2 +- crates/cortex-core/examples/vector_search.rs | 6 +++--- crates/cortex-core/src/api.rs | 2 +- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/crates/cortex-client/src/lib.rs b/crates/cortex-client/src/lib.rs index d08e06a..0765eff 100644 --- a/crates/cortex-client/src/lib.rs +++ b/crates/cortex-client/src/lib.rs @@ -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<()> { diff --git a/crates/cortex-core/examples/auto_linker.rs b/crates/cortex-core/examples/auto_linker.rs index 4eb4318..2a67e2d 100644 --- a/crates/cortex-core/examples/auto_linker.rs +++ b/crates/cortex-core/examples/auto_linker.rs @@ -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; @@ -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() diff --git a/crates/cortex-core/examples/basic_usage.rs b/crates/cortex-core/examples/basic_usage.rs index 9e711c4..b91362d 100644 --- a/crates/cortex-core/examples/basic_usage.rs +++ b/crates/cortex-core/examples/basic_usage.rs @@ -1,4 +1,4 @@ -use cortex_core::{ +use cortex_memory_core::{ Edge, EdgeProvenance, Node, NodeFilter, NodeKind, RedbStorage, Relation, Source, Storage, }; diff --git a/crates/cortex-core/examples/graph_queries.rs b/crates/cortex-core/examples/graph_queries.rs index c7fcbc5..2544c36 100644 --- a/crates/cortex-core/examples/graph_queries.rs +++ b/crates/cortex-core/examples/graph_queries.rs @@ -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, }; diff --git a/crates/cortex-core/examples/vector_search.rs b/crates/cortex-core/examples/vector_search.rs index 5962103..dca2769 100644 --- a/crates/cortex-core/examples/vector_search.rs +++ b/crates/cortex-core/examples/vector_search.rs @@ -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; diff --git a/crates/cortex-core/src/api.rs b/crates/cortex-core/src/api.rs index ea2d103..ab9163c 100644 --- a/crates/cortex-core/src/api.rs +++ b/crates/cortex-core/src/api.rs @@ -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();