mirror of
https://github.com/samvallad33/vestige.git
synced 2026-07-02 22:01:01 +02:00
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) <noreply@anthropic.com>
This commit is contained in:
parent
81e808dfcb
commit
0b368b7e58
3 changed files with 32 additions and 17 deletions
|
|
@ -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<f32>)> = 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 {
|
||||
|
|
|
|||
|
|
@ -932,12 +932,19 @@ impl PredictiveMemory {
|
|||
|
||||
/// Get proactive suggestions ("You might also need...")
|
||||
pub fn get_proactive_suggestions(&self, min_confidence: f64) -> Result<Vec<PredictedMemory>> {
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue