Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions site-astro/src/content/blog/an-index-cannot-answer-twice.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Asking "what calls this function" without a graph means a text search, then read

The same applies to reverse imports, to inheritance chains, and to the general class of question where the answer is a relationship rather than a location. Text search is bad at those. A graph is good at them. Fine.

## The thing it does not change
## You cannot traverse a graph until you know where to enter it

What a graph makes cheaper is the individual hop. What it does not touch is how many hops you need.

Expand All @@ -33,7 +33,7 @@ You cannot traverse a graph until you know where to enter it. Finding the entry

When I traced what these tools do at that first step, the answer was blunter than I expected. The entry point is found by text search: a grep, essentially, against the repository, with the results then sorted using the graph. In the common case, the graph isn't being traversed at all. It's used only to rank the output of a text search.

## Ranking by popularity has a specific failure
## The ranking never sees your question

That ranking step is worth looking at closely, because the way it usually works has a consequence people do not expect.

Expand All @@ -59,7 +59,11 @@ The edge count was several times larger than the one I maintain. Once builtins a

If you are evaluating one of these, the useful question isn't how many edges there are. It's what fraction of call sites (the individual places in the code where a call happens) resolved at all, and of those, how many had exactly one candidate. Those two numbers are the honest ones and they are rarely published.

## The part that actually bothers me
The same applies to the three-dimensional rendering, since it comes up. It is for you, not for the agent. The agent receives text; it never sees the picture. A beautiful graph view is a fine thing to build and I understand why it demos well, but it is evidence about a presentation layer, not about whether an agent finished a task in fewer steps.

The metric that would settle these arguments is unglamorous: for a fixed set of tasks, how many actions did the agent take before it had the right answer. Steps, not tokens, because tokens follow from steps. I have not seen that number published by anyone, including by me, and I am wary of any comparison that leads with something else.

## The two hours that nothing recorded

Everything so far is a quality argument, and quality arguments can be answered with better engineering. Someone can improve the resolver, add the query to the ranking, expose confidence. The next objection cannot be fixed that way, because it is about what kind of object a graph is.

Expand Down Expand Up @@ -130,18 +134,14 @@ That is the claim in one line. A graph makes each hop cheaper. It does not reduc
<figcaption>A graph makes each hop cheaper. It does not reduce how many hops you need, and because everything in it was already in the source, it cannot hold a conclusion, so the second occurrence of a question costs what the first one did.</figcaption>
</figure>

## A note about the visualisation

Since it comes up: the three-dimensional rendering of your codebase is for you, not for the agent. The agent receives text. It never sees the picture. A beautiful graph view is a fine thing to build and I understand why it demos well, but it is evidence about the tool's presentation layer and not about whether an agent finished a task in fewer steps.

The metric that would settle these arguments is unglamorous: for a fixed set of tasks, how many actions did the agent take before it had the right answer. Steps, not tokens, because tokens follow from steps. I have not seen that number published by anyone, including by me, and I am wary of any comparison that leads with something else.
## Why the missing record has to be authored

## What has to be different
If the missing thing is a conclusion, two properties follow, and neither is negotiable.

If the missing thing is a conclusion, then the record has to be written by whoever reached it, and it has to be a separate object from the derived index.
It has to be written by whoever reached it. A conclusion is not recoverable from the source afterwards: that is exactly what made it a conclusion rather than a lookup. The only party holding the invariant, the hypothesis that was checked and cleared, and the reason two files are coupled is the one that just spent the two hours.

That is the split coldstart is built around. There is a static index, and it is deliberately ordinary: it ranks files against the words in your question using paths, symbol names, exports, and references, and it can show you the shape of a file and who uses it. It is the cheap-hop layer and I make no larger claim for it.
And it has to be a separate object from the derived index, because it fails in the opposite direction. A derived index cannot go stale; regenerate it and it is correct by construction. An authored record can, and does, the moment someone edits the file it was about. Keeping both in one structure means either regenerating the notes away or serving them with the index's confidence, and both are worse than keeping them apart.

