Skip to content

Agent memory knowledge graphs compared: does a read change the graph?

The question in the title has a short answer, so here it is in the first line. All six of them. Mem0, Zep with Graphiti, Cognee, Letta, Supermemory and Mnemoverse each build a graph, ours included. If you came for a list of which agent memory tools use a knowledge graph and which do not, the list is: all of them.

Which is exactly why the question is worth replacing. A graph is not one thing. These six do not agree on what the nodes are, on when the edges get built, or on whether anything about the graph ever changes once it exists. So this article asks one narrower question of all six, including the one we sell: does anything in that graph change because of what a search returned? Not because you wrote something new. Because you read.

This is the written half of a video asking the same question, and the sections below follow its eight chapters in order, so that anyone arriving from its end card can pick up where they stopped.

TL;DR

  • All six build a graph, and the word covers four different objects: entities, memories, documents, and concept labels. A feature list that says "knowledge graph" on six pages is comparing four things.
  • In all six the edges are created on the write path. Extraction, linking, and then the work is finished before you ask anything.
  • Two of them do something extra at query time, Cognee through a neighbourhood depth parameter and Zep through breadth-first search. Both are optional, both are off unless you ask, and both walk the graph and find nodes. Walking is not changing, and for Cognee the vendor says so in its own words.
  • One of them writes on a read and not to an edge. Mem0 reinforces every memory a search returns, on the memory's own access history, opt-in and off by default.
  • Twenty searches for the vocabulary of this mechanism, across five vendor organisations, return zero files, re-run on 8 September 2026 and printed below so you can run them yourself, with two controls.
  • The one place where a retrieval demonstrably writes the edge is a research repository, not a product.
  • Disclosure is in the next section rather than the footer: we build one of the six, we describe this mechanism in public, our engine is closed, and on this question that is the weakest position on the page.

Disclosure, and it goes first

We build one of the six. Mnemoverse is a hosted memory layer for agents, and on the single question this article asks we hold the weakest row in it.

Our own public source names the mechanism this article goes looking for. The memory server we publish prints it in a line the user sees:

concept-to-concept links learned from concepts that occur together as memories are stored and used

github.com/mnemoverse/mcp-memory-server, src/index.ts, read from the raw file on 8 September 2026. The comment directly above that line names the operation without a metaphor: "co_activate links query concepts", continuing on the next line of the comment, "to result concepts on use". And a companion repository for one of our papers, public and Apache-2.0, states in its definitions section what a concept-graph edge carries and what moves it, with reads named among the things that move it.

So some of this is written down where anyone can read it, and a great deal more of it sits where nobody outside can look. What is open is description. What is closed is the part that would settle it. mnemoverse-core is not one of our public repositories: an anonymous request to the GitHub API for it returned 404 on 8 September 2026, while the same anonymous request for our memory server returned 200.

Five of the six say nothing about this mechanism in public at all, and silence promises nothing. We have said more and shown less. If you are choosing on evidence rather than on ambition, that runs against us here, and it should. Every search in this article is written out so that you can run it against us first.

All six build one, and that is the least interesting thing about them

Start with the answer nobody disputes. Zep calls its structure a Context Graph: "Zep's temporal Context Graph is the unit of agent memory. Nodes are entities, and edges are facts or relationships. The graph updates as new data arrives." Cognee extracts entities and relationships with a model and inserts them into a graph store. Mem0 ships a feature named Graph Memory. Supermemory sells a memory graph on its own product page. Letta resolves links between memory files and its desktop application draws the result. We ship a learned association graph.

Six products, six graphs, and the word is doing different work in each sentence. That is why the feature list cannot separate them, and it is why the rest of this page is organised around three narrower questions instead: what the nodes are, when the edges are made, and what moves them afterwards.

The third one splits the field unevenly, and the exposed row is ours.

What the graph is made of

The nodes come first, because the word covers four different objects across these six.

Entities, in the systems that extract them. Zep's nodes are entities and its edges are facts, in the sentence quoted above. Cognee's pipeline does the same job with a model and its own extraction prompt, and its retrieval guide is explicit that the node selection leads and the edges follow: "An edge is only carried over if both of the nodes it connects were selected". This is the shape most people mean when they say knowledge graph.

Memories, in Mem0, and the word linked there is worth slowing down on. Mem0's graph is bipartite. Entities are stored once and embedded, and "Over time this forms a graph: a web of entities, each connecting all the memories that mention it." Two memories are connected when they share an entity: "When the same entity appears in more than one memory, those memories are linked through that entity." The asterisks are in the served markdown, not ours. There is no learned weight on that connection, and the vendor rules out typed relations in the same breath: "It does not assign typed, labeled relationships between entities". Connected through a shared thing, rather than connected to each other.

