nullhex

Memory Redesign: Three Layers Instead of One

7 Apr 2026·11 min read·ai

A month ago we wrote about teaching Claude to remember. The system in that post still exists, in the same shape, in the same files. What changed is what we built around it.

The AI world moves fast enough that an architecture that fits in March needs more layers by April. Not because the March version was wrong. The March version was the right answer to the questions we had then: "where does a memory live, how do we score it, how do we get it back". By the time the leaves came back on the trees there were two more questions on the pile, and the answers turned out to need their own homes.

This is the architecture story for what nullhex's Claude Code memory looks like in April 2026. Three layers, three different jobs, and one house rule that says they only ever talk to each other through well lit hallways.

// what was there

The 2026-03-05 post described an FTS5 full-text index over a directory of markdown files, salience scoring across recency / frequency / relevance, and a hook chain that captured new memories on the way past. Everything in that description still applies. The file layout is unchanged. The MEMORY.md index pattern is unchanged. The salience scoring logic still exists in the same Python module it was in. The FTS5 engine itself sits archived at ~/dev/memory/engine/, where it stays as a working reference and a fallback we hope never to need. None of that work was wasted. It just stopped being the only thing in the picture.

The change is that the picture grew. Three layers grew on top of and around the original one, each one answering a question the original could not. The original answered "what did we say about this". The new layers also answer "what does the code itself say about this", and "is any of this still true". Three different questions, three different stores, all coordinated.

// layer 1: cortex

Cortex is a Postgres plugin with pgvector and a knowledge graph schema, and it is now the canonical store for what Claude Code knows about projects. The migration ran on 2026-03-31 and moved 152 markdown memories across into the database, where they were promoted into 281 entities and 1,482 relationships between those entities. Vector search is enabled. The graph half of the schema is the part that matters most: a memory by itself is a flat string of text, but a memory connected to the entity it is about, and to the other memories that share that entity, is something a session can actually walk.

(Live counts have moved on since the migration. Session capture has been running steadily for a week, and the database is north of two thousand memories now. The 152 number is the migration snapshot, the moment when the old store was emptied into the new one. The growth since is the proof that the capture pipeline is doing its job.)

The reason for a graph at all is composition. Flat search will find every memory that mentions a particular function name, and that is a useful thing to be able to do. A graph will tell you that the function is part of a service that depends on a queue that lives in a worker that we redesigned in February for a reason that we wrote down at the time. That second answer is not a longer version of the first one. It is a different shape of answer. The original flat search engine could not produce it, and a vector-only system could not produce it either, because the relationships between entities are not just semantic similarity. They are structured, and the structure is the point.

Cortex ships with a set of plugin lifecycle hooks that took over the work the original custom hook chain was doing. SessionStart injects relevant context into a fresh session. PostToolUse captures new observations as they happen. SessionEnd handles graceful shutdown and final commits. There is also a compaction checkpoint that fires when the conversation context is about to be compressed, so that anything important on the way out gets saved before it is summarised into oblivion. Each of those hooks superseded a corresponding piece of the older chain, one at a time, with the original still on disk during the cutover so we could compare both running side by side.

A working example. A session starts on the BayOS codebase. Before the prompt has even rendered, the SessionStart hook has pulled the relevant Cortex entities for "BayOS / queue worker / February redesign" and dropped them into the opening context. The session begins already knowing that the queue worker was changed to use a different backoff strategy in February, that the change was made because a particular customer was hitting a particular failure mode, and that there is a regression test that pins the new behaviour. None of that had to be re-explained. None of it needed a "remember when we" preamble. The graph knew where it lived and brought it along.

// layer 2: nexus

Cortex knows about decisions. It does not know about call graphs. That is what GitNexus is for.

GitNexus is a code knowledge graph. It uses tree-sitter to parse a repository into an abstract syntax tree, then it stores the nodes and the relationships between them in its own LadybugDB store. It is not the same database as Cortex. They are deliberately two different stores, because they answer different questions, and because forcing them into one schema would have been the kind of clever architecture that looks elegant in a diagram and falls over the first time someone needs to query it.

We adopted GitNexus on 2026-04-05 after a comparison against several similar tools. The evaluation lives in reference_tool_evaluations_2026_04_05.md for the curious. After installation we indexed three repositories. CoachSync came in at 4,122 nodes across 147 community clusters. BayOS came in at 4,111 nodes across 377 clusters. The pa-mcp tool, which is much smaller, indexed at 198 nodes across 12 clusters. Those node counts are not lines of code. They are functions, classes, modules, and the edges between them, with Leiden community detection used to cluster them into related neighbourhoods that get treated as natural groupings during search.

