I don't trust AI agent memory. It keeps facts that stopped being true and can't tell which version is current.
Every memory demo I've seen goes the same way. You tell the agent your favorite color, it saves "User's favorite color is blue" somewhere, and a week later it proudly tells you your favorite color is blue. Cool. Now change your favorite color.
The agent ends up with two memories that disagree, a vector search that returns both, and a model that has to guess which one is still true. Run that for a few months and you have a pile of half-true sentences about your users that nobody can clean up.
My take: anything that can be declarative should be declarative. If a fact has one correct value, it goes in a column. The agent is an operator. It reads and updates data. It shouldn't be a warehouse for everything a user ever said.
I wanted to see if that holds up outside my own head, so I built a small demo.
TL;DR: Store facts about users (diet, city, plan, language) as plain table columns that the agent reads and updates. Keep vector memory for stuff that has no structure, like notes and past conversations. Scope both to the user in the database, so the agent can't recall what someone else told it.
What is AI agent memory?
AI agent memory is whatever an agent carries over from earlier interactions. Most guides split it in two: short-term memory is the current conversation sitting in the context window, and long-term memory is whatever survives between sessions.
Long-term memory usually gets three labels borrowed from cognitive science. Here's the standard list, plus one column most guides skip:
| Type | What it holds | Example | Where I'd store it |
|---|---|---|---|
| Short-term (working) | The current conversation | "Make that function async" | Context window |
| Semantic | Facts about the user | "User is vegetarian" | A table column |
| Episodic | Things that happened | "Refund issued on March 3" | An events table with timestamps |
| Procedural | How to do things | "Always confirm before deleting" | Your prompt or your code |
Most of what people call long-term memory already has a natural home in a database. Only the leftovers, like free-form notes or "what did we talk about last time," need embeddings.
The problem with "the agent will remember"
I built a small demo on Nhost and stored the same user two ways in one Postgres database:
-
Vector memory: an
agent_memoriestable where the agent saves sentences from conversations, with embeddings for semantic search. This is roughly how most memory tools work. -
Declarative memory: a
user_profiletable with one row per user and plain columns likediet,cityandpreferred_language.
Then I fed in a year of a fictional user's life. January: steak every week, apartment in Berlin. By March they'd gone vegetarian, and in June they moved to Lisbon. On top of that, about ten unrelated memories about work and hobbies.
"What does this user eat?"
Here are the top 5 results from vector memory:
1. Jan 8 I eat steak pretty much every week
2. Mar 12 I went vegetarian last month
3. Jan 22 I work as a product designer at a fintech startup
4. Jan 15 I live in Berlin
5. Aug 9 I prefer meetings before lunch
The outdated fact came first. Vector search compares meaning and ignores dates, so "steak every week" beats "went vegetarian" for a question about food. The agent gets both and has to guess which one is still true, with the wrong one on top.
It happened again with "where does this user live?" "I live in Berlin" from January ranked first, and "I moved to Lisbon" from June came second.
A top-5 search also returns five results whether or not five are relevant, so "I prefer meetings before lunch" counts as an answer about food. You can add a similarity cutoff and a recency boost, but then you're tuning a ranking formula to imitate what a single UPDATE already does. And "I went vegetarian last month" only makes sense if you know it was saved in March. Pull it out a year later and "last month" means nothing.
The table returns one row: diet = vegetarian.
"Forget where I live"
In the table, that's city = null. One field, done.
With vector memory, you have to find every sentence that says where the user lives and delete each one. Here's what a semantic search for "where I live" found:
1. Jan 15 I live in Berlin
2. Jun 3 I moved to Lisbon
3. Feb 5 I'm training for a half marathon in the spring
4. Apr 10 I spent Easter hiking in the Dolomites
5. Oct 3 I joined a pottery class on Thursday evenings
The two right rows are at the top, and the search didn't miss any. But it can't tell you where "about where I live" stops, so if the agent deletes the top five, the user also loses their half marathon, their Easter hike and their pottery class.
Another user asks about steak
I added a second user, B, who lives in Madrid and has a steak memory of their own: a medium-rare steak is their favorite dinner.
When B searched for "steak," B got their own steak memory first and then the rest of their own memories. When A searched for "steak," A got A's. A plain read of both tables showed the same thing: A sees 14 memories and one profile, all A's, and B sees 5 memories and one profile, all B's.
Both approaches passed this one, because both tables have the same rule: a user can only read their own rows. The rule lives in the database, so it holds no matter what the agent decides to do.
AI agent memory architecture
Here's the setup I'd build in 2026:
- The agent acts as the user. It calls your API with the user's session, so it gets the same permissions that user has and can't read anything they couldn't.
- Facts live in tables: profile, preferences, plan, settings. The agent reads them and changes them with normal updates.
- Events go in an events table. Orders, tickets, refunds, each with a timestamp. That's your episodic memory, and SQL already knows how to query it.
- Vectors cover the rest: notes, chat summaries, documents. Searchable by meaning and scoped to the user like everything else.
The agent decides what to read and what to change.
Agent memory management: rules I'd follow
- If a fact has one correct value, make it a column.
- When a fact changes, update it. Appending a new sentence leaves the old one behind.
- "Forget this" should be a delete or an update you can point to.
- Put permissions in the database, so every memory is scoped to its owner before the agent sees it.
- Save events with timestamps, so "what happened last month" becomes a plain query.
- Use embeddings only for text that doesn't fit a schema, and expect fuzzy results there.
Where tools like Mem0 fit
Memory tools like Mem0 pull "memories" out of conversations with an LLM, then use another LLM call to decide whether to add, update or delete them. For the messy leftovers, like notes and chat history, that's a reasonable approach.
For facts with one correct value, I'd still pick a column in my own database. I can read it, change it and delete it without asking a model which sentence wins.
How I built it on Nhost
The whole demo runs on one Nhost project. It's a Postgres database with auth and permission rules on top, so both kinds of memory sit in one database under the same rules.
The tables
Declarative memory is a regular table with one row per user:
create table user_profile (
user_id uuid primary key references auth.users (id),
diet text,
city text,
preferred_language text,
updated_at timestamptz not null default now()
);
Vector memory is a table too. agent_memories stores the sentences, and Graphite, the AI service in Nhost, fills in an embeddings column for every row. When the text of a row changes, a small trigger marks it as outdated and Graphite embeds it again on its next sync. I didn't write any embedding code.
Permissions
Both tables get the same rule for the user role: you only see rows where user_id is your own ID.
{ "filter": { "user_id": { "_eq": "X-Hasura-User-Id" } } }
In the demo, users can only read. In a real app you'd also let them update their own profile row, so "forget where I live" goes through the same rule.
Search
Graphite adds a semantic search query for the table, and it runs through the same permission rule as any other query:
query Recall($query: String!) {
graphiteSearchAgentMemories(args: { query: $query, amount: 50 }, limit: 5) {
content
created_at
}
}
That's why user B never saw user A's steak. Nobody had to remember a WHERE clause in the search code.
Forgetting
On the declarative side, "forget where I live" is one update:
mutation Forget($id: uuid!) {
update_user_profile_by_pk(pk_columns: { user_id: $id }, _set: { city: null }) {
city
}
}
Graphite's API is evolving, so check the Nhost docs for the current setup.
FAQ
Does an AI agent need a vector database for memory?
For most of it, no. Facts about a user fit in table columns, and events fit in a table with timestamps. You only need embeddings for unstructured text, and pgvector inside Postgres handles that without a second database.
What's the difference between short-term and long-term agent memory?
Short-term memory is the current conversation in the model's context window, and it's gone when the session ends. Long-term memory is stored outside the model and loaded back in when it's relevant, across sessions.
How do you make an AI agent forget something?
If the fact lives in a column, you update or delete that field. If it only lives in vector memory, you have to search for every sentence that mentions it and delete those, and you can't be fully sure the search found them all.
How do you stop an agent from remembering other users' data?
Scope memory in the database. If every memory row has an owner and the agent queries with the user's permissions, it never gets other users' rows back. An instruction in the prompt can be ignored, and a filter in the agent's code only works if every query remembers to add it.
Your agent needs less memory than you think
Most of what an agent "remembers" is data your app should already own: profile fields, settings, events. Give the agent access to that, scoped to the user, and keep the fuzzy memory small.
What does your agent remember right now that should've been a column? Tell me in the comments.




Top comments (0)