Memory Retrieval and Decay: Maintaining Fresh Context
Memory decay is the process of gradually reducing confidence in old memories as time passes. Without decay, an agent would treat a user preference learned months ago the same as one learned yesterday, leading to outdated decisions. Decay is also crucial for forgetting: GDPR "right to be forgotten" and CCPA require deletion of personal data after a retention period. A well-designed decay system balances stability (preserving reliable long-term patterns) and responsiveness (adapting quickly to recent behavior changes).
Why Memory Decay Matters: The Forgetting Curve
The human forgetting curve (Ebbinghaus, 1885) shows that memory strength decays exponentially unless reinforced. Memories fade fastest in the first hours, then more slowly. AI agents exhibit similar patterns: older memories become less reliable without periodic reinforcement.
In production agents, decay serves three functions:
- Compliance: Delete personal data after retention periods (GDPR, CCPA).
- Adaptation: Reduce weight of stale patterns so the agent responds to recent behavior changes.
- Reliability: Mark memories for human review or re-validation if they haven't been reinforced recently.
For example, a user's format preference learned a year ago has lower confidence than one learned last week. If the user's behavior changes (they request a different format), the agent should shift patterns quickly rather than stubbornly maintaining the old preference.
Exponential Decay Models
The simplest decay function is exponential: confidence_today = confidence_initial * exp(-decay_rate * days_elapsed). The decay rate controls how fast confidence fades. Common values:
decay_rate = 0.01: Half-life of ~70 days (moderate decay, good for user preferences).decay_rate = 0.05: Half-life of ~14 days (fast decay, good for trending patterns).decay_rate = 0.001: Half-life of ~700 days (slow decay, good for stable facts like user name).
# Example: Exponential decay implementation
import math
from datetime import datetime, timedelta
class MemoryWithDecay:
def __init__(self, fact, initial_confidence, decay_rate=0.01):
self.fact = fact # e.g., {"user_id": "bob", "predicate": "format_preference", "value": "pdf"}
self.initial_confidence = initial_confidence
self.decay_rate = decay_rate
self.created_at = datetime.now()
self.last_reinforced = datetime.now()
def current_confidence(self, as_of: datetime = None) -> float:
"""Compute confidence with decay applied."""
as_of = as_of or datetime.now()
seconds_elapsed = (as_of - self.last_reinforced).total_seconds()
days_elapsed = seconds_elapsed / (24 * 3600)
decayed = self.initial_confidence * math.exp(-self.decay_rate * days_elapsed)
return max(decayed, 0.0) # Confidence doesn't go below 0
def is_reliable(self, min_confidence=0.50, as_of: datetime = None) -> bool:
"""Check if this memory is still reliable enough to use."""
return self.current_confidence(as_of) >= min_confidence
def reinforce(self, as_of: datetime = None):
"""Update when we see supporting evidence again."""
as_of = as_of or datetime.now()
self.last_reinforced = as_of
# Optionally bump confidence toward initial if it hasn't decayed too much
current = self.current_confidence(as_of)
if current > 0.3: # If still somewhat valid, reinforce
self.initial_confidence = min(self.initial_confidence + 0.05, 1.0)
# Example: Using decay in memory lookup
def get_user_preference(user_id: str, preference_name: str, memory_store, min_confidence=0.60):
"""
Retrieve a user preference, only if confidence is above threshold.
Return None if the memory is too decayed.
"""
memory = memory_store.get(f"{user_id}_{preference_name}")
if memory is None:
return None
if memory.is_reliable(min_confidence):
return memory.fact["value"]
else:
# Confidence too low; suggest re-asking the user
return None # Will trigger agent to ask: "Do you still prefer PDF format?"
Decay with Reinforcement: Learning from Recent Behavior
Decay is more effective when combined with reinforcement: each time we see supporting evidence for a memory, we bump its confidence. This creates a self-reinforcing cycle: reliable memories stay strong; unreliable ones fade.
def observe_user_behavior(
user_id: str,
action: str,
memory_store,
decay_rate=0.01
):
"""
Observe a user action and update relevant memories.
If action matches a memory, reinforce it.
If action contradicts a memory, weaken it.
"""
# Example: User exports as CSV
if action == "export_as_csv":
memory_key = f"{user_id}_format_preference"
memory = memory_store.get(memory_key)
if memory:
if memory.fact["value"] == "csv":
# Reinforce: we were right
memory.reinforce()
print(f"Reinforced: {user_id} prefers CSV (confidence now {memory.current_confidence():.2f})")
else:
# Contradict: we were wrong
# Weaken the old preference and learn the new one
memory.decay_rate *= 2 # Increase decay rate
# Create new memory for CSV preference (start with low-medium confidence)
new_memory = MemoryWithDecay(
{"user_id": user_id, "predicate": "format_preference", "value": "csv"},
initial_confidence=0.40, # Tentative
decay_rate=decay_rate
)
memory_store.set(f"{user_id}_format_preference_csv", new_memory)
print(f"Learned: {user_id} may prefer CSV (low confidence, will reinforce if repeated)")
Decay Strategies for Different Memory Types
Different memory types have different decay rates:
| Memory Type | Half-Life | Use Case | Decay Rate |
|---|---|---|---|
| Working (current task) | Minutes | Active reasoning | N/A (cleared on completion) |
| Episodic (past events) | 7–30 days | Context within weeks | 0.02 (confidence halves every 35 days) |
| Semantic (preferences) | 3–6 months | Personalization | 0.01 (confidence halves every 70 days) |
| Semantic (stable facts) | 1+ years | User identity, roles | 0.001 (confidence halves every 700 days) |
For example, a user's name (stable fact) decays very slowly; a trending topic preference (episodic) decays quickly.
def assign_decay_rate(memory_fact: dict) -> float:
"""Assign decay rate based on memory type and predicate."""
fact_type = memory_fact.get("type", "unknown")
predicate = memory_fact.get("predicate", "")
# Stable facts decay slowly
if predicate in ["user_name", "user_email", "user_id", "account_status"]:
return 0.0001 # Very slow decay
# Preferences decay moderately
elif predicate in ["format_preference", "communication_channel", "urgency_level"]:
return 0.01 # Half-life of 70 days
# Trending patterns decay quickly
elif predicate in ["current_project", "recent_interest", "trending_topic"]:
return 0.05 # Half-life of 14 days
# Episodic events decay quickly
elif fact_type == "episodic":
return 0.02 # Half-life of 35 days
else:
return 0.01 # Default
Retention and Deletion Policies
Compliance requires deletion: GDPR allows 30-day deletion after user request, CCPA allows 45-day window. Design a deletion policy:
class RetentionPolicy:
"""Define how long different types of data are retained."""
def __init__(self):
# Retention in days
self.min_retention = 7 # Keep at least one week for agent learning
self.default_retention = 365 # One year is standard
self.sensitive_retention = 90 # CCPA baseline
self.deletion_grace_period = 30 # Days to process deletion request
def can_delete(self, memory_item: dict) -> bool:
"""Check if a memory item can be safely deleted."""
days_old = (datetime.now() - memory_item["created_at"]).days
sensitivity = memory_item.get("sensitivity", "normal")
if sensitivity == "sensitive":
return days_old >= self.sensitive_retention + self.deletion_grace_period
else:
return days_old >= self.default_retention + self.deletion_grace_period
def mark_for_deletion(self, user_id: str, deletion_request_id: str):
"""Mark a user's data for deletion. Hard-delete after grace period."""
# In practice, set a flag in the database
pass
def handle_gdpr_deletion_request(user_id: str, episodic_store, semantic_store):
"""
Implement GDPR Right to Be Forgotten.
Delete or anonymize all personal data within 30 days.
"""
# Immediate anonymization
episodic_store.anonymize_user(user_id) # Replace name with hash, redact text
semantic_store.delete_user_facts(user_id) # Remove all learned preferences
# Schedule hard deletion after 30 days
from datetime import datetime, timedelta
deletion_date = datetime.now() + timedelta(days=30)
schedule_hard_deletion(user_id, deletion_date)
Memory Auditing and Refresh
Over time, memories may become stale or incorrect. Regular audits catch problems:
def audit_memory_health(memory_store, verbose=False) -> dict:
"""
Audit the quality of stored memories.
Return statistics and flag problematic memories.
"""
stats = {
"total_memories": 0,
"healthy_count": 0, # Confidence >= 0.60
"stale_count": 0, # Confidence 0.30–0.60
"unreliable_count": 0, # Confidence < 0.30
"flagged_for_review": []
}
for memory in memory_store.all():
stats["total_memories"] += 1
confidence = memory.current_confidence()
if confidence >= 0.60:
stats["healthy_count"] += 1
elif confidence >= 0.30:
stats["stale_count"] += 1
else:
stats["unreliable_count"] += 1
stats["flagged_for_review"].append(memory)
if verbose and confidence < 0.30:
print(f"UNRELIABLE: {memory.fact} (confidence {confidence:.2f})")
return stats
def refresh_unreliable_memories(user_id: str, memory_store):
"""
For memories with low confidence, ask the user to confirm/update.
This re-validates the memory or learns a new one.
"""
unreliable = [m for m in memory_store.get_user_memories(user_id)
if m.current_confidence() < 0.40]
for memory in unreliable:
# Generate a question to re-validate
predicate = memory.fact["predicate"]
old_value = memory.fact["value"]
question = f"I have an outdated note that you prefer {predicate}={old_value}. Is that still correct?"
return {
"questions": [question],
"purpose": "Re-validate stale memories with user"
}
Key Takeaways
- Implement exponential decay with a configurable half-life to balance stability (long-term patterns) and responsiveness (recent behavior changes).
- Reinforce memories when you see supporting evidence; weaken them when behavior contradicts them.
- Assign different decay rates to different memory types: stable facts decay very slowly, preferences decay moderately, trends decay quickly.
- Enforce retention and deletion policies for compliance; mark for deletion on GDPR requests and hard-delete after grace period.
- Regularly audit memory health; flag memories with low confidence for human review or refresh.
Frequently Asked Questions
What decay rate should I use for user preferences?
Start with 0.01 (half-life of 70 days). Adjust based on your domain: slower for stable preferences (0.005, half-life 140 days), faster for volatile preferences (0.02, half-life 35 days). Test with A/B: measure agent accuracy with decay vs. without.
Should I decay all memories or just semantic memory?
Decay episodic records (old events become less relevant), semantic facts (learned patterns), and working memory (clear on completion). Don't decay the current task context until the task completes.
What happens when a memory fully decays (confidence = 0)?
Mark it for deletion or archive it. Don't let it take up space indefinitely. After 1 year, most episodic records can be archived; semantic facts below 0.10 confidence can be removed unless they're stable (like user ID).
How do I handle conflicting memories (user said X then later said Y)?
Create separate memories with conflicting values and let recency/reinforcement decide. The newer memory will have higher confidence if the user consistently chooses Y. If they keep switching, both decay and the agent re-asks.
Can I use active learning (re-asking users) to refresh memories?
Yes, and it's powerful. Periodically ask users to confirm stale memories: "Do you still prefer PDF reports?" If they confirm, boost confidence. If they contradict, learn the new preference.
Further Reading
- The Forgetting Curve: Ebbinghaus (1885) — foundational work on memory decay.
- Exponential Decay Models in Agent Learning (2024) — modern applications to LLM agents.
- GDPR Article 17: Right to Be Forgotten — compliance requirements.
- Memory Consolidation in Reinforcement Learning (2023) — stability-plasticity tradeoff in learning systems.