Separately there is a notebook. After a real task, the agent writes down what it worked out, anchored to the files it actually used. That record is not derivable from the source, which is the whole point, and it is also the reason it can go stale in a way the index cannot. So each note carries the state of the files it was based on. If those files changed, the note is not served as truth. It is served as a claim that needs re-checking, which is roughly what a colleague saying "this was true last month" gives you.
That is the split coldstart is built around, and it is worth stating briefly because the argument does not depend on it. There is a static index, deliberately ordinary: it ranks files against the words in your question using paths, symbol names, exports, and references, and it can show you the shape of a file and who uses it. It is the cheap-hop layer and I make no larger claim for it. Separately there is a notebook. After a real task, the agent writes down what it worked out, anchored to the files it actually used, and each note carries the state of those files at the time it was written. If they have changed since, the note is not served as truth. It is served as a claim that needs re-checking, which is roughly what a colleague saying "this was true last month" gives you.

Fewer notes that are true beat a large derived structure that cannot hold a conclusion. Both layers are useful. They are answering different questions, and the mistake worth avoiding is expecting the first one to do the second one's job.
Both layers are useful, and the mistake worth avoiding is expecting the first to do the second's job. The general form, whatever you end up building: anything derived from your source can only tell you what is in the source. It cannot tell you what was already checked and ruled out, and on the second occurrence of a hard question, that is most of what you wanted to know.
30 changes: 23 additions & 7 deletions site-astro/src/content/blog/codebase-memory-is-not-agent-memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,28 @@ keywords: "codebase memory, agent memory, persistent memory for coding agents, A
kicker: "Memory"
ogDescription: "Remembering a person and remembering a place are different problems. Codebase memory needs its own shape, not a copy of conversational memory."
publishDate: 2026-08-08
readingTime: "6 min"
readingTime: "7 min"
tags: ["memory", "notebook", "codebase-memory"]
next: "why-coldstart-makes-zero-llm-calls"
---

Most things called "memory" for an AI agent are about a person. The agent talked to you last week, and it should remember your name, your preferences, the project you mentioned, the decision you already made so it doesn't ask again. That is a real problem and a reasonable one to solve with a store of facts about a user, retrieved by similarity when a new conversation starts.

I build a different kind of memory, and for a while I described it the same way, because the word was already sitting there. It took getting the design wrong once to notice it is not the same problem at all.
I build a different kind of memory, and for a while I described it the same way, because the word was already sitting there. Borrowing the word turned out to mean borrowing an assumption with it, and I did not notice until the assumption was already shipped.

## The retrieval keys that never expired

The mistake was in the retrieval keys.

Every note in coldstart's notebook carries a handful of short alias strings: the words a later search should be able to find it by. I let them accumulate. Each time an agent updated a note it contributed whatever vocabulary it had been using, and nothing was ever removed.

That is the right behaviour for memory about a person. You told me in March that you prefer typed configs. That is still true in August unless you say otherwise, so a store of facts about you should union and hold, and forgetting would be the bug.

A note about a file is not like that, and the reason is specific. Much of the vocabulary an agent brings to a file arrives from an incident. Something was broken, the symptoms had names, and those names are what ended up written down. Once the bug is fixed, the symptom stops being a description of the file. It is a description of a week.

So the aliases silted up. Going back through my own notebook I found notes still keyed on phrases like "capture never fires" and "stop hook fires rarely": accurate accounts of a regression that had been fixed a while earlier, still attached to a note whose actual summary had moved on, still pulling that note to the surface whenever those words came up. Nothing was corrupt. Every one of those strings had been true when it was written. They had simply outlived the thing they described, and nothing in the design gave them any way to expire, because I had built them on the assumption that a fact once stated holds until it is contradicted.

The fix was to split the field in two. Names for stable things about the file accumulate, as they should. Words tied to whatever the note currently claims get replaced wholesale by the next write that changes the note's substance, so silence means "not carried forward" rather than "keep the old ones." That is a small change. The reason it was needed is the entire subject of this post: I had answered a codebase-memory question with a conversational-memory answer, and the two problems disagree about whether facts expire.

## What a codebase already remembers

Expand Down Expand Up @@ -70,12 +84,14 @@ That's the part conversational memory tooling doesn't have to solve and codebase
<figcaption>A preference has no ground truth to check against, so it just holds until you say otherwise. A note about code sits next to the file it describes, and that's what lets the system catch the note going stale instead of quietly acting on it, not extra ceremony.</figcaption>
</figure>

