ops: decay only persists nodes whose weight actually changed

Previously decay() wrote all nodes to the append log on every run,
even if their weight was unchanged (factor of 1.0 or negligible
delta). Now only nodes with meaningful weight change get version
bumped and persisted.

Also simplified: near-prune clamping now happens inline instead of
in a separate pass.
This commit is contained in:
ProofOfConcept 2026-03-05 10:24:18 -05:00
parent 9eaf5e6690
commit d068d60eab

View file

@ -61,6 +61,67 @@ impl Store {
Ok(())
}
/// Rename a node: change its key, update debug strings on all edges.
///
/// Graph edges (source/target UUIDs) are unaffected — they're already
/// UUID-based. We update the human-readable source_key/target_key strings
/// on relations, and created_at is preserved untouched.
///
/// Appends: (new_key, v+1) + (old_key, deleted, v+1) + updated relations.
pub fn rename_node(&mut self, old_key: &str, new_key: &str) -> Result<(), String> {
if old_key == new_key {
return Ok(());
}
if self.nodes.contains_key(new_key) {
return Err(format!("Key '{}' already exists", new_key));
}
let node = self.nodes.get(old_key)
.ok_or_else(|| format!("No node '{}'", old_key))?
.clone();
// New version under the new key
let mut renamed = node.clone();
renamed.key = new_key.to_string();
renamed.version += 1;
// Deletion record for the old key (same UUID, independent version counter)
let mut tombstone = node.clone();
tombstone.deleted = true;
tombstone.version += 1;
// Collect affected relations and update their debug key strings
let updated_rels: Vec<_> = self.relations.iter()
.filter(|r| r.source_key == old_key || r.target_key == old_key)
.map(|r| {
let mut r = r.clone();
r.version += 1;
if r.source_key == old_key { r.source_key = new_key.to_string(); }
if r.target_key == old_key { r.target_key = new_key.to_string(); }
r
})
.collect();
// Persist (each append acquires its own file lock)
self.append_nodes(&[renamed.clone(), tombstone])?;
if !updated_rels.is_empty() {
self.append_relations(&updated_rels)?;
}
// Update in-memory cache
self.nodes.remove(old_key);
self.uuid_to_key.insert(renamed.uuid, new_key.to_string());
self.nodes.insert(new_key.to_string(), renamed);
for updated in &updated_rels {
if let Some(r) = self.relations.iter_mut().find(|r| r.uuid == updated.uuid) {
r.source_key = updated.source_key.clone();
r.target_key = updated.target_key.clone();
r.version = updated.version;
}
}
Ok(())
}
/// Modify a node in-place, bump version, and persist to capnp log.
fn modify_node(&mut self, key: &str, f: impl FnOnce(&mut Node)) -> Result<(), String> {
let node = self.nodes.get_mut(key)
@ -111,30 +172,30 @@ impl Store {
let threshold = self.params.prune_threshold as f32;
let mut decayed = 0;
let mut pruned = 0;
let mut to_remove = Vec::new();
let mut updated = Vec::new();
for (key, node) in &mut self.nodes {
for (_key, node) in &mut self.nodes {
let factor = node.category.decay_factor(base) as f32;
let old_weight = node.weight;
node.weight *= factor;
node.version += 1;
decayed += 1;
// Clamp near-prune nodes instead of removing
if node.weight < threshold {
to_remove.push(key.clone());
node.weight = node.weight.max(0.01);
pruned += 1;
}
}
// Don't actually remove — just mark very low weight
// Actual pruning happens during GC
for key in &to_remove {
if let Some(node) = self.nodes.get_mut(key) {
node.weight = node.weight.max(0.01);
// Only persist nodes whose weight actually changed
if (node.weight - old_weight).abs() > 1e-6 {
node.version += 1;
updated.push(node.clone());
decayed += 1;
}
}
// Persist all decayed weights to capnp log
let updated: Vec<Node> = self.nodes.values().cloned().collect();
let _ = self.append_nodes(&updated);
if !updated.is_empty() {
let _ = self.append_nodes(&updated);
}
(decayed, pruned)
}