Mnemo — Memory that reinforces, decays, and forgets
💡 Inspiration
Every AI assistant we used had amnesia. Close the tab, and it forgets your name, your preferences, everything. The industry's "fix" was brute force: stuff the entire history into the context window. But that doesn't scale — context is finite and expensive, and a wall of stale text buries the three facts that actually matter.
Human memory doesn't work that way. We don't retain everything equally. We strengthen what we use and let trivial things fade. Hermann Ebbinghaus quantified this in 1885 with the forgetting curve — retention decays exponentially with time, but every review flattens the curve.
We asked: what if an agent's memory worked like a mind instead of a log file? It should learn as you talk, reinforce what proves useful, forget what stops mattering — and, crucially, explain why it remembered something. That's Mnemo.
🛠 How we built it
Every turn runs through retrieve → respond → reflect, with a lazy forgetting sweep in the background.
user ─▶ RETRIEVE ─(recall K)─▶ re-rank ─▶ MMR pack ─▶ context block │ RESPOND ◀────┘──▶ reply │ REFLECT ─▶ extract ─▶ consolidate ─▶ store
Stack: Qwen (DashScope) for chat + text-embedding-v3 embeddings, Chroma as the persistent vector store (cosine space), a FastAPI backend, and a single-file animated web UI that visualizes memory live. With no API key it falls back to a local embedder + stub LLM, so the whole thing runs offline for a demo.
Retrieval: cheap recall, then precise re-rank
Stage 1 pulls the top-K nearest memories from Chroma by cosine similarity. Stage 2 re-ranks each candidate with a composite score blending four normalized signals (inspired by Stanford's Generative Agents):
score = w_sem·semantic + w_rec·recency + w_imp·importance + w_freq·frequency
recency = 0.5 ^ (days_since_access / half_life) # exponential decay frequency = 1 − e^(−access_count / k) # saturating usage
semantic is cosine relevance, recency decays with time, importance is the stored salience, and frequency saturates — so a memory used 50× isn't 50× a memory used once. Defaults: w_sem=0.55, w_rec=0.20, w_imp=0.15, w_freq=0.10 (all tunable).
Forgetting: timely, not destructive
Each memory carries an Ebbinghaus retention curve. Its stability grows with importance and every reinforcement:
stability = base · (0.5 + importance) · (1 + ln(1 + access_count)) retention = 0.5 ^ (days_since_access / stability)
A memory is forgotten when retention < threshold and it isn't protected (importance ≥ protect_importance — identity, preferences, and standing instructions never decay away). Nothing is destroyed: forgotten memories are archived with their retention at removal, so the behavior is auditable.
Recall within a budget: MMR packing
Re-ranking gives an ordering; packing decides what fits. Maximal Marginal Relevance trades relevance against diversity, so a tight token budget isn't wasted on five near-identical memories:
MMR = λ · score(m) − (1 − λ) · max cosine(m, already_selected)
Consolidation: no duplicate bloat
On write, a new memory is compared to its nearest neighbor. If cosine ≥ 0.88 it auto-reinforces the existing one. In the gray zone [0.70, 0.88), an LLM judge decides "same fact?" and merges paraphrases into one canonical phrasing — catching "I like Python" vs "the user's favourite language is Python", which pure cosine misses.
The frontend
The UI doesn't just chat — it shows the algorithm working: a live memory store where each card has a color-coded retention decay bar, and a "why these memories" panel that decomposes every recall into its semantic / recency / importance / frequency components as animated bars. The black box becomes glass.
📚 What we learned
- Recall is a ranking problem, not a search problem. Semantic similarity gives recall; precision comes from blending in recency, importance, and usage. We measured it — on real Qwen embeddings the composite re-ranker lifted recall@3 from 0.93 → 1.00 by surfacing a relevant memory cosine alone ranked too low.
- Forgetting is a feature. A memory system gets better when it discards the right things and reinforcement protects what proves useful.
- Saturating functions matter. Linear access counts let a spammy memory dominate ranking forever; 1 − e^(−c/k) gives diminishing returns and keeps ranking honest.
- Explainability is cheap and huge. Exposing the score breakdown cost almost nothing and turned "trust me" into "look."
🧗 Challenges we faced
- The precision/recall tradeoff has teeth. Boosting importance recovered a missed memory but occasionally demoted an exact match from first place (MRR dipped 0.93 → 0.86). We made every weight configurable rather than pretend one setting is optimal.
- Online dedup is order-dependent. We compare each new memory only to its single nearest neighbor, so two paraphrases can survive separately if a third memory sits between them. We documented it honestly and built the LLM-judge primitive a periodic full-pass consolidation would use.
- Chroma's cosine distance vs similarity. Chroma returns distance (1 − cosine); an early sign flip quietly inverted our whole ranking until the eval harness caught it.
- Concurrency. The agent keeps in-process conversation state, so wrapping it in a web server needed per-session locks to stop turns from racing into the same store.
- Making the math legible. Turning four abstract scoring terms into something a judge grasps in five seconds drove the entire "why" panel design.

Log in or sign up for Devpost to join the conversation.