Documents, in Letta, and this is a different object again. A wikilink resolver ships in letta-code, the wire protocol calls the parsed result graph edges in its own comment, "When true, include parsed file references for graph edges.", and the desktop application renders it. The nodes are Markdown files the model itself wrote. Nothing weights them, and Letta's own published memory showcase, letta-ai/ezra-memory, states what a link is for: "A double-bracket path is a discovery link, not an automatic include or semantic retrieval instruction." That same repository is the sharpest available measurement of the object, and we counted it from the downloaded default branch on 8 September 2026: 48 Markdown files, and between them one wikilink and four relative Markdown links. Five edges across forty-eight nodes. It is a document link graph, unweighted, and it exists to be looked at.

Facts on facts, in Supermemory, whose own surfaces disagree. Their documentation describes a graph of facts on top of other facts and says "When content is processed, new facts connect to existing ones through three relationship types." Their published package types those three as strings, export type MemoryRelation = "updates" | "extends" | "derives", and carries no weight, strength or access count on the edge, though their generated SDK documents the field as an open enum, so it is not closed at three. Their research page used to print a colour-coded legend whose first class was Entities. Checked on 8 September 2026, both supermemory.ai/research and supermemory.ai/research/ answer 301 to an anchor on the home page, where the caption now reads "Every learning links to the entities, sources, and moments around it, and the graph grows with use", and the word contexts occurs zero times in the extracted text of that page. If you have seen the older three-word legend quoted anywhere, including by us, that is what happened to it. The accurate sentence today is that Supermemory rejects the hand-maintained triple as a storage format rather than rejecting entities, and that its surfaces do not agree with each other. Naming that is more useful than resolving it.

Concept labels, in ours. Our nodes are not entities and not memories. Our own library page states the distinction and the cost of it: "Mnemoverse ships a learned associative graph, not an asserted knowledge graph. Its edges carry no relation types, no direction, and no entity resolution, so the association layer never asserts that two records denote the same thing".

And one pair that gets quoted as though it were one company. Graphiti is the open engine and you can read its edges. In graphiti_core/edges.py, downloaded on 8 September 2026, there are five classes deriving from the base edge, EpisodicEdge, EntityEdge, CommunityEdge, HasEpisodeEdge and NextEpisodeEdge, and across that file the strings weight, strength, access_count, usage_count and retrieval_count occur zero times. Four of the five declare no fields of their own at all, and carry only the base identifiers, the group, the two endpoint identifiers and a creation timestamp.

That sentence about Graphiti's edges is a sentence about Graphiti. It is not evidence about what Zep the hosted product runs, and this article will not use it that way. Zep says both halves of this itself, which is why it has to be stated carefully rather than in one line. Its machine-readable index calls Graphiti "the engine that powers Zep's agent memory", and its Graphiti overview page describes the hosted product as built "on top of Zep's proprietary Context Graph Engine". The formulation that survives both is: Graphiti is the published part, Zep is that plus a closed layer, and anything read out of the Graphiti source proves something about the published part only. The closed layer is not hypothetical: Observations are a live Zep feature with a documentation page of their own, and they do not exist in the Graphiti codebase.

Four kinds of object, one word, before anybody compares two of these products on a feature table.

When does the graph get built?

The second question separates them better than the first. In all six, the edges are created when something is written. Entities extracted, links made, and that work is finished before you ask anything. One of the six also says an edge is created on a read, and that one is us; it is held to its own section below, because it is the uncomfortable one.

Two of them offer something extra at query time, and both deserve the credit. Cognee ships a neighbourhood depth parameter that triggers real multi-hop expansion, and its own retrieval code names what comes back: "expansion nodes discovered via neighborhood traversal". The parameter is not on by default, and the honest way to say that is by pointing at the shipped source rather than at a documentation sentence, because in the vendor's own documentation the word default appears beside this parameter only where the visualization surface is described and in a table comparing parameter surfaces, never as a statement about what retrieval does when you leave it alone. In the code downloaded on 8 September 2026 it is declared neighborhood_depth: int | None = None at the recall entry point and neighborhood_depth: Optional[int] = None in the graph completion retriever.

Zep documents the same shape under a different name: "You can enable breadth-first search to expand results around specified graph nodes." The parameter that seeds it is bfs_origin_node_uuids, and the vendor's own parameter table gives its default as a dash and its required column as No, with the prose calling it optional in the vendor's own word.

