The memory graph
Someone who has worked on a project knows what depends on what, why things are the way they are, and what broke before. An agent starts every session without any of it. post-cortex keeps that knowledge in a graph, written by the agent itself while it works, and answers it in one call.
Every session starts from a blank page
Section titled “Every session starts from a blank page”You solve a problem in two steps: first you understand it, then you decide exactly what to do.
Say you have a medium-sized software project and a task: change the code that writes the data. Depending on the architecture, that can touch dozens of files. Someone who has worked on the project already knows where that code is, what depends on it, what could break, and how and where to make the change. They carry it in their head.
An agent, however capable, starts every new session from a blank page. It has to gather all of this again with the tools it has:
- it reads the documentation, if there is any and it is still true;
- it walks the code with
grep,catandfind; - it asks the user.
To put the picture together it needs the connections between the parts: what depends on what, why something is the way it is, and where it came from. The basic tools, with all respect to them, give it text. So the agent makes call after call over large amounts of text until the picture comes together. In the example below that is about 160,000 tokens. Even then it does not know what broke here before, because that is not in the code.
One question, three ways
Section titled “One question, three ways”The question comes from real work on post-cortex itself, about 250,000 lines of Rust: the code that writes sessions to storage, SessionPersister, needs a change. What does it touch, and what went wrong in it before?
| Calls | Read by the agent | What it got | |
|---|---|---|---|
grep, then reading the files it found |
6 searches, then the files they hit | 271 matching lines in 70 files; the files hold about 650 KB, roughly 160,000 tokens | lines that contain the words; how they connect only after reading the files; nothing about what broke before |
A code graph (trace over the call graph) |
1 | about 5.5 KB, roughly 1,400 tokens | every caller, three levels deep, each with its file and lines: the record writes that go through it and the 26 tests that guard it |
The memory graph (impact in post-cortex) |
1 | about 6.5 KB, roughly 1,600 tokens | what depends on it and why, and the five problems found in it before, each with its cause and its fix |
What the two single calls returned, shortened.
The code graph, trace of persist_and_wait, the write every record goes through: who calls it, three levels deep.
{ "name": "SessionPersister::persist_and_wait", "at": "memory_system/persister.rs:187-201", "callers": [ { "name": "Records::swap_in", "at": "capabilities/records/commit.rs:66-108" }, { "name": "Records::commit", "depth": 2, "at": "capabilities/records/commit.rs:38-64" }, { "name": "Records::write", "depth": 3, "at": "capabilities/records.rs:116-131" }, { "name": "Records::bulk_write", "depth": 3, "at": "capabilities/records.rs:154-183" }, "... and the 26 tests that reach it, among them retry_of_a_failed_write_carries_removals_and_retirements, loads_racing_a_delete_never_bring_the_session_back" ]}The memory graph, impact of SessionPersister: what depends on it and why, then what went wrong in it before.
{ "entity": "SessionPersister", "dependents": [ { "entity": "RecordEvent", "relation_type": "depends_on", "context": "events are sent after the persister's storage write succeeds" }, { "entity": "StorageActor", "relation_type": "depends_on", "context": "actor routes session persists and deletes through the per-session writer" } ], "risks": [ { "interaction_type": "problem_solved", "timestamp": "2026-10-21", "title": "A retry after a failed write carried empty lists, so deletes and forgets never reached storage", "description": "The failed write now keeps its rows and removals; the retry merges them in front of the new batch." }, { "interaction_type": "problem_solved", "timestamp": "2026-10-18", "title": "A load error was read as 'no such session', and an empty session overwrote the stored one", "description": "Only a typed not-found creates a new session." }, { "interaction_type": "problem_solved", "timestamp": "2026-10-18", "title": "A load racing a delete brought the session back", "description": "Per-session handle generations; deletes retire them all." }, "... 2 more, each with its author, time and whether it still holds" ]}The first two answer where in the code. Only the third answers what we know about it: three ways this code already lost data, and how each was fixed. That knowledge is not in the code, so no search over the code can find it, and an agent that does not have it can bring any of those bugs back.
One line of that answer is out of date: StorageActor was removed from the code on October 22, and the record of its removal did not end its links. What if a link is not stated explains why that happens and what catches it.
What a graph is, and why text search is not enough
Section titled “What a graph is, and why text search is not enough”Text search, by words or by meaning, finds passages that look like the question. It does not know that two things are connected unless they share words. A question about the code that writes sessions will not find the bug report about deleted records coming back, although it is the same code.
A graph stores the connections themselves: things (a service, a library, a bug, a decision) and typed links between them (depends on, caused by, solves). It answers questions about structure, not wording: what depends on this, where did this come from, what does this solve.
Agent memories use three kinds of graph:
| Kind | How it is built | Good at | Where it breaks |
|---|---|---|---|
| Extracted by a model (Zep and Graphiti, mem0’s graph, Cognee) | a language model reads each new text and guesses the entities and links | asks nothing of the writer | a model call, its cost and seconds of latency on every write; links the model invented; the same thing under three names |
| Summaries of communities (GraphRAG) | a language model clusters a corpus and summarizes each cluster | questions about a whole set of documents | built in batches; stale after every change; expensive to rebuild |
| Stated by the writer (post-cortex) | whoever learned something names what it is about and how those things relate, in the same write | exact, typed links at no extra cost | the links have to be stated; the plugin’s instructions ask the agent to do it |
How the graph is written
Section titled “How the graph is written”Most graph-based memories run a separate, usually smaller model. It reads each new text afterwards and tries to guess which entities it mentions and how they connect. That is extremely inefficient, slow and expensive: a model call on every write, seconds of delay, links the model made up, and the same thing under three different names.
post-cortex does something else. While the agent works on the task, it already has the whole context: it knows which code it touched, what depends on it, what caused what, and what solves what. We simply ask it to write that down itself, in a fixed shape, in the same call that stores the knowledge. No other model, no second pass. The links come from the one who knows them, not from someone guessing from the text.
A real record: a problem, its cause and its fix, with the entities and the relation it states.
{ "interaction_type": "problem_solved", "content": { "problem": "A retry after a failed write carried empty lists, so deletes and forgets never reached storage", "solution": "The failed write keeps its rows and removals; the retry merges them in front of the new batch." }, "entities": [ { "name": "SessionPersister", "entity_type": "code_component" }, { "name": "carry_over", "entity_type": "code_component" }, { "name": "lost removals on retry", "entity_type": "problem" } ], "relations": [ { "from_entity": "SessionPersister", "to_entity": "carry_over", "relation_type": "depends_on", "context": "retry merges failed rows via carry_over" } ]}The graph is updated in this same write.
That record and the ones written before it, as the graph holds them:
What it costs the agent
Section titled “What it costs the agent”In the record above, the entities and the relation take about 340 bytes, roughly 85 tokens; the problem and its fix take about 160. The agent pays for those tokens in a call it makes anyway. A separate extraction model reads the whole text again, with its own instructions, on every write.
What if a link is not stated
Section titled “What if a link is not stated”Then the graph does not know it. That is the price of this approach, and it is better to know it. Four things keep it small:
- A record without an entity and a relation is refused, and so is a relation without its reason. Knowledge cannot be stored unattached.
- The radar shows every problem that nothing solves yet. Below is a real case: the fix was recorded, but its record did not say what it solved.
- Every link carries the record it came from, so a wrong one is found and corrected.
- The plugin asks the agent for the links it tends to forget. When a record reads as a correction and does not say which earlier record it replaces, the plugin’s stop check asks for it before the turn ends.
The StorageActor line above is that price, seen in our own memory. The record of its removal linked it to SessionPersister with related_to and did not end the older depends_on, so impact still lists it. Nothing in the graph says it is gone, because nobody wrote that down.
What the graph is made of
Section titled “What the graph is made of”Records
Section titled “Records”A record is one piece of knowledge: a question and its answer, a decision and its reason, a problem with its cause and fix, a code change, a requirement, a definition. Every record names the entities it is about and the relations between them.
Entities
Section titled “Entities”The things the knowledge is about, each with a type: technology, code_component, concept, problem, solution, decision. An entity keeps its aliases and every record that mentions it, so one name leads to everything known about it.
Relations, by the question they answer
Section titled “Relations, by the question they answer”Every relation carries why it holds, who stated it, in which record, and since when.
What breaks if I change it? impact reads these.
depends_on: A does not work without B. Real:RecordEvent depends_on SessionPersister, “events are sent after the persister’s storage write succeeds”. ChangeSessionPersister, andimpactshowsRecordEvent.required_by: B cannot be without A. The same direction, said from the side of the thing depended on.implements: A is how B is done. A change to B reaches A.
How did we get here? lineage reads these.
caused_by: A happened because of B. Real:duplicate derived work on update caused_by MemoryServiceImpl, whose update call persisted the session and applied the graph several times over.leads_to: A results in B.solves: A fixes B. Real:SessionPersister solves duplicate derived work on update. Only throughsolvesdoes the radar know a problem is closed.
What no longer holds?
conflicts_with: A and B cannot both be true. When a record states it, the older records that asserted another relation between the same two things end. They are not deleted; they keep the reason they ended.
Just connected.
related_to: connected, with no stronger relation. It helps search and topics but says nothing about direction. Real:cross-session embedding delete related_to SessionPersister, “found in the same write-path review”.
| Relation | Means | Read by |
|---|---|---|
depends_on |
A does not work without B | impact |
required_by |
B cannot be without A | impact |
implements |
A is how B is done | impact |
caused_by |
A happened because of B | lineage |
leads_to |
A results in B | lineage |
solves |
A fixes B | lineage, the radar |
conflicts_with |
A and B cannot both be true | the history |
related_to |
connected, nothing stronger | search, topics |
Facts change; history stays
Section titled “Facts change; history stays”A record ends when a later one names it in supersedes, when a later one states the opposite with conflicts_with, or when someone forgets it. It is not deleted: it keeps when and why it ended, and ranks lower in search.
A real case from our memory. On October 29 a record said a permission fix was verified on our test server. Two minutes later another record said the first was wrong: the check had not proved anything yet. The second did not name the first, so for nine days both were current, and a search asking whether the fix was verified put the wrong one third, with a score of 0.948. A record that named it in supersedes ended it. The same search now shows it fifth, with half its score, marked with its end:
{ "combined_score": 0.474, "end_reason": "superseded", "valid_to": "2026-11-07T14:25:27Z"}The graph can also be read as it stood on a given date: what the project knew on the day a decision was made.
Two graphs, one store
Section titled “Two graphs, one store”- The workspace graph joins the entities and relations of every session of a project.
impact,lineage,topicsand the radar read it. - The retrieval graph of a session joins its records through their terms, their entities and their relations. During a search, the query’s best hits seed a Personalized PageRank walk, and the records it lands on join the candidates: records that neither words nor meaning reach. On the LoCoMo benchmark’s multi-hop questions this raised recall from 0.777 to 0.809.
What you can ask it
Section titled “What you can ask it”Each question is one call for an agent, or a page of the app: Graph, Topics, Radar.
| Ask | View | What comes back |
|---|---|---|
| What breaks if I change X? | impact |
everything that depends on X, directly and through others, with the reason of each link, and the current decisions, requirements and problems about them: a risk report before the change |
| How did we get here? | lineage |
the problem, what caused it, what solved it and what followed, in time order |
| Everything about X | entity |
its records, decisions, relations and aliases |
| What is this project about? | topics |
groups of connected entities, each with its key decisions |
| What is still open? | radar |
see the radar |
| What do we know about this file? | file |
the past problems, decisions and dependents of one source file |
The app also has a timeline: the records and relations of a period of time.
The radar
Section titled “The radar”When a problem is found, it is recorded as an entity of type problem, with its description. When it is fixed, the fix is recorded and linked to the problem with solves. An agent or a person can write either record, in the same session or another, even weeks later. The radar lists every problem that no solves points to yet. The moment one does, the problem leaves the radar, and the fix stays linked to it: whoever meets the same problem later finds the answer with it.
Nobody keeps that list by hand. The radar computes it from the graph on every call, so it cannot go stale.
This is how it went while this page was written. The radar listed module config null as unsolved: a new organization could not enable a module without a config type.
{ "unsolved_problems": [ { "name": "module config null", "entity_type": "problem", "first_seen": "2026-11-06T15:24", "description": "A new organization cannot enable a catalog module without a config type: EnableModule fails with INTERNAL" }, "... 4 more" ]}It had been fixed that afternoon, but the record of the fix did not say what it solved, so the graph could not know. One record closed it:
{ "interaction_type": "problem_solved", "entities": [ { "name": "module config null", "entity_type": "problem" }, { "name": "empty module config", "entity_type": "solution" } ], "relations": [ { "from_entity": "empty module config", "to_entity": "module config null", "relation_type": "solves", "context": "a module without a config type is enabled with an empty object" } ]}The next radar call no longer listed it.
The radar watches four more things the same way:
- decisions recorded without a reason, until the reason is added;
- questions without an answer, until they get one;
- entities nobody has mentioned for 90 days, such as
SSEBroadcaster, last mentioned on May 17; - two names that may mean one thing, such as
DocumentKeyanddocument keys, until they are merged or marked as different.
A line the radar got wrong is dismissed, and it is not shown again.


