v4.1: Add gentle memory decay (1%/day) and pruning (30 days stale)
Browse files
mnemo.py
CHANGED
|
@@ -402,6 +402,11 @@ class TieredMemoryManager:
|
|
| 402 |
EVICTION_THRESHOLD = 0.2
|
| 403 |
LOOP_MERGE_THRESHOLD = 0.8
|
| 404 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 405 |
def __init__(self, tuner: SelfTuner):
|
| 406 |
self.tuner = tuner
|
| 407 |
|
|
@@ -413,7 +418,9 @@ class TieredMemoryManager:
|
|
| 413 |
self.stats = {
|
| 414 |
"promotions": 0,
|
| 415 |
"demotions": 0,
|
| 416 |
-
"evictions": 0
|
|
|
|
|
|
|
| 417 |
}
|
| 418 |
|
| 419 |
def add_to_tier(self, memory: Memory, tier: MemoryTier):
|
|
@@ -540,6 +547,59 @@ class TieredMemoryManager:
|
|
| 540 |
"""Get all memories across tiers"""
|
| 541 |
return {**self.semantic_memory, **self.working_memory}
|
| 542 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 543 |
def get_tier_stats(self) -> Dict:
|
| 544 |
"""Get tier statistics"""
|
| 545 |
return {
|
|
@@ -709,6 +769,30 @@ class NeuralLinkManager:
|
|
| 709 |
|
| 710 |
return len(to_prune)
|
| 711 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 712 |
def get_stats(self) -> Dict:
|
| 713 |
return {
|
| 714 |
"total_links": len(self.links),
|
|
@@ -1047,13 +1131,27 @@ class Mnemo:
|
|
| 1047 |
self.link_manager.decay_links()
|
| 1048 |
|
| 1049 |
# Prune weak links
|
| 1050 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1051 |
|
| 1052 |
# Auto-tune parameters
|
| 1053 |
adjustments = self.tuner.auto_tune()
|
| 1054 |
|
| 1055 |
return {
|
| 1056 |
-
"links_pruned":
|
|
|
|
|
|
|
| 1057 |
"parameter_adjustments": adjustments
|
| 1058 |
}
|
| 1059 |
|
|
|
|
| 402 |
EVICTION_THRESHOLD = 0.2
|
| 403 |
LOOP_MERGE_THRESHOLD = 0.8
|
| 404 |
|
| 405 |
+
# Memory decay settings (gentle)
|
| 406 |
+
MEMORY_DECAY_RATE = 0.01 # 1% quality decay per day for unused memories
|
| 407 |
+
MEMORY_PRUNE_THRESHOLD = 0.15 # Prune memories below this quality
|
| 408 |
+
MEMORY_STALE_DAYS = 30 # Consider memory stale after this many days unused
|
| 409 |
+
|
| 410 |
def __init__(self, tuner: SelfTuner):
|
| 411 |
self.tuner = tuner
|
| 412 |
|
|
|
|
| 418 |
self.stats = {
|
| 419 |
"promotions": 0,
|
| 420 |
"demotions": 0,
|
| 421 |
+
"evictions": 0,
|
| 422 |
+
"memories_decayed": 0,
|
| 423 |
+
"memories_pruned": 0
|
| 424 |
}
|
| 425 |
|
| 426 |
def add_to_tier(self, memory: Memory, tier: MemoryTier):
|
|
|
|
| 547 |
"""Get all memories across tiers"""
|
| 548 |
return {**self.semantic_memory, **self.working_memory}
|
| 549 |
|
| 550 |
+
def decay_memories(self) -> int:
|
| 551 |
+
"""
|
| 552 |
+
Apply gentle quality decay to unused semantic memories.
|
| 553 |
+
Memories that are accessed stay fresh; unused ones gradually decay.
|
| 554 |
+
Returns number of memories affected.
|
| 555 |
+
"""
|
| 556 |
+
now = time.time()
|
| 557 |
+
affected = 0
|
| 558 |
+
|
| 559 |
+
for memory in self.semantic_memory.values():
|
| 560 |
+
# Calculate days since last access
|
| 561 |
+
days_unused = (now - memory.last_accessed) / 86400 # seconds per day
|
| 562 |
+
|
| 563 |
+
if days_unused > 1: # Only decay if unused for >1 day
|
| 564 |
+
# Gentle decay: quality *= (1 - decay_rate * days_unused)
|
| 565 |
+
# Capped to prevent instant destruction
|
| 566 |
+
decay_factor = min(days_unused * self.MEMORY_DECAY_RATE, 0.1)
|
| 567 |
+
memory.quality_score *= (1 - decay_factor)
|
| 568 |
+
affected += 1
|
| 569 |
+
|
| 570 |
+
return affected
|
| 571 |
+
|
| 572 |
+
def prune_stale_memories(self) -> Tuple[int, List[str]]:
|
| 573 |
+
"""
|
| 574 |
+
Remove memories that have decayed below threshold.
|
| 575 |
+
Returns (count_pruned, list_of_pruned_ids).
|
| 576 |
+
"""
|
| 577 |
+
now = time.time()
|
| 578 |
+
to_prune = []
|
| 579 |
+
|
| 580 |
+
for mem_id, memory in self.semantic_memory.items():
|
| 581 |
+
days_unused = (now - memory.last_accessed) / 86400
|
| 582 |
+
|
| 583 |
+
# Prune if: quality too low AND unused for too long
|
| 584 |
+
if (memory.quality_score < self.MEMORY_PRUNE_THRESHOLD and
|
| 585 |
+
days_unused > self.MEMORY_STALE_DAYS):
|
| 586 |
+
to_prune.append(mem_id)
|
| 587 |
+
|
| 588 |
+
# Remove pruned memories
|
| 589 |
+
pruned_ids = []
|
| 590 |
+
for mem_id in to_prune:
|
| 591 |
+
del self.semantic_memory[mem_id]
|
| 592 |
+
pruned_ids.append(mem_id)
|
| 593 |
+
|
| 594 |
+
return len(pruned_ids), pruned_ids
|
| 595 |
+
|
| 596 |
+
def refresh_memory(self, memory_id: str):
|
| 597 |
+
"""Mark a memory as freshly accessed (resets decay)"""
|
| 598 |
+
if memory_id in self.semantic_memory:
|
| 599 |
+
self.semantic_memory[memory_id].last_accessed = time.time()
|
| 600 |
+
elif memory_id in self.working_memory:
|
| 601 |
+
self.working_memory[memory_id].last_accessed = time.time()
|
| 602 |
+
|
| 603 |
def get_tier_stats(self) -> Dict:
|
| 604 |
"""Get tier statistics"""
|
| 605 |
return {
|
|
|
|
| 769 |
|
| 770 |
return len(to_prune)
|
| 771 |
|
| 772 |
+
def remove_links_for_memory(self, memory_id: str) -> int:
|
| 773 |
+
"""Remove all links connected to a memory (when memory is pruned)"""
|
| 774 |
+
to_remove = []
|
| 775 |
+
|
| 776 |
+
# Find all links involving this memory
|
| 777 |
+
for link_id, link in self.links.items():
|
| 778 |
+
if link.source_id == memory_id or link.target_id == memory_id:
|
| 779 |
+
to_remove.append(link_id)
|
| 780 |
+
|
| 781 |
+
# Remove them
|
| 782 |
+
for link_id in to_remove:
|
| 783 |
+
link = self.links.pop(link_id)
|
| 784 |
+
self.outgoing[link.source_id].discard(link_id)
|
| 785 |
+
self.incoming[link.target_id].discard(link_id)
|
| 786 |
+
self.stats["links_pruned"] += 1
|
| 787 |
+
|
| 788 |
+
# Clean up empty entries
|
| 789 |
+
if memory_id in self.outgoing:
|
| 790 |
+
del self.outgoing[memory_id]
|
| 791 |
+
if memory_id in self.incoming:
|
| 792 |
+
del self.incoming[memory_id]
|
| 793 |
+
|
| 794 |
+
return len(to_remove)
|
| 795 |
+
|
| 796 |
def get_stats(self) -> Dict:
|
| 797 |
return {
|
| 798 |
"total_links": len(self.links),
|
|
|
|
| 1131 |
self.link_manager.decay_links()
|
| 1132 |
|
| 1133 |
# Prune weak links
|
| 1134 |
+
links_pruned = self.link_manager.prune_weak_links()
|
| 1135 |
+
|
| 1136 |
+
# Decay memory quality (gentle)
|
| 1137 |
+
memories_decayed = self.memory_manager.decay_memories()
|
| 1138 |
+
self.memory_manager.stats["memories_decayed"] += memories_decayed
|
| 1139 |
+
|
| 1140 |
+
# Prune stale memories
|
| 1141 |
+
memories_pruned, pruned_ids = self.memory_manager.prune_stale_memories()
|
| 1142 |
+
self.memory_manager.stats["memories_pruned"] += memories_pruned
|
| 1143 |
+
|
| 1144 |
+
# Clean up links to pruned memories
|
| 1145 |
+
for mem_id in pruned_ids:
|
| 1146 |
+
self.link_manager.remove_links_for_memory(mem_id)
|
| 1147 |
|
| 1148 |
# Auto-tune parameters
|
| 1149 |
adjustments = self.tuner.auto_tune()
|
| 1150 |
|
| 1151 |
return {
|
| 1152 |
+
"links_pruned": links_pruned,
|
| 1153 |
+
"memories_decayed": memories_decayed,
|
| 1154 |
+
"memories_pruned": memories_pruned,
|
| 1155 |
"parameter_adjustments": adjustments
|
| 1156 |
}
|
| 1157 |
|