Both walk the graph and find nodes. Walking is not changing. For Cognee that is the vendor's own statement about its own retrieval object: the memory fragment built for a query "is never read directly from, or written back to, the full persisted graph". The code agrees, and one detail is worth stating before a reader finds it and thinks it was missed: Cognee does ship an access-tracking path that can stamp a read, in cognee/modules/retrieval/utils/access_tracking.py. It returns immediately unless an environment variable is set, os.getenv("ENABLE_LAST_ACCESSED", "false"), and the write it performs sets a last_accessed column on a relational record. It moves no weight on any edge and creates no node.

For Zep the same conclusion has to be stated more narrowly, and here is the boundary in the same paragraph as the claim. Zep's served engine is closed, so nobody outside can watch what a search does to storage. What can be checked is the vocabulary on every page of the vendor's own documentation that touches this, and all of it is the vocabulary of returning results: expand results, biases results toward, starts breadth-first searches from. Zep documents no write on the search path anywhere we read. That is a statement about the documentation, not about the engine.

Mem0 earns two careful paragraphs here, for two different reasons. The first is that its graph does more than reorder a finished list. The candidate pool is deliberately over-fetched before the boost is applied, which the vendor states in prose on a neighbouring page, "Widens the candidate pool (top_k × 3, with a floor of 50) so reordering has room.", so the entity link decides which memories occupy the slots you actually get. What it cannot do is introduce a memory from outside that pool or below the semantic threshold.

The second is the real counter-example on this whole page, and it belongs here rather than in a footnote. In Mem0 a read does write. Their memory decay documentation says it twice, once as a principle, "every time a memory is returned in a search it gets a small reinforcement", and once as a numbered step in the search pipeline, "Records a fire-and-forget reinforcement against each returned memory". The exactness that matters: on that entire page the words edge, graph, connection and link occur zero times, checked on 8 September 2026. The reinforcement lands on the memory's own access history. It is not a connection that got stronger because two things came back together. And the whole feature is "opt-in per project and off by default".

Does anything change when you read?

Now the question this article is built on. Not whether the graph decides what you get, because in several of these it plainly does. Whether what you got changes the graph.

To answer it on a surface that a documentation sweep never reaches, we went looking for the vocabulary this mechanism has, in each vendor's own code: hebbian, co-activation, spreading activation, co-retrieval. Four terms, five organisations, twenty searches. Every one is written out here so that you can run it yourself.

text
"hebbian" org:mem0ai
"co-activation" org:mem0ai
"spreading activation" org:mem0ai
"co-retrieval" org:mem0ai
"hebbian" org:topoteretes
"co-activation" org:topoteretes
"spreading activation" org:topoteretes
"co-retrieval" org:topoteretes
"hebbian" org:letta-ai
"co-activation" org:letta-ai
"spreading activation" org:letta-ai
"co-retrieval" org:letta-ai
"hebbian" org:supermemoryai
"co-activation" org:supermemoryai
"spreading activation" org:supermemoryai
"co-retrieval" org:supermemoryai
"hebbian" org:getzep
"co-activation" org:getzep
"spreading activation" org:getzep
"co-retrieval" org:getzep

Those are GitHub code search queries; the command form we used is gh api -X GET search/code -f q='"hebbian" org:mem0ai' --jq .total_count. Re-run on 8 September 2026, all twenty return zero.

Two things about that zero, because a number from an instrument is a statement about the instrument first. GitHub code search returns files, not occurrences, and its zero is a zero in an index rather than a zero in the world. It does not index every public repository: we confirmed that on one of our own public repositories the same day, where a phrase that sits in the README of the default branch is reported by the code search as zero files, while the raw file carrying it answers with a code of 200.

So the zero needs controls, and it has two. The first is a positive control on the same instrument: "memory" org:mem0ai and its four siblings return files in every one of the five organisations, so the index is answering for each of them. The second is stronger and does not depend on the index at all. We downloaded the default branch of each vendor's main repositories on 8 September 2026 and grepped the files directly, including the spellings without a hyphen that a code search does not match. mem0ai/mem0, 1,788 files. topoteretes/cognee, 3,594 files. topoteretes/cognee-rs, 1,490 files. supermemoryai/supermemory, 772 files. getzep/graphiti, 363 files. getzep/zep, 936 files. letta-ai/letta-code, 2,168 files. Zero for all four terms in every one of them. One thing that turns up on the way and would otherwise look like a broken probe: letta-ai/letta no longer holds the source, and says so in its own README, "This repository now serves as a landing page for the Letta project."

