July 6, 2026

Memory stops being one drawer, becomes a house

Chalie's memory system got split into dedicated verticals for facts, contacts, places, and system state, plus a smarter way of recognizing when two facts are really the same fact.

Yesterday I went after a problem that’s been quietly limiting how good Chalie’s memory can get: everything it remembers — facts about you, saved places, contacts, its own internal bookkeeping, things it noticed on its own — was passing through one generic, catch-all storage path. That works when the system is small. It stops working the moment different kinds of memory need to behave differently, and bugs were hiding in the gap between “generic enough for everything” and “correct for any one thing.”

So the day’s work was decomposition: give each kind of memory its own model and service, built on a shared base that still owns the plumbing they all need — search indexing, decay, the bookkeeping that tracks when something was created, reinforced, or superseded. Facts about you now live in their own lane with key-scoped forgetting and a decay curve for things that haven’t come up in a while. Saved places got the same treatment, which also surfaced a real bug — the generic path had quietly dropped places from the set of things that get superseded on update, so re-saving “home” with new coordinates piled up duplicates instead of replacing the old entry. Contacts got their own vertical with fuzzy lookup, so a partial name still resolves. Chalie’s own internal state — the running summary it keeps of you, its background-worker checkpoints — moved into a system lane read and written by exact key and kept out of the search index, so machine bookkeeping never pollutes what gets recalled in conversation.

The more interesting piece is what happens when a fact actually gets stored. Tell Chalie your birthday one day, then mention your “birth date” weeks later — same fact, but a naive key-based store files them separately and you end up with two half-answers instead of one right one. The fix: every new fact key gets embedded and matched against a preshipped concept table, and a confident match files it under the canonical key instead of the raw one. Each concept carries a conflict rule too — some facts simply supersede the old value (your address changes, the new one wins), some coexist as multiple live values (more than one email), and some are immutable, where a contradicting write gets rejected rather than silently overwriting something that shouldn’t change. Keys that don’t confidently match still get stored under their raw key, with the near-miss logged for where the concept table needs expanding.

None of this matters if the underlying decay-and-cleanup engine doesn’t actually run, and it turned out it didn’t — a swallowed error had been silently no-op’ing every decay cycle, so old memories weren’t fading and stale rows weren’t being purged. That’s replaced with a small orchestrator that runs each memory type’s cleanup in isolation, so one subsystem failing loudly stops that subsystem, not the whole cycle. The search-index sync that keeps full-text and vector search current on new memories had also gone missing along the way — restored, so recall now reflects what’s actually been stored.

One lane is still uncovered: discovery memories, the things Chalie notices proactively rather than what you’re told directly, got their own home too, but I haven’t yet audited the older callers the way I did for places. That’s the next pass — make sure every caller talks to the right vertical before this becomes the foundation the next round of memory features builds on.

  • Facts, contacts, places, system state, and discovery memories each moved off the generic data-graph gateway onto dedicated models and services

  • Fixed a real bug where re-saving a place under an existing name (e.g. “home”) piled up duplicates instead of replacing the old entry

  • New facts are now canonicalized against a concept lookup table, so different phrasings of the same fact (birthday vs. birth date) land under one key instead of splintering

  • Per-fact conflict rules now govern whether a new value replaces the old one, coexists alongside it, or gets rejected outright when it contradicts something that shouldn’t change

  • Replaced a silently broken decay cycle (a swallowed error had been no-op’ing cleanup entirely) with an orchestrator that isolates failures per subsystem; restored full-text/vector search indexing on newly stored memories