## Where the notes actually come from
## Who is qualified to write one

One more difference falls out of the same root. For a person, the agent is the only witness: it heard what you said, and that is the end of it. For code the ground truth is on disk and anyone can re-read it, which means the note worth having is precisely the one that *isn't* readable off the file, and the only party holding that is the agent that just spent the turns working it out. That argument has its own post, [made at length](/blog/notes-should-be-written-by-whoever-read-the-code/). The short form: a codebase note is a byproduct of the task, not a later extraction pass over a log.

The other difference is who's qualified to write the note. For a person, the agent is the only witness. For code, the agent that just spent real turns tracing a call path, reading three files to find where a value actually gets set, running the failing case down to the line, is also the only witness that has the full context right now. A note written after the fact, by summarizing a transcript, is a paraphrase of a paraphrase, not the same thing at all.
## Remembering a person, remembering a place

So the natural place to write a codebase note is the moment the work happens, by the agent doing the work, not as a separate memory-extraction pass over a log later. The note is a byproduct of the task, not a second task.
I stopped calling this "agent memory" because the name imports the wrong answers to questions it does not look like it is answering.

## Naming the category
A memory about a person is a record of what was said, and saying it is what made it true. There is nothing else to check it against, so it holds until it is restated. A note about code is a record of what was *found*, and the thing that made it true is still sitting on disk, changing, without telling you. That single difference is what forces the address, the freshness stamp, the expiry, and the split between a name and a symptom. None of it is extra ceremony. It is what remembering a place costs when the place can move.

I stopped calling this "agent memory" because it invites the wrong comparison. This is memory for a codebase specifically, not a bigger or smarter version of remembering a user's preferences: address it to the code, expire it against the code, and let the agent that did the reading be the one who writes it down. Once you name it as its own thing, the design questions get a lot easier to answer, because you stop importing answers from a problem that isn't the one in front of you.
Every design question I had got easier the moment I stopped answering it from the other problem.
12 changes: 7 additions & 5 deletions site-astro/src/content/blog/from-four-tools-to-two.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ It started with four. There was an overview operation for locating files, a stru

Both traversal operations were deleted. The path there is more instructive than the destination.

## Deletion one: the two tools that were really missing fields
Four removals follow. They are grouped by what each one turned out to be rather than numbered, because the count was never the point: each was a different wrong assumption I had to be shown, and only one of them was about having too many tools.

## The two tools that were really missing fields

The important thing about removing the traversal tools is what did not get removed. The graph stayed. The import edges, the call edges, the reverse lookups, all of it survived and is still there.

Expand Down Expand Up @@ -95,7 +97,7 @@ Which leaves two real options. Default it on and accept the cost, or move it to

The general version: if a capability requires the agent to opt in, and the agent only knows to opt in after seeing the result, it will not get used. Make it the default or fold it into something else. A third option of explaining it more clearly does not exist.

## Deletion three: the labels
## The labels that were guesses in the costume of facts

The first version tried to tell the agent what each file *was*.

Expand All @@ -109,7 +111,7 @@ The deeper problem is a division of labour. The agent reading my output is a lan

So the rule I ended up with: return evidence, never classification. Paths, symbol names, exports, references, line numbers, the matched lines themselves. Things I can point at in a file. No role labels, no capability tags, and no generated descriptions of what a file is for. Let the model do the interpreting, since it is better at it than my heuristics and it is going to redo the work anyway.

## Deletion four: the duplicate
## One operation, two names

A smaller one, included because the lesson is not the obvious one.

Expand All @@ -123,9 +125,9 @@ So the tools were renamed to match the shell verbs exactly, the duplicate verb w

The cost of an extra name isn't the code: it's that everything you write about your tool now has to be conditional.

## Deletion five: the server that served
## The daemon that served nothing

The third removal is the one I would not have predicted.
The last removal is the one I would not have predicted.

The architecture had a background process, a daemon, that held the index in memory and answered queries over a local connection. This is the obvious shape. The index is expensive to build, so build it once, keep it warm, and have the command line be a thin client that asks the running process. There was a bridge layer and an HTTP daemon (a background server, listening for requests the way a website's server does) to do exactly this.

Expand Down
Loading
Loading