Personal Memory System
Make a stateless LLM remember — by scoring memories on relevance, recency, and importance
Learning Objectives
By the end of this project, you will be able to:
- Explain why an LLM is stateless and why a memory system is the layer that makes an assistant remember across turns and sessions.
- Describe the memory stream: a growing store of memory objects, each with a creation time, a last-accessed time, and an importance score.
- Implement the retrieval score that combines relevance (semantic similarity), recency (exponential time decay), and importance (salience) into one number.
- Implement exponential recency decay,
recency = decay_rate ^ (hours since last access), and explain why retrieval refreshes a memory's recency. - Distinguish episodic (events, dated), semantic (durable facts), and procedural (skills) memory, and why tagging by kind beats one flat store.
- Explain the memory hierarchy — small in-prompt main context vs large out-of-prompt external context — and frame retrieval as paging into a limited budget.
- Name and reproduce the major failure modes of a memory system and say which signal caused each.
1. Motivation
Why This Exists
An LLM is stateless: each call starts cold, and the context window is finite. Project 01's chatbot only "remembered" because you replayed the entire history into every prompt — that works for one short conversation and then collapses: histories outgrow the context window, every replayed token costs money, and nothing survives across sessions. Close the app and the assistant forgets you. A memory system is the layer that fixes this: store experiences durably outside the prompt, and retrieve only the relevant few back in when they matter.
You want an assistant that remembers — your preferences, past decisions, what you told it last week — without stuffing your entire history into every prompt. Replay-everything blows the window and the bill. Retrieve-by-similarity-only (plain RAG over your chat log) surfaces the semantically closest memory even if it's ancient and trivial. The memory-system answer: keep a memory stream, and rank memories by relevance and recency and importance, retrieving the top few into the prompt.
What Breaks Without It
The failure is quiet and corrosive: an assistant that forgets a standing instruction you gave it (no write-back), or fixates on an old irrelevant detail because it scored relevance only (no recency/importance), or contradicts a decision you made yesterday (never retrieved it). Users don't file a bug — they just stop trusting it.
Real-World Stakes
Every assistant that "remembers you" — ChatGPT's memory, coding copilots that recall your conventions, support bots that know your history — is some version of this. The judgment that separates a toy from a product: did the right memory get retrieved, did stale memory decay, and did the new turn get written back?
Memory is the difference between a stateless tool and an assistant that compounds in value the more you use it. Users pay for an assistant that knows them — and that is precisely a well-scored, well-maintained memory stream.
2. Mental Model
Explain Like I'm 12
Imagine a friend with a giant box of index cards. Every time something happens, they jot it on a card and toss it in the box: "Eric likes dark mode" (Tuesday), "we decided to use Groq" (last week), "brushed teeth" (this morning). The box is huge — they can't reread every card before answering you. So when you ask something, they grab the handful of cards that are (1) actually about your question, (2) recent or recently-used, and (3) a big deal — and ignore the rest. A card you keep pulling out stays near the top; a trivial old card sinks and fades. That three-part "which cards do I grab?" rule is the whole project.
Explain Like I'm a Software Engineer
- A memory stream is an append-only list of memory objects:
{text, kind, created_at, last_accessed, importance, embedding}. - The store lives in external context (out of the prompt — your "disk"); the prompt is main context ("RAM"), small and fixed. Retrieval is paging: select the top-k memories to load into the prompt for this turn.
- The ranking is a weighted sum of three signals: relevance =
cosine(query, memory); recency =decay_rate ^ hours_since(last_accessed); importance =memory.importance / 10. Thenscore = w_rel·rel + w_rec·rec + w_imp·imp; sort descending, takek. - Retrieving a memory refreshes its
last_accessed, so used memories stay warm and unused ones decay. After the model answers, write the new turn back so the system grows.
Real-World Analogy
A memory system is a good executive assistant, not a search engine. Ask "what's the plan for the Berlin trip?" and a great assistant doesn't dump every email containing "Berlin" (that's relevance-only). They surface the relevant threads, weighted toward the recent ones, and they know which were a big deal (the signed contract) versus noise (a lunch reservation). And every time they pull a file to help you, it stays on top of the pile for a while.
How It Works (Diagram)
WRITE (every turn / observation):
"Eric prefers Groq for the default API" ──embed──► vector
+ created_at=now, last_accessed=now, importance=7, kind=semantic
└─► append to memory stream (external context)
RETRIEVE (per query, e.g. "which API should I default to?"):
query ──embed──► q
for each memory m in the stream:
rel = cosine(q, m.embedding) # is it about this? [0..1]
rec = decay_rate ** hours_since(m.last_accessed) # is it fresh? (0..1]
imp = m.importance / 10 # does it matter? [0..1]
score(m) = w_rel*rel + w_rec*rec + w_imp*imp
top_k = sort(memories by score, desc)[:k]
touch(top_k): last_accessed = now # retrieval refreshes recency
inject top_k into the prompt ──► LLM answers grounded in memory
3. Technical Explanation
Formal Definition
A memory is an object m = (text, kind, created_at, last_accessed, importance, embedding); the memory stream is the list of all such objects. Retrieval scores every memory against a query q on three components and returns the top-k:
- relevance
rel(q, m) = cos(e_q, e_m)— cosine similarity of the embeddings (Project 02). - recency
rec(m) = d^h, wheredis the decay rate (the paper uses0.995) andh= hours sincem.last_accessed. - importance
imp(m) = importance / 10, a salience score rated 1–10 at creation.
Score: score(q, m) = w_rel·rel + w_rec·rec + w_imp·imp. The paper min-max normalizes each component to [0,1] and sets all weights to 1.
How It Works Step by Step
- Write. When something happens, create a memory: embed its text once, stamp
created_at = last_accessed = now, assign animportance(fixed default, or an LLM 1–10 rating), tag akind, and append to the stream. - Score (relevance). Embed the query with the same model used for memories; relevance is the cosine similarity. Mixing models makes it meaningless — a silent bug.
- Score (recency).
rec = decay_rate ^ hours_since(last_accessed). At 0 hours this is1.0. Recency is from last access, not creation — so retrieving and touchinglast_accessedkeeps used memories warm. - Score (importance).
imp = importance / 10. Set once at write time — "distinguishes mundane from core memories." It's why "we signed the contract" (10) outranks "brushed teeth" (1) at equal recency. - Combine & retrieve.
score = w_rel·rel + w_rec·rec + w_imp·imp; sort descending; take the top-k that fit the budget. Touch the returned memories'last_accessed = now. - Inject & answer, then write back. Put the retrieved memories into the prompt (main context), call the LLM, then store the new turn as a memory so the system grows.
Key Concepts
| Concept | Definition | Why It Matters |
|---|---|---|
| Statelessness | An LLM call has no memory of prior calls; each request is independent | The reason a memory layer must exist — the model won't remember unless you make it |
| Memory stream | A growing list of memory objects (text, times, importance, embedding) | The store you retrieve from; the central data structure |
| Relevance | Cosine similarity between the query embedding and a memory's embedding | Surfaces memories that are about the current situation |
| Recency | decay_rate ^ (hours since last access) — exponential decay toward 0 | Recent/recently-used memories matter more; models forgetting |
| Importance | A salience score (1–10) assigned at write time — mundane vs core | Keeps a trivial-but-recent memory from outranking a pivotal one |
| Retrieval score | w_rel·rel + w_rec·rec + w_imp·imp | The single number that ranks memories; the lesson's learning target |
| Episodic / semantic / procedural | Memory of events (dated) / facts (durable) / skills (applied) | Different kinds want different decay & retrieval; one flat store is worse |
| Memory hierarchy | Small in-prompt main context vs large out-of-prompt external context | Retrieval = paging the right memories into a limited budget |
A memory system is not just RAG over your chat log. RAG ranks by relevance alone; a memory system adds recency and importance. The hardest-to-spot bug is forgetting to touch last_accessed on retrieval — recency then silently degenerates into "age since creation," and memories you use constantly decay anyway.
4. Guided Examples
The lab stack: litellm (embeddings + chat via config.py), an in-memory MemoryStore, and pure-Python scoring (cosine, decay). Examples mirror the guiding tests.
Example 1: Simplest Case — exponential recency decay
from scoring import recency_score
now = 1_000_000.0 # epoch seconds (any fixed "now")
hour = 3600.0
# A memory accessed right now vs 1 hour ago vs 2 hours ago, decay_rate = 0.995:
print(recency_score(now, now, decay_rate=0.995)) # 1.0 (0 hours)
print(recency_score(now, now - hour, decay_rate=0.995)) # 0.995 (1 hour)
print(recency_score(now, now - 2*hour, decay_rate=0.995)) # 0.990025 (2 hours)
Recency is decay_rate ^ hours, so a just-touched memory scores 1.0 and decays smoothly as it ages. This is why touching last_accessed on retrieval matters — it resets the clock and keeps used memories near the top.
Example 2: Real-World Case — the three-signal score ranks memories
from scoring import retrieval_score
# Three already-computed (relevance, recency, importance) triples, equal weights:
w = (1.0, 1.0, 1.0)
relevant_but_old = retrieval_score(rel=0.9, rec=0.10, imp=0.3, weights=w) # 1.30
fresh_but_off_topic = retrieval_score(rel=0.1, rec=1.00, imp=0.2, weights=w) # 1.30
relevant_and_fresh = retrieval_score(rel=0.8, rec=0.90, imp=0.7, weights=w) # 2.40 ← wins
print(relevant_and_fresh > relevant_but_old, relevant_and_fresh > fresh_but_off_topic) # True True
No single signal wins alone. The memory that is about the question and fresh and matters outranks the one that is merely very relevant but ancient, or merely fresh but off-topic. The weights w are the knob that tunes this.
Example 3: When It Fails — relevance-only retrieval grabs the wrong memory
from memory_store import Memory, MemoryStore
from retriever import retrieve
now = 1_000_000.0
store = MemoryStore()
# Same topic embedding [1,0,0]; one is trivial+old, one is important+fresh:
store.add(Memory("m0", "brushed teeth", kind="episodic", created_at=now-1e6,
last_accessed=now-1e6, importance=1, embedding=[1.0, 0.0, 0.0]))
store.add(Memory("m1", "decided to default to Groq", kind="semantic", created_at=now-10,
last_accessed=now-10, importance=8, embedding=[1.0, 0.0, 0.0]))
q = [1.0, 0.0, 0.0] # query embedding — equally relevant to BOTH memories
top = retrieve(store, q, now=now, k=1, weights=(1.0, 1.0, 1.0), decay_rate=0.995)
print(top[0].id) # 'm1' — recency + importance break the relevance tie correctly
Relevance alone can't tell these apart (identical embeddings). Recency and importance break the tie toward the memory actually worth surfacing. A plain RAG-over-chat-log (relevance only) could just as easily return m0. This is the reason a memory system is more than Project 04 over your history.
5. Reflection Before Building
Before writing any code, fill in UNDERSTANDING.md in your own words. No copy-pasting from the lesson. No AI assistance for writing — only for checking. If you can't explain it simply, go back and re-read.
Knowledge Check — Answer These in UNDERSTANDING.md
- In your own words, why is a memory system more than "Project 04 RAG over my chat history"? Use the words relevance, recency, and importance.
- Why is an LLM stateless, and what exactly did Project 01's chatbot do to fake memory? Why does that approach break down?
- Recency is measured from last access, not creation, and retrieval touches
last_accessed. Predict what goes wrong if you forget to touch on retrieval. - Predict what each signal does alone: relevance-only, recency-only, importance-only. For each, give a query where it returns the wrong memory.
- Episodic vs semantic vs procedural: give one example of each from your own use of an assistant, and say which should decay and which should not.
- Memory hierarchy: what is "main context" vs "external context," and why is retrieval a budget (top-k) decision rather than "inject everything"?
- The one thing you still don't fully understand about memory systems.
After filling in UNDERSTANDING.md, use the AI mentor pattern in docs/meta/learning-flow.md to get feedback. Record the feedback in UNDERSTANDING_FEEDBACK.md.
6. Project Assignment
See PROJECT.md and source/project.md for the full specification including file structure and extended requirements.
Core Requirement
Build a personal memory system in code/ that stores experiences and retrieves the most worth-remembering ones for a query, then a chat assistant that uses it:
scoring.py— the three scoring signals and their combination:recency_score(exponential decay),importance_score(normalize 1–10),relevance_score(cosine),retrieval_score(weighted sum). Learner core — the learning target.retriever.py— score every memory in the store, sort, return the top-k, and touch theirlast_accessed. Learner core.memory_store.py— theMemorydataclass and an in-memoryMemoryStore. (Provided — the data structure.)embedding_helpers.py— embeddings glue, same model both sides. (Provided — Project 02–04 reuse.)chat_with_memory.py— embed query → retrieve → inject memories → answer → write back. (Provided orchestrator.)
Extended: LLM-rated importance (poignancy 1–10); kind-aware decay (semantic/procedural resist decay); reflection (promote repeated episodic memories into durable facts); persistence across sessions.
Retrieval ranks on relevance + recency + importance (not similarity alone); recency is exponential decay and retrieve() touches last_accessed; retrieval returns a bounded top-k; the chat loop writes each turn back; the guiding tests pass; UNDERSTANDING.md done before any code; FAILURE_ANALYSIS.md has ≥3 experiments; EVALUATION.md is quantitative; STARCALLOS_REFLECTION.md names ≥1 concrete pattern.
Open code/README.md for setup, the milestone build order, and the file roles (which files are provided vs. learner-owned). Run python -m pytest to see the failing guiding tests, then implement the learner-owned functions in milestone order until they pass.
7. Project Milestones
Work through these in order. Each milestone should produce runnable code before moving on.
M1 — Recency decay
scoring.recency_score — decay_rate ^ hours_since(last_accessed). Validation: 0h → 1.0; 1h → 0.995; older → smaller (offline test).
M2 — Importance & relevance
scoring.importance_score (1–10 → [0,1]) and scoring.relevance_score (cosine). Validation: identical vectors → 1.0; orthogonal → 0.0; imp(10)=1.0 (offline test).
M3 — Combined score
scoring.retrieval_score — weighted sum of the three signals. Validation: relevant+fresh+important outranks relevant-but-old and fresh-but-off-topic (offline test).
M4 — Retrieve top-k
retriever.retrieve — score all, sort desc, return top-k, touch last_accessed. Validation: the important+fresh memory beats the trivial+old one on identical embeddings; returned memories' last_accessed == now.
M5 — Chat with memory
chat_with_memory wires embed → retrieve → inject → answer → write back. Validation: the assistant answers using a fact you told it earlier in the session.
M6 — Break / Evaluate
Drop a signal (relevance-only, no decay, no write-back) and watch recall degrade. Validation: each ablation surfaces the wrong memory or forgets; recorded in FAILURE_ANALYSIS.md.
8. Self-Evaluation
After building, honestly evaluate your implementation against these criteria. Record your answers in EVALUATION.md.
| Criterion | Does your implementation... | Pass? |
|---|---|---|
| Three signals | score memories on relevance and recency and importance, not just similarity? | ☐ |
| Exponential decay | compute recency as decay_rate ^ hours_since(last_accessed)? | ☐ |
| Touch on retrieve | update last_accessed = now for the memories it returns? | ☐ |
| Top-k budget | retrieve a bounded top_k into the prompt, not the whole stream? | ☐ |
| Same embedding model | embed memories and queries with the one model from config.py? | ☐ |
| Write-back loop | store each new turn so the memory actually grows? | ☐ |
| Kinds | tag memories episodic/semantic/procedural (and ideally decay them differently)? | ☐ |
Your implementation may have problems if:
- Retrieval is cosine similarity only — you've rebuilt Project 04, not a memory system.
- Recency never changes because you never touch
last_accessedon retrieval. - Importance is recomputed at query time instead of stored at write time.
- You inject the entire memory stream into the prompt (no budget).
- The assistant never remembers anything new because there's no write-back.
- You report "it remembers" with no ablation showing a dropped signal breaking recall.
9. Common Mistakes
| Mistake | Why It Happens | Consequence | Fix |
|---|---|---|---|
| Relevance-only retrieval | "It's just RAG over my history" | Surfaces the semantically-closest memory even if ancient/trivial; misses what just mattered | Add recency + importance to the score |
Never touching last_accessed | Forgetting recency is measured from access, not creation | Frequently-used memories decay anyway; recency becomes "age" | Set last_accessed = now for every retrieved memory |
| One flat decay for everything | Treating a preference like a one-off event | Durable facts (dark mode, default API) decay and get forgotten | Make semantic/procedural memories resist decay by kind |
| Importance computed per query | Confusing salience with relevance | Wasted LLM calls; importance stops meaning "how big a deal was this" | Assign importance once, at write time; store it |
| Injecting the whole stream | "More context is better" | Blows the context window and the bill; buries the answer | Retrieve a bounded top-k — paging, not dumping |
| No write-back | Treating retrieval as the whole system | The assistant never learns; same questions every session | Store the new turn (and its importance) back into the stream |
| Mixing embedding models | Memories and query embedded differently | Relevance is garbage; whole ranking fails silently | Pin one embedding model both sides (carries from Project 02) |
10. Connections
Builds On
Relevance scoring is Project 02's cosine similarity over embeddings, and retrieving top-k from a store is Project 03/04's retrieval — a memory system is that retrieval with two extra signals (recency, importance) stacked on top, ranking your own past instead of a PDF. The prompt that consumes the retrieved memories is Project 01's system+user message construction; the "same embedding model both sides" rule is inherited wholesale.
Enables
Project 08 (agent) turns write-back into a tool the model calls — MemGPT's self-editing memory. Project 09 (personal learning OS) builds on reflection: synthesizing durable semantic knowledge from accumulated episodic memories. Project 07 (evaluation) generalizes the LLM-rated importance into a full LLM-as-judge scorer.
Production Patterns
Real assistant-memory systems are this hierarchy plus engineering: a small in-prompt working memory and a large vector-backed store (MemGPT's main vs external context); retrieval scored on relevance + recency + importance (Generative Agents); episodic observations promoted to durable semantic facts via reflection; summarization on eviction when retrieval overflows; and the model itself deciding what to write via tools. Persistence (a real vector DB) and per-user isolation are the production add-ons.
StarcallOS Relevance
Any StarcallOS feature that "knows you" — remembers your preferences, recalls past decisions, carries context across sessions — is this memory system. The disciplines here decide whether StarcallOS feels like an assistant that compounds (right memory retrieved, stale memory decayed, new facts written back and promoted to durable knowledge) or a goldfish that forgets every session.
Sources
See source/resources.md for the complete annotated source list.
Tier 1 — Official Documentation
sources/official-docs/scikit-learn-cosine-similarity.md—x·y / (‖x‖‖y‖); the relevance signal (carried from Project 02)sources/official-docs/chromadb.md— a real vector store for persisting the memory stream (carried from Project 03)
Tier 2 — Foundational Papers
sources/papers/generative-agents.md— Park et al. 2023: the memory stream and the retrieval score (relevance + recency + importance), exponential decay, reflectionsources/papers/memgpt.md— Packer et al. 2023: the memory hierarchy (main vs external context), retrieval as paging under a context budgetsources/papers/memory-systems-taxonomy.md— Tulving/Squire: episodic vs semantic vs procedural memory; why kind-aware storage/decay matterssources/papers/sentence-bert.md— bi-encoder embeddings behind the relevance signal (carried)
Tier 3 — Engineering Guides
- None specific to this lesson — the Generative Agents and MemGPT papers are themselves the engineering blueprints.
Tier 4 — Educational Sources
- None specific to this lesson — the Project 02–03 embedding/retrieval sources are the prerequisites.