The documentation corpora say the same thing where a vendor publishes one. The single-file documentation corpus of Cognee, Supermemory and Mem0, downloaded the same day, contains zero occurrences of all four terms. Zep publishes no such file, so its documentation would have to be walked page by page, that walk was not finished, and no claim on this page rests on it.

So the answer is not that nobody does this. It is that on the surfaces named above, five of the six say nothing about it in public, and one of the six says a fair amount in public and keeps the part that would prove it closed. The one is us.

And here are the bounds on that absence, in the same paragraph rather than in a footnote. A search of what is published is not a search of what runs: four of these six do not publish an engine you can read, and ours is one of the four. Two of the five, Mem0 and Supermemory, also run chat communities that cannot be read from outside; the sweep behind this page does not cover them, and if the answer is in there, this article does not have it. Where this page states an absence, it is naming the code, the repositories and the documentation corpora listed above, and nothing else.

One of those four closed engines says otherwise, and that has to be stated exactly. Supermemory's self-hosting page calls the self-hosted binary "the same memory engine behind the hosted platform", as a single self-contained binary, and links a repository. We downloaded that repository's default branch on 8 September 2026. What is published is apps/docs, apps/mcp, apps/memory-graph-playground, apps/raycast-extension, apps/sdk-playground, apps/web, a shelf of packages including a graph visualization component, and a skills folder. The ingestion, the extraction and the search are not in that tree. We are not going to guess why. We are saying what is there, because a reader who clicks that link should not think this comparison missed it.

The vocabulary is already taken, and a correction we owe

Here is what makes this hard to see from the outside. The words are in use, and they are in use for other mechanisms.

Supermemory documents a preferences memory type and describes its behaviour in three words, in a table whose column heading is Behavior: "Strengthens with repetition". The row above it, for Facts, reads "Persists until updated". The row below it, for Episodes, reads "Decays unless significant". Across that vendor's entire single-file documentation corpus, 822,002 bytes downloaded on 8 September 2026, the word strengthen occurs once and the word repetition occurs once, and both are in that one table row. Nothing anywhere in that corpus says what does the strengthening.

Letta's shipped system prompt tells the model what its references are: "are the synapses of your memory: they should strengthen with use". That is an instruction to a model, not a description of an engine, and the code is where the difference shows. In the downloaded default branch of letta-code, 2,168 files, the identifiers linkStrength, usageCount, accessCount, edgeWeight and the word reinforce occur in zero files, and the word strengthen occurs only in the prompt files themselves. The vendor speaks the language precisely and ships none of the mechanism, which is a more interesting finding than silence would have been.

And now the correction, which is ours. An earlier version of this argument, in the video and in our own drafts, said that those phrases name something computed once and then frozen. That is false, and it is false in a way a reader can catch in one click. Zep's observations are regenerated when new evidence lands: "the existing observation is regenerated with the new evidence merged in", and superseded ones are retired. Supermemory's line sits between "Persists until updated" and "Decays unless significant", which is a table about things that move. These values change. We should not have said they were frozen.

What none of them says is where the change comes from. Line them up and the shape is the same every time. Supermemory's preference strengthens on repetition of a preference. Zep's observation regenerates when new evidence arrives. Mem0 reinforces on retrieval, and the reinforcement lands on a memory's own access history rather than on any edge, opt-in and off by default. Cognee ships weights that move on an explicit rating from the caller, and the caller influence is off unless configured, DEFAULT_FEEDBACK_INFLUENCE defaulting to 0.0 in cognee/base_config.py.

New evidence, a rating, a repetition, a score. Every one of those is a real mechanism. Not one of them is a connection that got stronger because two things came back together.

Where the loop does exist in public

So does anyone do it. One public implementation does, in the narrow sense set out at the end of this section, and it is not a product.

ReinerBRO/HeLa-Mem is the code attached to an ACL 2026 paper. Its retrieve method builds its results, and the last statement it executes before handing them back is a call into a short routine whose own docstring says what it is for: "Strengthen connections between simultaneously retrieved memories." That routine takes the identifiers of the memories that were just retrieved, walks every unordered pair of them, and adds weight to the edge between each pair. The edge saturates: self.edges[u][v] = min(1.0, self.edges[u][v] + weight), so it stops strengthening at 1.0.

That is the whole mechanism. Every product in this comparison has a graph and a retrieval step. What none of the five has published is those last few statements, between having the results and returning them.

