DEV Community

Cover image for I Built a Memory Layer for LLM Agents That Knows Which Facts Go Stale
Richard Emate
Richard Emate

Posted on Edited on Originally published at github.com

I Built a Memory Layer for LLM Agents That Knows Which Facts Go Stale

VoltMem didn't start because I kept hitting bugs in production agents.

It started with a conversation about how memory actually works — why some beliefs stick for decades while others evaporate in hours, and what triggers the audit when an old calibration stops matching present reality. That led to continual-learning research on the stability–plasticity tradeoff, and then to a structural parallel in agent memory: most layers treat every fact the same at write and search time.


The Berlin → Paris Problem
Most memory systems treat "I live in Berlin" (volatile) with the same protection as "I prefer concise answers" (stable). VoltMem differentiates them by domain.

Your AI assistant knows you live in Berlin. You moved to Paris three months ago. It still thinks you live in Berlin. Meanwhile, the fact that you prefer concise answers — stable for years — gets the same grip as "currently working on a database migration," which you finished last week.

Everything is stored the same way. Everything decays (or doesn't) at the same rate. There's no concept of how volatile a piece of knowledge actually is.

Mem0 remembers relevant facts. VoltMem remembers current truth.

Table of Contents

The core insight

Think about how different types of facts actually behave over time:

  • Your personality traits — barely change over decades
  • Core preferences (communication style, aesthetic sensibilities) — stable for years
  • Your job — changes every few years
  • What you're currently working on — changes weekly
  • Your mood right now — changes hourly

An LLM memory system that treats all of these with the same protection strength makes systematic errors in a predictable direction: it holds volatile facts too long (stale knowledge), or overwrites stable facts on thin evidence (corrupted knowledge). You can't fix both with one dial.

What you need is domain-aware protection — and, at search time, down-ranking of stale volatile memories even when they're semantically close.


The math (stay with me, it's not that bad)

Protection weight (per domain):

wd=1Vdγ w_d = \frac{1}{V_d^{\gamma}}

Write / audit decision — escalate (audit + update) when Et>θtE_t > \theta_t :

Et=[MtRtCα]VdGt θt=θ0VdLt \begin{aligned} E_t &= \left[\frac{M_t \cdot R_t}{C^{\alpha}}\right] \cdot V_d \cdot G_t \ \theta_t &= \frac{\theta_0}{V_d} \cdot L_t \end{aligned}

Where:

  • MtM_t — how strongly the new observation contradicts what's stored (0 to 1)
  • RtR_t — how reliable the source is (explicit user statement vs. weak inference)
  • CC — how many times this memory has been confirmed (repetition count)
  • α\alpha — entrenchment exponent (how hard history fights back)
  • VdV_d — domain volatility prior (how fast does this type of fact change?)
  • GtG_t — goal-attainment delta (is updating this memory actually useful?)
  • LtL_t — cognitive load

High-volatility domain → low threshold → easy to update.
Low-volatility domain → high threshold → hard to update.

Retrieval — down-rank stale volatile memories:

score=similarity(1Vdstaleness) \text{score} = \text{similarity} \cdot \left(1 - V_d \cdot \text{staleness}\right)

The practical effect: a confident blip like "the user seemed extroverted today" won't overwrite a deeply confirmed "user is introverted" — but "user moved to Paris" will cleanly supersede "user lives in Berlin" because location is a volatile domain where a single explicit statement clears the update bar easily.

On Split-MNIST, this isn't a free-lunch accuracy booster. It's a validated control knob: run the same pipeline with volatility priors shuffled or inverted, and the ordering breaks (REAL > SHUFFLE > SWAP). Pre-arXiv draft: volatility_ewc_portfolio.pdf. Full reproduction: docs/RESEARCH.md.


What it looks like in practice

I compared VoltMem against Mem0 (open-source LLM memory) on three concrete scenarios — a case study, not a leaderboard claim:

Scenario 1: Location update
User says they moved from Berlin to Paris.

Mem0 VoltMem
Result Stale "Berlin" stored, 2 conflicting facts Updated to "Paris", 1 clean fact

Scenario 2: Stable preference blip
User says they "really like short replies" in one session, contradicting an established preference for thorough explanations.

Mem0 VoltMem
Result Adopts the blip Keeps original (resists weak contradicting evidence)

Scenario 3: Volatile mood
User's mood shifts from "great" to "stressed".

Mem0 VoltMem
Result Stale "great" persists Updates to "stressed"

VoltMem: 3/3 current top answer on these scripted scenarios. Challenge the scripts:

python experiments/mem0_side_by_side.py
Enter fullscreen mode Exit fullscreen mode

Retrieval haystack (same chunks, different ranker): cosine-only returns the stale fact first 20% of the time; VoltMem 0% stale@1.

python experiments/retrieval_haystack_bench.py
Enter fullscreen mode Exit fullscreen mode

LongMemEval-S (n=60): 70% answer@5ties cosine, does not beat it. If your only metric is public benchmark SOTA, this isn't the pitch. The pitch is update policy + retrieval freshness on mixed-volatility personal memory.

python experiments/longmemeval.py --split s --per-type 10
Enter fullscreen mode Exit fullscreen mode

Using it

pip install voltmem[embeddings]
Enter fullscreen mode Exit fullscreen mode

Core library: zero required dependencies. Embeddings optional (sentence-transformers).

from voltmem import create_memory

mem = create_memory("app.db", user_id="alice")

mem.add("I live in Berlin")
mem.add("I prefer concise, direct answers")
mem.add("Actually I moved to Paris last month")   # updates location, not prefs

hits = mem.search("where does the user live?", limit=3)
print(hits[0]["memory"])   # "Actually I moved to Paris last month"
Enter fullscreen mode Exit fullscreen mode

Inject into any LLM system:

memories = mem.search(user_message, limit=5)
context = "\n".join(f"- {m['memory']}" for m in memories)
system = f"What you know about this user:\n{context}"
Enter fullscreen mode Exit fullscreen mode

Built-in domain volatility priors

VoltMem ships with sensible defaults you can override:

Domain Volatility Behavior
personality_trait 0.05 Strongly protected
core_preference 0.08 Strongly protected
biographical 0.10 High protection
professional_context 0.30 Medium — changes every few years
current_project 0.55 Updates readily
emotional_context 0.80 Fast-moving
current_task 0.90 Minimal protection

Custom domains:

from voltmem import create_memory, DomainRegistry

domains = DomainRegistry()
domains.register("client_relationship", 0.35)
domains.register("active_deal_stage", 0.70)

mem = create_memory("crm.db", user_id="rep_01", domains=domains)
Enter fullscreen mode Exit fullscreen mode

The priors are hand-tuned today — that's an open gap, and one of the places I'd most like real-world feedback.


LangChain integration

pip install voltmem[langchain]
Enter fullscreen mode Exit fullscreen mode
from voltmem.integrations.langchain import VoltMemMemory

memory = VoltMemMemory(session_id="user-42", db_path="app.db")
memory.load_memory_variables({"input": "Where do I live?"})
memory.save_context({"input": "I moved to Paris"}, {"output": "Noted."})
Enter fullscreen mode Exit fullscreen mode

Where this came from

This grew out of a philosophical conversation about how human minds handle stale beliefs — when to trust an old habit and when to question it.

The observation: animals mostly rely on impulses and simple reinforcement to build routines. Human minds add a monitoring layer on top — but that layer can go wrong when calibrated by social contexts that no longer apply. An old rule, reinforced enough times in a specific environment, can feel like an unquestionable fact even when the environment has fundamentally changed.

That maps almost exactly onto the LLM memory problem. A memory system calibrated by early conversation data can become rigid in the same way — protecting old "truths" that are now stale because they were confirmed enough times in the past.

The escalation equations above formalize the same idea: use historical reinforcement as one input, but also factor in domain volatility, source reliability, and actual mismatch — rather than letting any one factor dominate. The continual-learning experiments validated that as a causal control knob; VoltMem is the engineering artifact applied to agent context memory.


What's next

  • More benchmark scenarios — expanding beyond the 3-scenario Mem0 comparison, including cases where VoltMem loses
  • Smarter domain classification — heuristics and optional Ollama LLM work today, but priors are hand-tuned and new domains still need manual registration; next up is better defaults, cloud LLM support, and inferring domain + volatility from context
  • Async support — sync store today; most production LLM apps are async
  • arXiv preprint — theoretical foundations written up; submission in progress

Try it

pip install voltmem[embeddings]
python examples/contradiction_demo.py
python -m examples.chat_app
Enter fullscreen mode Exit fullscreen mode

If you're building anything with persistent LLM memory, I'd genuinely like to hear how the stale-knowledge problem shows up in practice — the use cases I haven't thought of are usually the most interesting ones.

Check out VoltMem on GitHub and leave a Star!

Top comments (7)

Collapse
 
max_quimby profile image
Max Quimby

The Berlin→Paris framing nails something most memory layers get wrong: they treat "I live in Berlin" and "I prefer concise answers" with identical protection, so they systematically hold volatile facts too long and overwrite stable ones on thin evidence. Making volatility a first-class dimension is the right call.

The part I'd push on is classification, because that's where I've watched systems like this get brittle in practice: assigning a fact's domain/volatility at write time is itself an LLM judgment that drifts. Are your volatility priors static per-domain, or learned from how often a fact actually gets contradicted? The second one is more robust but has a cold-start problem. And there's a nasty edge case in the down-ranking: a "stable" fact that genuinely changes — a job change, a move — needs to overcome its own high protection weight. Does a strong contradiction signal at write time let a stable fact update fast, or does the protection that prevents corruption also delay legitimate updates? That tension between "don't corrupt on noise" and "don't miss a real change" feels like the actual core of the problem.

Collapse
 
rouche01 profile image
Richard Emate

These are very valid points. In the case of classification, you're right that assigning a domain at write time is its own judgment call, and it can be wrong. Today the default is keyword heuristics, with an optional Ollama LLM path, and my plan is to push further on LLM-based classification rather than rely on keywords long term. But that doesn't really eliminate the brittleness. An LLM assigning domain/volatility at write time is still a judgment that can drift. On top of the initial hand-tuned domain volatility priors, the library can optionally learn from confirm/contradiction patterns when auto_discover=True, blending in how often facts in each domain get confirmed vs contradicted or superseded. That helps tune volatility over time, but it still has a cold-start problem and doesn't fix a wrong domain label at write time. For apps where you already know the useful domain types, registering custom domains upfront (volatility priors and keyword rules via DomainRegistry) can help a lot in practice, though it doesn't remove the write-time judgment problem entirely. So classification stays the hard part either way.

On stable facts that genuinely change: this is the tension I care about most. The model is meant to block noisy updates on stable facts but still allow real ones when the signal is strong enough (explicit statement + high contradiction). Berlin → Paris works because location is treated as volatile. But a real job or career change hits a more protected domain, and you're right to ask whether protection against noise also delays legitimate updates.

I checked the eval script on exactly that case ("stable domain + strong explicit evidence") and it currently fails. The fact gets logged as a mismatch but not updated. So you've pointed at a real gap, not a theoretical one.

The core tradeoff really is: don't corrupt on noise vs don't miss a real change. That's the heart of it. Working on fixes (stronger explicit-override path, cumulative mismatch escalation).

Curious if you've seen anything that actually reduces that drift in practice, whether LLM-based or not. Would love to hear what you've tried. Thanks for reading it this carefully.

Collapse
 
motedb profile image
mote

The domain-aware volatility framing is exactly the right way to think about this problem. Most memory layers treat staleness as a scalar TTL or a fixed decay rate, which forces you to choose between over-forgetting stable facts and under-forgetting volatile ones. Your approach — using a per-domain prior and a contradiction-weighted audit threshold — is closer to how human memory actually works: we do not forget our native language at the same rate we forget what we had for lunch.

I want to push on the retrieval scoring function for a second. You down-rank stale volatile memories by similarity * (1 - V_d * staleness), which works well when the retrieval query is semantically close to the target memory. But in practice, a lot of agent queries are under-specified — something like "what was I working on?" does not have a single semantic anchor, so the similarity score is already spread across a broad set of candidates. In that case, a high-volatility memory that is slightly more similar can still outrank a stable memory that is the better answer, because the similarity gap is smaller than the volatility penalty. Have you seen this in the LongMemEval-S data? I wonder if a second-stage reranker that explicitly scores "answerability" — not just semantic proximity — would help on those under-specified prompts.

The other thing that struck me is the write-path assumption: every new observation is classified into a domain, and that classification gates the protection weight. In my experience with embedded agents — think drones or robots that collect sensor data, not just chat logs — a single observation is often multimodal: it might contain a GPS fix (location), an IMU reading (mood/activity proxy), and a user voice command (task) all in the same event. Treating each modality as a separate domain makes sense, but the underlying storage then has to keep a unified view of the event while supporting per-domain TTL and audit policies. That is where a unified multimodal store starts to matter — not as a replacement for the memory-layer logic, but as the substrate that can hold the raw event, the derived embeddings, and the structured state in one place, each with its own expiration semantics. We have been experimenting with that pattern in moteDB for exactly this reason: a robot needs to know that a corridor map (stable) and a battery estimate (volatile) came from the same 50ms tick, even if they decay at different rates.

One practical question: the hand-tuned domain priors are a clear starting point, but do you have any signal from real-world logs about how far off the defaults are? I would love to see a histogram of how often the audit threshold actually fires per domain — it would tell us which priors are too conservative and which are too permissive.

Collapse
 
rouche01 profile image
Richard Emate

Thanks, this is a sharp read of where my approach helps and where it still falls short.

On retrieval: you’re right that similarity × (1 − V_d · staleness) works best when the query is specific enough that similarity already separates good candidates from noise. On under-specified prompts (“what was I working on?”), scores flatten across many memories, and the volatility term can tip ranking the wrong way — a slightly closer volatile item beating a stabler, better answer. That’s a retrieval-time issue, not a write-time one. Our LongMemEval-S results are consistent with that picture: volatility helps on some axes, but overall we tie plain cosine rather than clearly beating it. I've logged this as an open problem (under-specified retrieval / similarity plateaus): measure specific vs vague queries separately first, and only then consider answerability-style reranking or adaptive penalties when top-k similarity is flat — without throwing away domain-aware freshness.

On multimodal / multi-facet events: I agree this shouldn’t stay a chat-only memory layer. The volatility idea is modality-agnostic — a corridor map and a battery reading from the same tick should decay differently. The gap is API and item shape: one observation can yield several domain-tagged facets that stay linked (e.g. via event_id) while keeping independent priors, audit, and staleness. I'd like to treat VoltMem as the policy layer (protection + freshness), not as a multimodal database; storage and encoders stay pluggable. I've also logged this as a scope-expansion open problem, with multi-write / event linkage as the first step.

On priors: the hand-tuned defaults are a starting point, and auto_discover can blend in empirical rates, but we don’t yet expose a clear per-domain view of how often the audit threshold actually fires versus logged mismatches and confirms. Thanks to you, I'll add this as an enhancement (prior calibration telemetry): export audit/mismatch/confirm rates per domain so we can see which priors are too stubborn and which are too twitchy.

Appreciate you pushing on all three — happy to dig into any of them further.

Collapse
 
voltagegpu profile image
VoltageGPU

Interesting approach to managing memory decay in LLM agents—something I've wrestled with when building GPU-based inference pipelines where stale data can silently poison results. Have you considered integrating time-to-live (TTL) metadata directly into the memory graph? At my day job, we’ve had some success with similar ideas in VoltageGPU, where we track data freshness for secure enclaves.

Collapse
 
rouche01 profile image
Richard Emate

Good point on stale data silently poisoning retrieval. VoltMem handles decay at search time rather than hard TTL today: volatile domains get down-ranked as they age, stable ones barely move. So it's soft freshness weighting, not an explicit expiry on the graph. I could see optional TTL as a complement for app-defined lifetimes (e.g. session context or a deal stage that should definitely expire), while volatility priors handle fact-type behavior. Thanks for raising it.

Collapse
 
rouche01 profile image
Richard Emate

Independent researcher here — if you're an arXiv endorser in cs.LG/cs.AI and willing to review the draft, I'd appreciate a DM. PDF + full repro in the repo.