From 0b368b7e5887d5dd62e0b40158723bfbe7978257 Mon Sep 17 00:00:00 2001 From: Sam Valladares Date: Sun, 28 Jun 2026 16:17:38 -0500 Subject: [PATCH] fix(audit): round-2 deadlock, lock-contention, trigger-clobber (swarm) Verified main-compatible moderate fixes from the complete sweep: - predictive_retrieval get_proactive_suggestions: clone session_context and drop the read guard before predict_needed_memories re-acquires it (re-entrant RwLock read can deadlock when a writer is queued between) - hippocampal_index create_semantic_associations: snapshot (id, summary) pairs under the read lock, drop it, THEN run the O(n) cosine scan (was blocking all writers for the full scan duration) - prospective_memory check_triggers: don't auto-expire an intention that was JUST triggered this iteration (the expire ran unconditionally after mark_triggered, clobbering a fresh trigger) core 477/0, clippy clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/neuroscience/hippocampal_index.rs | 25 +++++++++++-------- .../src/neuroscience/predictive_retrieval.rs | 17 +++++++++---- .../src/neuroscience/prospective_memory.rs | 7 ++++-- 3 files changed, 32 insertions(+), 17 deletions(-) diff --git a/crates/vestige-core/src/neuroscience/hippocampal_index.rs b/crates/vestige-core/src/neuroscience/hippocampal_index.rs index 5e1a367..378ae9e 100644 --- a/crates/vestige-core/src/neuroscience/hippocampal_index.rs +++ b/crates/vestige-core/src/neuroscience/hippocampal_index.rs @@ -1976,15 +1976,22 @@ impl HippocampalIndex { return Ok(0); } - // Find similar memories - let mut candidates: Vec<(String, f32)> = Vec::new(); - for (id, index) in indices.iter() { - if id == memory_id || index.semantic_summary.is_empty() { - continue; - } + // Snapshot the (id, summary) pairs under the read lock, then DROP the lock + // before the O(n) cosine scan. Holding the read lock across the whole scan + // blocks all writers (index_memory/add_association/migrate_node) for its + // full duration on a large index. + let source_summary = source.semantic_summary.clone(); + let pairs: Vec<(String, Vec)> = indices + .iter() + .filter(|(id, index)| id.as_str() != memory_id && !index.semantic_summary.is_empty()) + .map(|(id, index)| (id.clone(), index.semantic_summary.clone())) + .collect(); + drop(indices); // release read lock BEFORE the expensive scan - let similarity = - self.cosine_similarity(&source.semantic_summary, &index.semantic_summary); + // Find similar memories (lock-free) + let mut candidates: Vec<(String, f32)> = Vec::new(); + for (id, summary) in &pairs { + let similarity = self.cosine_similarity(&source_summary, summary); if similarity >= similarity_threshold { candidates.push((id.clone(), similarity)); } @@ -1994,8 +2001,6 @@ impl HippocampalIndex { candidates.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); candidates.truncate(max_associations); - drop(indices); // Release read lock - // Add associations let mut added = 0; for (target_id, strength) in candidates { diff --git a/crates/vestige-core/src/neuroscience/predictive_retrieval.rs b/crates/vestige-core/src/neuroscience/predictive_retrieval.rs index 77934e1..da56f48 100644 --- a/crates/vestige-core/src/neuroscience/predictive_retrieval.rs +++ b/crates/vestige-core/src/neuroscience/predictive_retrieval.rs @@ -932,12 +932,19 @@ impl PredictiveMemory { /// Get proactive suggestions ("You might also need...") pub fn get_proactive_suggestions(&self, min_confidence: f64) -> Result> { - let model = self - .user_model - .read() - .map_err(|e| PredictiveMemoryError::LockPoisoned(e.to_string()))?; + // Clone the context and DROP the read guard before calling + // predict_needed_memories, which re-acquires user_model.read(). A + // re-entrant read on the same thread can deadlock under std::sync::RwLock + // when a writer is queued between the two acquisitions. + let session_context = { + let model = self + .user_model + .read() + .map_err(|e| PredictiveMemoryError::LockPoisoned(e.to_string()))?; + model.session_context.clone() + }; - let predictions = self.predict_needed_memories(&model.session_context)?; + let predictions = self.predict_needed_memories(&session_context)?; Ok(predictions .into_iter() diff --git a/crates/vestige-core/src/neuroscience/prospective_memory.rs b/crates/vestige-core/src/neuroscience/prospective_memory.rs index 133f17d..e98e09f 100644 --- a/crates/vestige-core/src/neuroscience/prospective_memory.rs +++ b/crates/vestige-core/src/neuroscience/prospective_memory.rs @@ -1279,6 +1279,7 @@ impl ProspectiveMemory { } // Check if triggered + let mut just_triggered = false; if intention .trigger .is_triggered(context, &context.recent_events) @@ -1286,6 +1287,7 @@ impl ProspectiveMemory { { intention.mark_triggered(); triggered.push(intention.clone()); + just_triggered = true; } // Check for deadline escalation @@ -1296,8 +1298,9 @@ impl ProspectiveMemory { } } - // Auto-expire overdue intentions - if self.config.auto_expire && intention.is_overdue() { + // Auto-expire overdue intentions — but never clobber one we JUST + // triggered this iteration (it should fire before it expires). + if self.config.auto_expire && intention.is_overdue() && !just_triggered { intention.status = IntentionStatus::Expired; } }