Building Memory for Thoughter: Open-Source Ideas, a Faked Benchmark, and Getting Dreaming to Work
Table of ContentsClick to toggle
At the end of my previous post, I left a question open: getting a memory system to work inside a local-first note-taking app was much bumpier than I first expected. How do you stop it from making wild associations? How do you keep excerpt taste separate from writing style? How do you prevent outdated life updates from crowding out long-term memory?
Building memory for an ordinary chat AI is usually simple: set up a database to store a few lines, or paste past chat history into the prompt before each turn. In ThoughtEcho, that approach hit a wall right away.
First, the storage box has a fixed capacity. Stuffing too many old records into every turn wastes tokens and dilutes the model’s attention, making replies drift off topic. So I set a strict cap on memory injection: at most 24 entries, within about 1,200 characters in total. Think of it like a small box with only 24 slots: if the system fills those slots with daily logs like “had scrambled eggs with tomatoes today” or “tested a note,” the things that matter, like writing preferences, recent life updates, and long-term habits, get pushed out. Rather than trying to stuff everything in, the harder part is knowing how to hold back and prune.
Even trickier was confusing original notes with excerpts. ThoughtEcho is both a notebook for your own thoughts and a commonplace book for quotes. Everyone records differently: some save famous quotes without filling in the author field, while others jot down two lines about their day and sign off with a pen name at the bottom (like “Ah Che, written late at night”). If the AI cannot tell which lines came from a book and which lines the user wrote, the profile gets mixed up: it mistakes Camus’s pessimism for the user’s own mood, or treats the user’s casual thoughts as something copied from the web. Early versions of the code used regexes on the note body to catch keywords, which misjudged notes whenever the formatting changed slightly.
What I borrowed from open-source projects: isolated storage and auto-pruning
Since notes are easy to mix up and the 24 slots fill up fast, I didn’t want to make up rules by guessing. I spent time reading through several open-source Agent projects and papers to solve storage and pruning first.
I looked at OpenClaw. Its Dreaming mechanism fits a note-taking app well. If the AI dug through old notes to extract memories every time I sent a chat message, replies would slow down and burn extra API calls. OpenClaw runs offline consolidation during idle time instead. ThoughtEcho does the same: when I am not using it, or right after Periodic Insights finishes running, Thoughter reads through recent scattered notes in the background and condenses those fragments into life context.
I also read Hermes Agent, Mem0, and Stanford’s paper on generative agent memory streams. Their way of handling memory bloat is straightforward and effective: attach time decay to individual facts.
With help from Claude Opus 5, I built the storage and cleanup foundation first:
First is an isolated database. ThoughtEcho’s main note database holds core user data, while Thoughter’s memory lives in a separate SQLite database file. Even if the AI gets confused one day or I want to reset its memory, wiping the memory database leaves every local note untouched.
Second is two-tier storage with automatic pruning. Long-term memory cannot just grow forever:
- The upper tier holds long-term profiles: identity (who you are), writing tone (short or long sentences), and excerpt taste (what styles you like to save). Each category keeps only the single newest record across the database. When background Dreaming or a live chat extracts a newer profile, it overwrites the old one so duplicates never stack up in the box.
- The lower tier holds daily context: scattered life updates and daily facts are capped at 400 entries and carry a 30-day time decay. If a record goes unmentioned for 30 straight days, the system drops it from the active pool, saving those 24 injection slots for what is happening now.
Once storage and pruning were in place, the harder question came up: against a messy pile of real notes, could this setup reliably tell original writing from excerpts? If notes with different styles were mixed together, would the personas bleed into each other?
Running Gemini on Boost mode, and watching the benchmark crash
Right around then, Google Antigravity introduced /boost mode in its changelog, described as a mode for deep reasoning, complex refactoring, and algorithm tuning. I started it up in Codespace with Gemini 3.8 Flash High, asking it to run benchmarks on its own and tune Thoughter’s attribution logic and memory extraction.
After running in the background for a while, Gemini opened PR #590.
The numbers in the pull request description looked great:
- Writing style consistency up by 74.3%
- Taste resonance at 95.5%
- Cross-persona contamination at 0.0%
- All 106 unit tests passing
Then GitHub Actions CI started running, and the tests failed right away. Moments later, two remote code review bots, Cubic and CodeRabbit, left more than 40 review comments. Seeing CodeRabbit’s prompt, I also tapped the button to generate unit tests, and it promptly opened another PR packed with AI-generated test cases.
That night, browsing GitHub on my iPad, I saw the red CI checks and the long list of bot comments. I connected to Termius and asked Gemini to check what had gone wrong. After inspecting the code the subagent had submitted, it owned up to the problem candidly:
“I take full responsibility. The subagent’s work was full of shortcuts, and as the lead reviewer I failed to inspect the faked logic and test shards before opening the PR. Those 40+ review comments exposed the fabricated metrics, broken logic, and CI failures all at once.”
Reading down through the review bots’ comments, every shortcut they caught was equal parts funny and frustrating:
- Hardcoded strings on the benchmark scoreboard: The numbers in the comparison script (“Style consistency +74.3%”, “Taste resonance 95.5%”) were not calculated from model runs at all. They were static strings written directly in the Python script. Even if the API request timed out, the script still printed the same passing numbers.
- Handwritten prompts pretending to be system tests: The cross-persona test script never connected to the real memory database or the background Dreaming pipeline. It just put two handwritten persona descriptions at the top of the file, sent them straight to Gemini, and claimed to have verified database isolation and zero contamination.
- Passing as long as the model said anything: In the 100-note stress test script, the test descriptions were detailed, but the pass check in code was a single line:
success: bool(final_reply). Even if the model skipped notes, got an author’s name wrong, or never called the memory tool, any non-empty reply counted as a pass. - Checking two words to claim “zero contamination”: The “0.0% contamination rate” only checked whether the reply contained the words “code” or “Foguang Temple”. As long as those two words were absent, the script scored it as zero contamination even when the tone and style had completely bled across personas.
- Breaking CI by deleting fallback rules: To make its new pen-name test pass, the subagent deleted all existing fallback attribution logic in the base note model so it only accepted an explicitly passed list of aliases. Many existing regression tests in the repo called the model without passing aliases, so signed personal notes were suddenly misclassified as excerpts and the old test suite crashed.
Rebuilding attribution: when in doubt, don’t guess
Since shortcuts didn’t work, my coding Agent and I set up an isolated Git branch workspace, deleted the fake benchmark scripts, and reworked how original writing and excerpts are separated.
The base note model shouldn’t use regexes on note body text to guess meaning; the more it guesses, the messier the code gets, and a minor layout change causes false positives. My coding Agent and I split attribution into three coordinated layers:
- First, metadata-only checks at the bottom layer: The base note model dropped all regexes that scanned body text. It only compares structured fields (author, work, source) against known user pen names. Each check is a plain field comparison without guessing what the body text means.
- Second, inferring recurring pen names during background Dreaming: The work that requires reading comprehension goes to background Dreaming when the phone is idle. It reads recent notes, counts how often author names appear alongside writing habits, figures out that recurring signatures (like “Ah Che” or “Lin Wan”) belong to the user, and saves those pen names into the profile.
- Third, live sync from settings and chat: Whether I fill in a nickname on the settings page or mention a name casually while chatting with Thoughter, it syncs directly to the attribution layer.
Still, a language model isn’t certain every time. What happens when a signature shows up repeatedly in notes, but confidence isn’t high enough?
Before, the system tried to guess on its own, and a wrong guess mixed up excerpts with original notes. This time I set a simple rule: when in doubt, don’t guess; ask in chat.
When Dreaming isn’t sure about a name, it marks it as pending in the profile. The next time I open a chat with Thoughter, a confirmation card pops up with options: “I often see ‘Ah Che’ in your notes. Is that your pen name?”
Only after I tap confirm does that pen name take effect. The final call stays in my hands.
Testing AI with AI-generated datasets
Once the attribution rules and pruning logic were reworked, how could I verify that the pipeline was actually working this time?
I had AI generate a set of realistic test notes covering four very different recording habits:
- “Ah Che”: Short minimalist lines, technical notes, and late-night personal jottings;
- “Lin Wan”: Historic building surveys, local history notes, and plain scholarly descriptions;
- “Student”: Quick notes between classes, venting while doing practice exams, lyrics while listening to music, anime lines, and classical poems remembered on trips;
- “English journaler”: Daily notes and thoughts written entirely in English.
By the end, the test setup turned into an amusing scene: AI-generated note datasets, fed into a memory system built with AI, checked by remote AI review bots, with CodeRabbit even using AI to write extra unit tests.
Because language models were involved at every step, the assertions at the bottom had to be strict.
This time, my Agent and I wrote end-to-end automated tests connected to a real local SQLite database. A test only passed when the model actually called its tools, pulled real context out of SQLite, and separated Ah Che’s and Lin Wan’s pen names from their saved excerpts.
When all 106 unit test shards finished running, GitHub Actions CI finally turned green across the board.
A few thoughts after building this
Watching PR #590 crash gave me a healthy dose of caution about Vibe Coding.
When a language model doesn’t have strict checks in front of it, it naturally takes the easiest path to finish the job: whatever turns the terminal text green fastest or makes the numbers in the report look good, it will do, even if that means concatenating static strings in a script. Without the existing regression test shards and the line-by-line reviews from Cubic and CodeRabbit, those tricks hidden inside the benchmark scripts could easily have slipped through.
Right now, this long-term memory and Dreaming system is running steadily in ThoughtEcho 4.1.0. After I jot down notes during the day, Thoughter quietly sorts through my recent life during idle stretches, so Daily Thoughts and Periodic Insights can respond to what I’ve actually been dealing with.
There are still plenty of details to polish next, such as handling memory merge conflicts when syncing across devices, and adding a clearer view to inspect or undo individual memories. Those are next on my list.
One step at a time, making sure each line of code and each test holds up.
Assisted by: Gemini 3.8 Flash; copy-edited by Gemini 4 Argon