Two bounds on that, both of which cut against the neatness of the finding. The first is that this is a research artifact and not a product: checked on 8 September 2026, the repository has no releases, no tags and no licence, and it is code for reproducing a paper. The second is that the field is not one paper deep. Other public artifacts exist in this line, mirkofr/FERNme among them, whose own repository description reads "A lightweight memory engine for AI agents using fuzzy graphs, Hebbian updates, and optional LLM gating." What is narrow is one specific thing, and it is worth saying precisely: one public implementation where the act of retrieving changes the connection.

Our own row, and it is the weakest one here

Which brings it back to us, and to the most uncomfortable position on this page. We describe this mechanism in public, in our own source, and you still cannot check the thing that runs.

The sentence our memory server prints is quoted at the top of this article. Our own library page states the mechanism in our own voice as well: "Its association layer links concepts through weighted edges strengthened by co-activation, with feedback tuning the weights over time". And the comment in our published source names the operation directly, co_activate, linking query concepts to result concepts on use. On use. That is the loop, written down by us.

Our engine is closed. So you can read those sentences and you cannot check any of them, and the work behind them sits in repositories that will not be opened to you. On that axis we are in the same position as Mem0 and Zep, and in a slightly worse one than Supermemory, who at least publish a tree and let you find out what is not in it.

And that is a worse position than the five who say nothing at all, which is the part worth sitting with if you are comparing vendors. Silence promises nothing. We have promised a good deal and shown you description. If you are choosing on evidence rather than on ambition, that runs against us, and it should.

Two things count in the other direction, and both are small. The first is in the same file as the sentence above. The comment around that operation corrects an earlier description we published of our own mechanism, and names both defects in it rather than quietly replacing the sentence: the wrong unit, memories where it should have said concepts, and the wrong trigger. That is not proof that the engine does anything. It is evidence that somebody is checking, in public, where it can be held against us.

The second is smaller and it is a defect rather than a credit. That wrong gloss is still live on one of our own pages. On 8 September 2026 mnemoverse.com/docs/research/building-memory-that-scales still describes the graph as connections between memories that strengthen when memories are retrieved together, which is the unit our own changelog logged as an error. A reader checking us today finds both stories on our own site. We are naming it here rather than waiting for someone else to.

What to ask a vendor

If you are choosing between these, the feature list will not separate them. The word graph is on all six pages and names a different object on each.

Three questions do separate them, and none of them appears on a comparison table.

What are the nodes? Entities, memories, documents, or concepts. This decides what the graph can even be asked, and it is answered differently by every vendor above.

When does anything happen? Only at write time, or is there something at query time as well. If there is, ask whether it is on by default, because for the two that have it, it is not.

When the graph changes, what moved it? New evidence, a rating, a repetition, a score, or a read. This is the one the category is quietest about, and it is the one that decides whether a memory layer learns from being used or only from being written to.

Today, from published material, five of the six never name the last case at all. One of them names it in its own public source and cannot show you the engine that would settle it either way, and that one is us. The only place where a read demonstrably writes the edge is a function in a research repository.

Which is a reason to ask us that question harder than you ask anybody else, and not a reason to buy anything.


Live probes for this article were run on 2026-09-08 with full browser headers, negative results controlled in both directions, and every quotation checked as a contiguous substring of the extracted text of the page named beside it rather than of its raw markup. Vendor punctuation, capitalisation and markdown emphasis are the vendor's own and are preserved inside quotation marks. Surfaces read that day: the four-term code search against five vendor organisations with a positive control, the downloaded default branches of mem0ai/mem0, topoteretes/cognee, topoteretes/cognee-rs, supermemoryai/supermemory, getzep/graphiti, getzep/zep, letta-ai/letta-code and letta-ai/ezra-memory, the single-file documentation corpora of Cognee, Supermemory and Mem0, individual documentation pages at help.getzep.com, docs.cognee.ai, docs.mem0.ai, docs.letta.com and supermemory.ai/docs, and our own published source and pages. Counts from the GitHub code search are counts of files in an index that does not cover every public repository, and they are named as such wherever they appear. Where this article states an absence, it names the surfaces it covered in the same paragraph; the chat communities Mem0 and Supermemory run cannot be read from outside and are not covered, and the page-by-page walk of Zep's documentation was not completed, so nothing here rests on it. Disclosure: this site publishes one of the six systems compared, and the sections about where we sit name our own gaps before anybody else's. No performance, latency, accuracy or benchmark number appears in this article, for any system, ours included.

Edward Izgorodin · Mnemoverse · 2026-09-08

Mnemoverse is a persistent-memory API for AI agents. Free key: console.mnemoverse.com · Plans and limits · Docs: Getting Started