Cards and one call for a task
Section titled “Cards and one call for a task”Two short notes that are not part of the graph come with every session: how you work, and how this project works. And one call gathers everything for a task: the cards, the graph around the names in the request and the records that match, sized for the model’s context. Both are on Cards and context for a task.
What it solves
Section titled “What it solves”Every example here comes from the memory that holds the work on post-cortex itself.
- Hidden dependencies. “Can we replace RocksDB?”
impactofRocksDBreturns what depends on it and why. The database schema is created in one transaction “because RocksDB syncs every commit”: its 130 statements, each in a transaction of its own, had taken one second of a 2.4-second cold start. That reason is not in the code next to the transaction; without it, the next change can split it again. The answer also brings the last attempt: on October 21 the database of our test server moved to SurrealKV, and the two engines were measured side by side. At the same search quality SurrealKV wrote about twice as fast and used three to four times the disk, so the test server went back to RocksDB. - Dependencies several steps away. The same
impactgoes a level deeper: this project’s Claude Code plugin depends on our test cloud, which depends on RocksDB. No record says “the plugin depends on RocksDB”. The graph joins two records, each about one of the two links. - Cause and effect. “Why did the write path do the same work twice?”
lineageof the problem returns, in order, what caused it (caused_by MemoryServiceImpl, found on October 18 at 12:12) and what solved it (SessionPersister solvesit, the same afternoon: one writer per session, persists coalesced). - Open work nobody remembers. The radar still lists board_create stages, a report that the tool that makes a board did not offer the stages of its columns: an agent spent its 50 steps on it, changed nothing and left no comment. Nobody marked it as open. It is listed because nothing
solvesit. - A project you do not know.
topicsshows its shape in one call. For this one, the largest group is the workflow module with our test cloud, 140 records, then the work on the production daemon, modules working together with their documents, the app’s data API, the cloud gateway with its control plane, and search with the graph. Each comes with its central entities and its key decisions. - Knowledge that no longer holds. A correction names the record it replaces, and the old one ends instead of standing next to the new one, as in the case above.