A chatbot can sound convincing for five minutes without remembering anything.
Then you mention the job interview you were stressed about last week, the name of your dog, or a small detail from a late-night conversation.
It replies like none of it happened.
That is where most "AI companion" demos fall apart.
I am building Local Waifu, a desktop AI companion that runs on the user's own Mac or PC.
One of the rules I set early was simple: conversations and memories should stay on the machine.
No central chat database.
No server that needs to be online for the character to remember someone.
The rule sounds clean.
Building it was not.
Saving chats is not memory The first version of memory was the obvious one: save messages.
That gives you history, which is useful, but it does not solve recall.
A long chat history grows fast.
Sending all of it back to a local language model on every message is slow, expensive in context space, and usually makes the reply worse.
The model does not need to see every conversation from the last six months.
It needs the few pieces that matter right now.
If someone says, "I have to take Luna to the vet tomorrow," the character should be able to find that Luna is their dog.
It should not need to reread hundreds of unrelated messages about work, movies, and dinner plans to get there.
So I treated chat history and long-term memory as different things.
Chat history is the recent conversation.
It gives the model immediate context.
Long-term memory is a small collection of facts, moments, preferences, and relationship details that may matter later.
Those memories need to be searchable by meaning, not only by exact words.
The memory data stays in SQLite I wanted the app to work without a hosted database, so the storage layer is local SQLite.
Each character gets their own data.
Chats, memories, extracted entities, and relationships are stored locally on the device.
If a user creates two characters, one character does not quietly inherit the other one's memories.
That separation matters more than it sounds.
A companion app is personal by design.
Mixing context across characters is not a harmless bug.
If one character starts talking about something belonging to another, the whole illusion disappears immediately.
For each memory, I store the text itself along with metadata such as: the character it belongs to when it was created importance emotional weight how often it has been used a vector embedding used for semantic search The embedding is the part that lets the app search by meaning. "Vet appointment" and "Luna is sick" may share no exact keyword.
A semantic search can still see that they are probably connected.
I use 768-dimensional vectors stored directly in SQLite as binary data.
At recall time, the app calculates similarity locally and ranks the results.
No message content needs to leave the user's computer for that lookup.
Recall needs more than similarity A pure similarity score is not enough.
Imagine a user mentioned their favorite game once two years ago, then spent the last month talking about a difficult family situation.
Both memories might be related to a new message in some vague way.
The more recent and more emotionally important memory should usually win.
So recall is weighted by more than vector similarity.
Importance matters.
Recency matters.
Emotional weight matters.
A memory that has been useful before gets a little extra credit too.
There is no perfect formula here.
I do not think there ever will be one.
A system that aggressively recalls every detail feels creepy and repetitive.
A system that barely recalls anything feels empty.
The work is mostly tuning that middle ground and accepting that a companion should sometimes not bring something up.
The user should feel remembered, not monitored.
I also extract a small knowledge graph Some information is easier to retrieve as a relationship than as a paragraph of chat text.
For example: Luna is the user's dog Alex is the user's brother The user wor