Once GitNexus is installed it slots in as a set of PreToolUse hooks. When the session is about to run a Grep or a Glob, the hook intercepts and augments the query with call-graph context. The session does not have to know GitNexus exists. It just suddenly gets back better answers when it asks about the code, because the answers now include "and here is everything that calls this thing, here is everything that this thing calls, here is the cluster this lives in".

A working example. A refactor on CoachSync that needs to change the signature of a function used by the booking flow. In the old world, the next step was a Grep for the function name across the repository, then manually walking through every match and checking which were real callers and which were just mentions in tests, comments, or documentation. The Grep finds twenty hits, and the human has to triage them. In the new world, the same Grep returns the twenty hits with the GitNexus call-graph annotation attached. Eleven of them are direct callers in the booking flow cluster. Three are indirect callers two hops away. The other six are tests and documentation. The triage is already done, because the call graph already knew.

// layer 3: freshness

Memory rots. A note about a service that lives at one address last month becomes a lie when the service moves. A reference to a config file that has since been split into three files becomes a lie. A statement about what a function does, written before the function was rewritten, becomes a lie. None of those lies announce themselves. They just sit in the store, waiting to be retrieved by an unsuspecting future session that has no way of knowing the underlying world has moved on.

The freshness layer is what tells us when a memory is lying. It is a Bun TypeScript CLI that lives at ~/dev/memory-freshness/, and it runs on a 4-hour cron. The CLI is exposed system-wide as mf, so you can also run it on demand from anywhere on the box. Every memory in the corpus gets a score using an Ebbinghaus decay curve, with the half-life calibrated to the memory type so that user preferences decay much slower than project status notes. The decay is not the only signal. The freshness daemon also corroborates each memory against a small set of external signals: recent git activity in the repository the memory is about, whether the file the memory references still exists at the path it claims, and whether the systemd services it talks about are still defined and running. Two or more fresh signals override the decay-based staleness flag. One signal is not enough.

When a project memory crosses seven days of compounded staleness with no corroboration, the freshness daemon pings @cat_devbot on Telegram with a one-line summary. The morning briefing also includes a freshness section, so even on quiet days the stale list is visible without anyone having to go and look for it. The point of the system is not to delete memories. Deleting is easy and rarely the right answer. The point is to surface memories that are lying, so that the next session can fix the lie instead of inheriting it.

The freshness CLI shipped on 2026-03-31. It carries 83 tests, was built across four GSD phases against 25 written requirements, and was designed to do exactly one thing well. There are deliberately no escape hatches that try to be cleverer than the human about what to keep and what to throw away. The tool's job is to point. The decision to act stays where it belongs.

// why three layers

A reasonable question at this point is "why three". Could one of these have done the job of the other two with a little more work?

Cortex does not know about call graphs. Vector search over flat text is excellent at "what did we say about this thing", and the graph schema makes it excellent at "what is connected to what we said". It is not excellent at "what calls this function in CoachSync", because it does not have a parser, and adding one would have meant teaching the wrong store the wrong job. GitNexus does not know about decisions. It can tell you the entire call structure of a repository, but it has no idea why any particular function exists, what we tried before this version, or what we ruled out. The freshness daemon does not know either of those things, and it is not trying to. It is the smoke detector. It does not put out the fire and it does not investigate the cause. It just makes a noise when something needs looking at.

Three different questions: "what did we decide and why" lives in Cortex. "What calls what" lives in Nexus. "Is any of this still true" lives in freshness. Composable, not redundant. Each layer fills a gap the others would have to be turned into something they are not in order to fill, and turning a tool into something it isn't is how you get architectures that nobody can debug a year later.

// what changes day to day

Concrete moments make the difference clearer than diagrams.

A new session on BayOS opens, and it already knows the architecture, because Cortex has injected the relevant entities and relationships into the opening context. There is no preamble. The first message is about the actual problem, not about who BayOS is or why we use it.

A refactor on CoachSync needs to walk every caller of a function. Instead of the old Grep-and-triage routine, the GitNexus PreToolUse hook augments the search with call-graph context, and the relevant callers come back with their relationships labelled. The triage is shorter and the misses are fewer.

A memory line about a config file in pa-mcp shows up in the morning briefing as stale. The freshness daemon noticed the file moved, the script that runs every four hours flagged it, the briefing surfaced it. The next session opens, sees the flag, updates the memory in two seconds, and moves on. The decision was the human's. The pointing was the daemon's.

The end of every session triggers a Cortex SessionEnd hook that captures whatever was learned on the way out. None of that has to be remembered or written by hand. It happens because the lifecycle hook fires whether anyone is paying attention or not. The next session inherits it for free.

The trick to a memory system is the same as the trick to a server room. It should be the thing you don't think about until you need it, and then it should be exactly where you left it. Three layers, three jobs, one quiet hum in the background. So far it is holding.