~10–15 hrs
Requires: Project 02
Project 05 of 9

Personal Memory System

Make a stateless LLM remember — by scoring memories on relevance, recency, and importance

📖 Read the lesson ✍ Fill in understanding 🛠 Build the project 🚫 Break it 📊 Evaluate it

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.

The Core Problem

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?

Startup Lens

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. Then score = w_rel·rel + w_rec·rec + w_imp·imp; sort descending, take k.
  • 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

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, where d is the decay rate (the paper uses 0.995) and h = hours since m.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

  1. Write. When something happens, create a memory: embed its text once, stamp created_at = last_accessed = now, assign an importance (fixed default, or an LLM 1–10 rating), tag a kind, and append to the stream.
  2. 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.
  3. Score (recency). rec = decay_rate ^ hours_since(last_accessed). At 0 hours this is 1.0. Recency is from last access, not creation — so retrieving and touching last_accessed keeps used memories warm.
  4. 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.
  5. 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.
  6. 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

ConceptDefinitionWhy It Matters
StatelessnessAn LLM call has no memory of prior calls; each request is independentThe reason a memory layer must exist — the model won't remember unless you make it
Memory streamA growing list of memory objects (text, times, importance, embedding)The store you retrieve from; the central data structure
RelevanceCosine similarity between the query embedding and a memory's embeddingSurfaces memories that are about the current situation
Recencydecay_rate ^ (hours since last access) — exponential decay toward 0Recent/recently-used memories matter more; models forgetting
ImportanceA salience score (1–10) assigned at write time — mundane vs coreKeeps a trivial-but-recent memory from outranking a pivotal one
Retrieval scorew_rel·rel + w_rec·rec + w_imp·impThe single number that ranks memories; the lesson's learning target
Episodic / semantic / proceduralMemory of events (dated) / facts (durable) / skills (applied)Different kinds want different decay & retrieval; one flat store is worse
Memory hierarchySmall in-prompt main context vs large out-of-prompt external contextRetrieval = paging the right memories into a limited budget
Common Misconception

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)
What to Observe

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
What to Observe

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
Why This "Fails"

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

Stop Here

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

  1. 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.
  2. Why is an LLM stateless, and what exactly did Project 01's chatbot do to fake memory? Why does that approach break down?
  3. Recency is measured from last access, not creation, and retrieval touches last_accessed. Predict what goes wrong if you forget to touch on retrieval.
  4. Predict what each signal does alone: relevance-only, recency-only, importance-only. For each, give a query where it returns the wrong memory.
  5. 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.
  6. Memory hierarchy: what is "main context" vs "external context," and why is retrieval a budget (top-k) decision rather than "inject everything"?
  7. 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 their last_accessed. Learner core.
  • memory_store.py — the Memory dataclass and an in-memory MemoryStore. (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.

Definition of Done

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.

Start building

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.

1

M1 — Recency decay

scoring.recency_scoredecay_rate ^ hours_since(last_accessed). Validation: 0h → 1.0; 1h → 0.995; older → smaller (offline test).

2

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).

3

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).

4

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.

5

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.

6

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.

CriterionDoes your implementation...Pass?
Three signalsscore memories on relevance and recency and importance, not just similarity?
Exponential decaycompute recency as decay_rate ^ hours_since(last_accessed)?
Touch on retrieveupdate last_accessed = now for the memories it returns?
Top-k budgetretrieve a bounded top_k into the prompt, not the whole stream?
Same embedding modelembed memories and queries with the one model from config.py?
Write-back loopstore each new turn so the memory actually grows?
Kindstag memories episodic/semantic/procedural (and ideally decay them differently)?
Red Flags

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_accessed on 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

MistakeWhy It HappensConsequenceFix
Relevance-only retrieval"It's just RAG over my history"Surfaces the semantically-closest memory even if ancient/trivial; misses what just matteredAdd recency + importance to the score
Never touching last_accessedForgetting recency is measured from access, not creationFrequently-used memories decay anyway; recency becomes "age"Set last_accessed = now for every retrieved memory
One flat decay for everythingTreating a preference like a one-off eventDurable facts (dark mode, default API) decay and get forgottenMake semantic/procedural memories resist decay by kind
Importance computed per queryConfusing salience with relevanceWasted 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 answerRetrieve a bounded top-k — paging, not dumping
No write-backTreating retrieval as the whole systemThe assistant never learns; same questions every sessionStore the new turn (and its importance) back into the stream
Mixing embedding modelsMemories and query embedded differentlyRelevance is garbage; whole ranking fails silentlyPin 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

StarcallOS Connection

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.mdx·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, reflection
  • sources/papers/memgpt.md — Packer et al. 2023: the memory hierarchy (main vs external context), retrieval as paging under a context budget
  • sources/papers/memory-systems-taxonomy.md — Tulving/Squire: episodic vs semantic vs procedural memory; why kind-aware storage/decay matters
  • sources/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.