Thinking · Day 9
AI Memory Is Not Magic. It Is Product Design.
Every conversation resets. What does it take to build an AI app that doesn't forget? More than you'd think, and less than you'd fear.
June 9, 2026·14 min read
✦ The Mentor's Notepad
Imagine a trusted mentor you meet with regularly.
This mentor is exceptional. They can explain complex ideas clearly, give sharp feedback, and help you think through problems you cannot solve alone.
But here is what makes them truly useful: they take notes.
Not everything you ever said. Not a transcript of every session. A small, deliberate journal. After each conversation, they write down what matters: what you are working toward, where you struggled, what you prefer, what you have already decided.
Before your next meeting, they review those notes.
When you arrive, they do not ask you to start from the beginning. They say:
Last time you mentioned you prefer working through examples before reading theory. Let us start there.
That is not memory in the mystical sense. It is a system. Someone decided what was worth writing down. Someone organized it. Someone brought it back at the right moment.
A well-designed AI app works the same way.
The model is the mentor. Your application is the notepad. The difference between a frustrating product and a genuinely useful one is whether the notepad exists at all.
✦ The Real Problem: AI Apps Feel Disconnected
The pattern shows up everywhere once you start looking for it.
A resume assistant asks for your target role every time.
A writing assistant forgets that you prefer direct, short sentences.
A learning assistant does not remember which topics you struggled with last week.
A coding assistant forgets the architecture decisions your team already made.
A support assistant opens a new ticket without any knowledge of the unresolved issue from last month.
In each case, the model's capability is not the bottleneck. The model can write, analyze, explain, and reason. The bottleneck is that the product treats every session as if it is the first one.
That may be acceptable for a simple utility. It is not enough for a product that people are supposed to use every day, for months, to accomplish something meaningful.
Continuity is what separates a capable tool from a useful relationship.
✦ Memory Is Not the Same as Chat History
A common early mistake: just save all the messages and send them again.
That works for very small cases. It does not scale.
Chat history is raw. It contains useful information, repeated information, corrected information, casual remarks, temporary context, and irrelevant turns. Sending all of it forward is expensive, slow, and noisy. It may even hurt the quality of the answer by burying the important signal under everything else.
A memory system does not blindly carry everything forward. It extracts what matters.
Three compact records. Not six conversation turns.
When the next session begins, those three records are retrieved and placed into the prompt. The model can now answer as if it already knows the user, because it does.
Good memory is compressed, structured, and retrievable. It is not a transcript. It is a model of the user.
✦ What Should an AI App Remember?
This is where product thinking matters more than engineering.
Not everything is worth remembering. The goal is not to store everything. The goal is to store what makes future interactions more useful.
Useful memories fall into four types. Each serves a different purpose.
How the user likes to work
Style, format, and communication choices that apply across many future interactions. These rarely change and almost always matter.
Examples
The user prefers examples before theory.
The user wants concise answers, not long explanations.
The user prefers plain language over technical jargon.
Each type serves a different purpose. Good memory systems distinguish between them rather than storing everything in one flat list.
One test for any piece of information: would knowing this change how the assistant responds tomorrow? If yes, it belongs in memory. If it is temporary, sensitive, or irrelevant beyond this session, it does not.
Think about a product you use regularly that feels forgetful. What are the two or three things it should remember about you that would make it meaningfully better? What type of memory would each one be?
✦ How AI Memory Actually Works
There is no hidden mechanism inside the model that stores your data.
Memory is architecture. It is a loop your application runs around the model.
Memory is not inside the model. It is a loop your application runs around the model.
User says something
A message arrives. The app does not send it straight to the model.
Is anything worth remembering?
The system checks whether the message contains durable information: goals, preferences, decisions, patterns.
Extract a compact memory
One structured record is created: type, entity, text, importance. Not the whole conversation. Just what matters.
Store with embedding
The memory is saved alongside its vector, so it can later be retrieved by meaning, not just by keyword.
Retrieve relevant memories
The new question is embedded. The app searches stored memories for the closest matches.
Inject into prompt
The top memories are placed into the prompt alongside the question. The model can now see what was known.
Model answers with context
The answer reflects past preferences, goals, and decisions. Not because the model remembered. Because the app did.
The important phrase in that diagram: the model never changed. Only what it could see in that moment changed.
This is worth sitting with. The same model that gave a generic answer yesterday can give a deeply personalized answer today, if your application brings the right context into the prompt. No retraining. No fine-tuning. Just a well-designed retrieval loop.
That loop is what we are about to build.
✦ What This Looks Like in Code
Create a new file called memory-demo.js. You will build the memory system in seven steps, each one visible and understandable on its own.
Step 1: Setup
import "dotenv/config";
import OpenAI from "openai";
import { z } from "zod";
import { zodTextFormat } from "openai/helpers/zod";
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
Add "type": "module" to your package.json if you have not already.
{ "type": "module" }
Step 2: The shape of a memory record
Memory begins as ordinary data. There is nothing mystical about it. It is a record your application can store in any database.
const memory = {
userId: "user_123",
type: "preference",
entity: "learning_style",
text: "The user prefers examples before theory when learning technical topics.",
importance: 4,
source: "user_message",
createdAt: new Date(),
};
The exact field names are flexible. The important shape is the idea behind each one:
Who does this memory belong to? What kind of memory is it? What entity or topic is it connected to? What exactly should be remembered? How important is it, relative to other memories? Where did it come from? When was it created?
Once memory becomes data, your application decides when to retrieve it and how to use it.
Step 3: Injecting memory into the prompt
Suppose the user asks:
Help me understand JavaScript closures.
Without context, the model guesses the user's level and preferred style. With retrieved memories, the prompt can say much more.
function buildPromptWithMemory(userQuestion, memories) {
const memoryContext = memories
.map((m, i) => `${i + 1}. [${m.type}] ${m.text}`)
.join("\n");
return `You are a helpful learning assistant.
Use the relevant memories below only if they help you answer better.
Relevant memories:
${memoryContext}
User question:
${userQuestion}`;
}
This function reveals the whole architecture in twelve lines.
The model is not remembering by itself. The application retrieved past context and placed it inside the current request. The model can now answer from what it can see.
One thing to notice: this function receives a memories array and formats it. It does not do the retrieval itself. For now, assume that array arrives already populated. How those memories are selected, and why a simple for loop is not enough once a user has dozens of them, is what Step 6 addresses.
Step 4: Generating the answer
async function generateAnswer(userQuestion, memories) {
const prompt = buildPromptWithMemory(userQuestion, memories);
const response = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: [{ role: "user", content: prompt }],
});
return response.choices[0].message.content;
}
The API call is identical to Day 5. What changed is what arrived in the prompt. The retrieval happened before this line. The personalization happened before this line.
The model does not know any of that. It just answers from what it was given.
Step 5: Letting the model suggest what to remember
Manual memory creation is useful for understanding the concept. In real systems, you ask the model to inspect each user message and decide whether anything is worth storing.
For this to work reliably, you need a structured output, not a vague paragraph.
const ExtractedMemory = z.object({
shouldRemember: z.boolean(),
type: z.enum(["semantic", "episodic", "procedural", "preference"]),
entity: z.string().nullable(),
memory: z.string(),
importance: z.number().min(1).max(5),
});
This schema is the contract. The model must return a decision, not a description. Should this be remembered? What type? What is it connected to? What exactly should be saved? How important?
async function extractMemory(userMessage) {
const response = await openai.responses.parse({
model: "gpt-4o-mini",
input: [
{
role: "system",
content: `You extract useful long-term memories from user messages.
Only remember information that may help future interactions.
Do not remember sensitive or personal information.
Do not remember temporary or session-specific details.
Return shouldRemember as false if nothing durable is worth saving.`,
},
{
role: "user",
content: userMessage,
},
],
text: {
format: zodTextFormat(ExtractedMemory, "extracted_memory"),
},
});
return response.output_parsed;
}
For the first version, we will keep the extractor simple. It will look at a user message and return one suggested memory with one dominant type.
In a real system, one message may create multiple memories, but starting with one keeps the mechanism visible.
If the user says:
I am building a habit tracking app in React. I am comfortable with components and hooks, but I struggle with state management at scale. Please explain concepts with working code examples.
The model may return:
{
"shouldRemember": true,
"type": "semantic",
"entity": "current_project",
"memory": "The user is building a habit tracking app in React, is comfortable with components and hooks, struggles with state management at scale, and prefers working code examples.",
"importance": 4
}
Notice that the model classified this as semantic without any mapping logic in the code. The enum values you define are not just labels. They are meaningful terms the model already understands.
Semantic, episodic, procedural, preference: these come from decades of cognitive science research on how human memory works. The model has encountered these concepts extensively and understands what distinguishes one from another. So when it reads a message about a project someone is actively building, it recognizes that as project context, as a fact about the user's world, and reaches for semantic without being told. When your field names carry meaning, the model's existing knowledge does the classification work for you.
Try this
Change the enum values in your schema from meaningful names to arbitrary labels and run the extraction again on the same message.
const ExtractedMemory = z.object({
shouldRemember: z.boolean(),
type: z.enum(["typeA", "typeB", "typeC", "typeD"]),
entity: z.string().nullable(),
memory: z.string(),
importance: z.number().min(1).max(5),
});
The output may look like this:
{
"shouldRemember": true,
"type": "typeA",
"entity": "current_project",
"memory": "The user is building a habit tracking app in React, comfortable with components and hooks, struggles with state management at scale, prefers working code examples.",
"importance": 4
}
The model followed the schema correctly. But the classification is now meaningless. It picked typeA because it had no basis to reason about which label fits. Same model. Same message. Same schema shape. The name carried the knowledge.
Look at the extractMemory function above. What instructions make the model selective rather than greedy? What would happen if those instructions were removed? What kinds of messages would produce shouldRemember: false?
Step 6: Retrieving the right memories
For a small demo, retrieving all memories for a user and passing them in is fine.
For real systems with dozens or hundreds of memories per user, you need selective retrieval. The prompt should receive only what is relevant to the current question.
async function getEmbedding(text) {
const response = await openai.embeddings.create({
model: "text-embedding-3-small",
input: text,
encoding_format: "float",
});
return response.data[0].embedding;
}
function dotProduct(a, b) {
return a.reduce((sum, val, i) => sum + val * b[i], 0);
}
async function retrieveRelevantMemories(userQuestion, allMemories, limit = 3) {
const queryEmbedding = await getEmbedding(userQuestion);
const scored = [];
for (const memory of allMemories) {
const memEmbedding = await getEmbedding(memory.text);
scored.push({ ...memory, score: dotProduct(queryEmbedding, memEmbedding) });
}
return scored.sort((a, b) => b.score - a.score).slice(0, limit);
}
The pattern here is identical to Day 7 and Day 8. Convert the question to a vector. Compare it against stored vectors. Return the closest matches.
The source has changed. In RAG, the retrieved context came from documents. Here, it comes from past interactions. The retrieval logic is the same.
In a production system, this comparison runs in a vector database: Supabase with pgvector, Qdrant, Pinecone, or any similar store. For a local demo, the in-memory comparison above is enough to understand the full loop.
Step 7: Showing what was used
After generating the answer, a good memory-enabled product shows the user which memories shaped it. Not buried in settings. Right there, beside or below the answer.
function formatMemoriesUsed(memories) {
return memories.map((m, i) => `${i + 1}. [${m.type}] ${m.text}`).join("\n");
}
console.log("\n--- Memories used in this answer ---");
console.log(formatMemoriesUsed(topMemories));
In a UI, this renders as a small visible panel:
This one detail changes the experience. The user can see that the model did not mysteriously know these things. The application retrieved them and brought them into the conversation. That transparency creates trust.
The complete file
import "dotenv/config";
import OpenAI from "openai";
import { z } from "zod";
import { zodTextFormat } from "openai/helpers/zod";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const ExtractedMemory = z.object({
shouldRemember: z.boolean(),
type: z.enum(["semantic", "episodic", "procedural", "preference"]),
entity: z.string().nullable(),
memory: z.string(),
importance: z.number().min(1).max(5),
});
async function extractMemory(userMessage) {
const response = await openai.responses.parse({
model: "gpt-4o-mini",
input: [
{
role: "system",
content: `You extract useful long-term memories from user messages.
Only remember information that may help future interactions.
Do not remember sensitive or personal information.
Do not remember temporary or session-specific details.
Return shouldRemember as false if nothing durable is worth saving.`,
},
{ role: "user", content: userMessage },
],
text: { format: zodTextFormat(ExtractedMemory, "extracted_memory") },
});
return response.output_parsed;
}
async function getEmbedding(text) {
const response = await openai.embeddings.create({
model: "text-embedding-3-small",
input: text,
encoding_format: "float",
});
return response.data[0].embedding;
}
function dotProduct(a, b) {
return a.reduce((sum, val, i) => sum + val * b[i], 0);
}
async function retrieveRelevantMemories(userQuestion, allMemories, limit = 3) {
const queryEmbedding = await getEmbedding(userQuestion);
const scored = [];
for (const memory of allMemories) {
const memEmbedding = await getEmbedding(memory.text);
scored.push({ ...memory, score: dotProduct(queryEmbedding, memEmbedding) });
}
return scored.sort((a, b) => b.score - a.score).slice(0, limit);
}
function buildPromptWithMemory(userQuestion, memories) {
const memoryContext = memories
.map((m, i) => `${i + 1}. [${m.type}] ${m.text}`)
.join("\n");
return `You are a helpful learning assistant.
Use the relevant memories below only if they help you answer better.
Relevant memories:
${memoryContext}
User question:
${userQuestion}`;
}
async function generateAnswer(userQuestion, memories) {
const prompt = buildPromptWithMemory(userQuestion, memories);
const response = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: [{ role: "user", content: prompt }],
});
return response.choices[0].message.content;
}
// --- Demo ---
const userMessage =
"I am building a habit tracking app in React. I know components and hooks but struggle with state management at scale. Please use working code examples.";
const extracted = await extractMemory(userMessage);
console.log("\n--- Extracted memory ---");
console.log(JSON.stringify(extracted, null, 2));
// In a real app, saved memories come from a database.
// For this demo we simulate a small saved store.
const savedMemories = [
{
type: "preference",
text: "The user prefers working code examples before theory.",
},
{
type: "semantic",
text: "The user is building a habit tracking app in React.",
},
{
type: "episodic",
text: "The user struggles with state management at scale.",
},
];
const question = "What should I use for state management?";
const topMemories = await retrieveRelevantMemories(question, savedMemories);
const answer = await generateAnswer(question, topMemories);
console.log("\n--- Question ---");
console.log(question);
console.log("\n--- Memories used ---");
topMemories.forEach((m, i) => console.log(` ${i + 1}. [${m.type}] ${m.text}`));
console.log("\n--- Answer ---");
console.log(answer);
Run it:
node --env-file=.env memory-demo.js
The question contains no mention of React, no mention of the project, no indication of the user's level. On its own, the model has no choice but to give a generic answer covering every option.
The retrieved memories change that. They tell the model this is a React project, that scale is the specific concern, and that the user learns best from working code. The answer shifts from a general survey to a grounded recommendation.
That is what memory is for. Not restating what the user already said. Bringing in what they did not have to say again.
✦ Memory vs RAG
RAG and memory use the same retrieval pattern. They are not the same thing.
| Dimension | RAG | Memory |
|---|---|---|
| Source of retrieved context | External documents, knowledge bases, files | Past interactions, user preferences, decisions |
| What it answers | What does this document say? | What does this user prefer? What did we decide? |
| Changes over time | When source documents are updated | As the user interacts and the app learns |
| Primary value | Grounding answers in external knowledge | Continuing from previous context |
| Typical scale | Thousands of document chunks | Dozens to hundreds of user-specific records |
In simple terms:
RAG helps AI answer from external knowledge. Memory helps AI continue from previous context.
Many serious AI applications need both. A learning coach might retrieve lesson content from a knowledge base (RAG) and also retrieve what the learner has already understood and where they struggled (memory). The two systems work together.
✦ Where Memory Breaks
Building memory well means understanding where it fails.
A wrong memory can slowly bend the whole experience If the system incorrectly concludes that a user prefers verbose explanations when they actually prefer concise ones, every future answer will drift in the wrong direction. Memory amplifies whatever pattern it learned, correct or not.
Stale memory gives confident but outdated context. A user's goal three months ago may not be their goal today. A project decision made in January may have been reversed in March. Memory needs timestamps, revisitation, and sometimes deletion.
Too many memories create noise. Retrieving twenty memories for every question buries the relevant ones. Selective retrieval is not just an optimization. It affects answer quality.
Hidden memory creates discomfort. If a user discovers the system has been silently building a profile about them, it can feel unsettling, regardless of how benign the data is. Visibility is not a design nicety. It is how trust is built.
No deletion means no control. A memory system that users cannot edit or clear is not a feature. It is a trap. Users need to be able to correct wrong memories, remove outdated ones, and turn the system off entirely.
Sensitive memory stored casually creates risk. Health details, financial situations, personal struggles, and relationship information sometimes surface in conversation. If a memory system saves these without care, the product creates liability and erodes trust.
Memory should make the experience better. Bad memory makes it worse than having no memory at all, because it adds the friction of the system being confidently wrong.
Imagine a learning assistant that has been incorrectly remembering that a user prefers long, detailed explanations, when they actually prefer short ones. How would you detect that the memory is wrong? What would a good product give the user so they could correct it?
✦ A Practical Project: Memory Garden
A project worth building around this concept:
Memory Garden. Not another chatbot. A full-stack AI app that makes memory visible, controllable, and trustworthy.
The app has five panels working together:
Chat panel - The user converses naturally Suggested memory - The system surfaces what it wants to remember, for approval Saved memories - A visible list of what has been stored, editable and deletable Memories used - After each answer, which memories shaped it Controls - Approve, edit, reject, and forget
A possible stack:
Frontend: React or Next.js Backend: Node.js with Express or Next.js API routes Database: Supabase Postgres Vector search: pgvector AI: OpenAI Validation: Zod
What the full loop looks like:
The user chats. The system detects possible memories and surfaces them. The user approves or rejects. Approved memories are saved with type, entity, importance, and source. On the next question, the system retrieves the relevant ones. The assistant answers from that context. The UI shows which memories shaped the answer.
This project covers the complete loop:
Extract → Store → Retrieve → Inject → Generate → Display → Update → Forget
✦ What the Best AI Apps Will Understand
The next generation of AI products will not win only because they have better prompts.
They will win because they understand the user's journey.
A learning app will remember where the learner struggled, and begin from there.
A career app will remember the user's goals, and orient every answer around them.
A writing app will remember the user's voice, and hold it steady across sessions.
A coding app will remember the architecture decisions that are already settled.
A support app will remember the unresolved issue from the previous ticket.
A business assistant will remember decisions, owners, deadlines, and the context behind them.
The interface may still look like chat. Underneath, the product will be a memory system. That is the real work.
A forgetful AI app can answer questions. A memory-enabled AI app can continue a relationship.
That difference matters, because users do not want to keep re-explaining themselves. They want software that understands the path they are already on.
This is no longer just a theoretical idea. Modern agent frameworks and managed AI platforms are starting to treat memory as a core part of system design.
They separate short-term conversation state from long-term user or application memory, and they include ways to retrieve, update, and forget stored context over time.
✦ Learn More
-
LangGraph: Memory overview
: one of the clearest official guides on short-term memory, long-term memory, semantic memory, episodic memory, procedural memory, and memory storage
-
AWS Bedrock AgentCore: Memory
: official AWS documentation on short-term and long-term memory for agents, including preferences, facts, and session summaries
-
OpenAI: ChatGPT Memory FAQ
: how memory works as a product feature in ChatGPT, including saved memories, chat history, controls, deletion, and memory sources
-
Supabase Vector Search with pgvector
: storing and retrieving embeddings in Postgres, the foundation for production memory retrieval
-
Zod documentation
: schema validation used throughout this lesson to give memory extraction a reliable, typed contract
Memory is not the weight of the past. It is the quiet care that lets a journey continue.