Memory that changes its mind.
Most memory layers append. Tell one that something changed and you get both answers back. Cortadel adjudicates the collision — the old fact gets an end date, the new one opens, and recall returns a single answer.
Self-hosted, graph-native, bi-temporal. Exposed over MCP and REST from one process on one port.
Write pipeline · live trace
stored 2026-03-02
“We ship from the Rotterdam warehouse.”
Validity
ships from Rotterdam
A fact is already in the graph.
- 93.0% R@5
- LongMemEval-s, n=500, with the cross-encoder. 75.2% without it.
- 1,671 tests
- xUnit, passing on 2026-08-05. Zero skipped.
- Console only
- The sole telemetry exporter in the project. No OTLP package, no endpoint.
The problem
Append-only memory keeps both answers.
A vector store has no opinion about contradiction. Six months after “we ship from Rotterdam”, “we moved to Memphis” is simply a second row — and whichever one ranks higher is the one your agent believes.
Receipt Core /
Append-only
both rows persist · ranking decides · the model guesses
Adjudicated
one answer returned · history kept
The mechanics behind the right panel: writes are classified STORE, INVALIDATE, DELETE_ENTITY, TOUCH or RESOLVE before anything lands, collisions above 0.85 cosine are adjudicated by an LLM, and a SUPERSEDES verdict closes the old fact with a timestamp. Nothing here is destroyed — a superseded memory keeps its interval and stays queryable. The one genuinely destructive path is deleting an entity outright, which is a hard delete and is meant to be.
Write path
Five decisions before anything is stored.
A write is not an insert. It is classified, checked for collisions, adjudicated, and only then persisted — which is why “forget the Rotterdam thing” mutates the graph instead of adding a note to it.
Receipt Core /
- 01
Classify
An LLM decides what the write means first — STORE, INVALIDATE, DELETE_ENTITY, TOUCH or RESOLVE. “That’s resolved” mutates; it doesn’t append.
Core/Mcp/IntentClassifier.cs
- 02
Collide
A vector lookup pulls near-duplicates at 0.85 cosine or better. Below the gate, the write is simply stored.
Core/Dedup/DedupService.cs
- 03
Adjudicate
The verdict: DUPLICATE, SUPERSEDES or DIFFERENT. A negation guard keeps “I like X” and “I don’t like X” apart.
Core/Dedup/DedupService.cs
- 04
Write
One bulk UNWIND insert. SUPERSEDES sets validTo on the old memory instead of updating in place — history stays reconstructible.
Core/Memory/MemoryWriteService.cs
- 05
Extract
Entities, relations, scope and cognitive type — extracted off the request path on a 500-slot bounded channel.
Core/Entities/ExtractionQueue.cs
Three call sites reach this pipeline: the two MCP write tools and the single-memory REST endpoint. The REST bulk endpoint takes a different, deliberately non-atomic path — worth knowing before you point a migration at it.
The substrate
One graph holds all of it.
Memories, the entities extracted from them, the communities they cluster into — and the three things a colliding or retiring fact can become: deduplicated away, superseded into a successor, or invalidated with none. Step through it.
Receipt Core /
Every write lands as a memory node, anchored to its user.
Entity identity is (userId, normalised name) — type is metadata, not identity — and every read is anchored to the user node, which is what makes one server hold many people’s memory without them bleeding together.
Recall
Two arms, fused, then re-read by a cross-encoder.
Text and vectors disagree about relevance often enough that picking one is a choice you will regret. Both run, their rankings are fused, and a local cross-encoder re-reads the top of the list.
Receipt Core /
Two arms
A BM25 text arm and a vector arm run against the same query, which is embedded exactly once and threaded to both.
Fuse
Reciprocal Rank Fusion at k = 60. On the FalkorDB path BM25 is sigmoid-normalised first, so an unbounded text score cannot swamp the vector arm.
Overfetch
The vector arm takes 5,000 candidates and filters by user afterwards, because neither graph engine has a filtered ANN index. This is symmetric across both providers.
Re-read
A local bge-reranker-v2-m3 cross-encoder re-scores the top candidates in-process, on CPU. No network call, no third-party API.
What live recall returns
Reads filter on invalidAt IS NULL, so a superseded fact is
never in the answer. It is still in the graph, still timestamped and still queryable
through history — which is what separates supersession from deletion. Results older than
90 days come back labelled stale; labelled, not filtered — nothing is dropped for age.
A gate worth knowing about
The graph and session arms only contribute when the cross-encoder is on. Their additions
rank below the fusion floor and surface at rerank. Setting the REST session flag without
also setting rerank does nothing at all.
Evidence
The numbers, and what they cost.
Every figure below is recomputed from row-level result data on each build, and the artifacts are hash-pinned. If the rows and the page disagree, the build fails rather than the page winning.
Receipt src /
| R@5 | R@20 | MRR | mean latency | |
|---|---|---|---|---|
| Raw RRF | 75.2% | 89.6% | 0.652 | 1,026 ms |
| With cross-encoder | 93.0% | 98.0% | 0.872 | 10,229 ms |
| Difference | +17.8 points | +8.4 | +0.220 | 10× |
That last row is the whole trade: the cross-encoder buys +17.8 points of R@5 and costs 10× the query latency on CPU. Both halves ship together, or neither would be honest.
- R@5
- 92.3%
- R@10
- 95.3%
- R@20
- 97.6%
- MRR
- 0.839
1,986 QA pairs · per-session · FalkorDB · 2026-07-25
Measured over 272 memories across ten users — roughly twenty-seven per person, not a lifetime of them. The run used the cross-encoder with query expansion, which is not any surface's default configuration.
The same run produced answer scores of F1 51.2 and LLM-judge 51.5. Those are not comparable to published competitor figures: ours were generated and judged by a local model, theirs by GPT-4o-class models. Here for completeness, not for comparison.
What this page will not claim
- A leaderboard place against mem0, Zep, cognee or Supermemory.
- Our headline figures are retrieval recall — did the right evidence come back. Several published competitor figures are answer accuracy. Putting them in one table would be a category error, not a comparison.
- A latency, throughput or memory-footprint number.
- No production measurement exists at any size. The only figures in the repository are harness runs that move tenfold with configuration alone, and one of them is on this page precisely so you can see that.
- That one graph engine is faster than the other.
- An earlier “35× faster” figure was retracted inside this repository after an audit found no artifact behind it. The corrected measurements land near parity once two of our own bugs are fixed.
Surface
One process, one port, three front doors.
The API, the OpenAPI UI, the MCP endpoint and the dashboard are the same ASP.NET Core process. There is no sidecar, no second container for the UI, and no separate MCP server to keep in sync.
Receipt Api /
Any Streamable-HTTP MCP client — Claude Code, Cursor, VS Code, Cline, Windsurf
{
"cortadel": {
"type": "http",
"url": "http://localhost:3001/mcp/claude-code/<userId>",
"headers": {
"Authorization": "Bearer <apiKey>",
"Project": "${workspaceFolderBasename}"
}
}
}
// There is no /sse sub-path. {clientName} becomes the memory's app
// name; {userId} must match the key's user or the server returns 403.Eight tools over MCP
answers needs its flag
Write
-
add_memoriesStore memories. Intent-classified, deduped, extraction queued.
-
add_conversationDistil a multi-turn conversation into atomic facts, then store those.
-
add_medianeeds Multimodal:EnabledCapture an image as searchable memories.
Read
-
search_memoryHybrid recall. Omit the query to browse chronologically; scan headlines, then expand ids without re-searching.
-
get_skillneeds MEMFORGE_SKILL_ARMExpand a distilled skill’s procedure.
Reconcile
-
reconcile_memoriesQueue duplicate-entity merge suggestions; auto-merge above 0.95.
-
reconcile_statusProgress of a reconcile run.
-
list_merge_suggestionsQueued suggestions with the judge’s reasoning, for approval.
Tools only — no MCP resources and no prompts. A Project
header scopes a connection so memories from different repositories stay separate under one
user, which pairs with an editor variable like
${workspaceFolderBasename}.
Against the field
Where it wins, and where it loses.
Capabilities, not benchmark scores — those are measured on different axes and the numbers are contested. Graphiti, which is also Zep's engine, matches this on most rows. The last row is one most of the field wins and we don't.
Receipt docs /
| Capability | Cortadel | Cognee | Graphiti | mem0 | Supermemory | Detail |
|---|---|---|---|---|---|---|
| Graph-native storage | ● | ● | ● | ◐ | ● | cognee runs a triple store: SQL + vector + graph |
| Bi-temporal — valid and invalid time | ● | ○ | ● | ○ | ◐ | cognee keeps provenance only; Supermemory has an isLatest flag |
| Contradiction detection | ● | ○ | ◐ | ○ | ● | mem0 is add-only |
| Supersede rather than delete | ● | ◐ | ● | ◐ | ● | cognee’s forget deletes; here the old version stays queryable |
| Entity resolution | ● | ◐ | ● | ◐ | ● | mem0 is hash dedup; cognee is memify consolidation |
| Reversible merge — undo, and never re-auto-merge | ● | ○ | ○ | ○ | ○ | the only row nothing else in the survey offers |
| Hybrid retrieval with RRF | ● | ◐ | ● | ◐ | ● | |
| Cross-encoder rerank | ● | ○ | ● | ○ | ● | local ONNX here — in-process, no rerank API call |
| Community detection | ● | ◐ | ● | ○ | ◐ | Louvain here; hierarchical summaries in cognee |
| Managed cloud | ○ | ● | ○ | ● | ● | not offered — cognee, mem0 and Supermemory all host. This row stays. |
● yes ◐ partial ○ no
as of the 2026-07 survey — capabilities move; re-verify before citing
Run it
What it actually takes.
Prerequisites first, because two of them will stop you and a quickstart that hides them is just a slower way to find out.
Receipt docker-compose.yml
Before anything runs
-
An embedding endpoint and an LLM endpoint
Cortadel ships no embedding model. Both are mandatory and both can be local — Ollama and LM Studio are the tested paths, Azure is the third provider. Embeddings are 1024-dimensional by default, and startup fails loudly on a width mismatch rather than corrupting the index.
-
The reranker model, fetched separately
A 543 MiB int8 ONNX build of bge-reranker-v2-m3 is git-ignored and shipped as a release asset. The Docker build fails on a missing COPY until you fetch it.
-
Docker with Compose
The compose file brings up FalkorDB and the API together. Nothing else is required.
Then three commands
-
01 Fetch the reranker model (543 MiB, once).
-
02 Point at your own endpoints. The shipped default is a LAN address that will not exist on your network.
-
03 Bring up FalkorDB and the API on port 3001.
That brings the dashboard, the REST API, Swagger and the MCP endpoint up together on port 3001, with FalkorDB alongside it. None of this works for you today — the repository and the model asset are not public yet.
Where it all runs
Everything below the dashed line is yours. The model endpoints are the one place traffic can leave — and whether it does is your configuration, not ours.
What it costs to run
- Licence
- Nothing. Apache 2.0.
- Per-memory or per-token fee
- Nothing. There is no meter and nobody to bill you.
- Graph store
- One FalkorDB or Memgraph container, on your hardware.
- Embeddings and LLM calls
- Whatever your endpoint charges. Zero if it is a local model on your own GPU.
- Reranking
- 543 MiB of disk and CPU per query. It is the reason recall is 93.0% and the reason queries are not fast.
Limits
What’s not true yet.
This is the section other memory products give to customer logos. We have none, and this is the better trade anyway: everything below costs us something, which is what makes the rest of the page cheaper to believe.
Receipt src /
-
The source is not public yet
The repository returns 404 to anyone who is not a collaborator, and the LICENSE file is present on disk but not committed. Until both change, every claim on this page is checkable only by someone who already has access — which is the weakest form of the thing this page is arguing for.
-
Auth is off by default
Auth:Secret ships empty, which disables the key gate entirely: every REST and MCP route answers unauthenticated, and CORS allows any origin. Set a secret and mint a key before this is reachable from anywhere but localhost.
-
A fresh clone cannot build
The Dockerfile copies a 543 MiB model that is git-ignored and distributed separately. Until that download succeeds, `docker compose up --build` fails on a missing file rather than on anything informative.
-
Two of the eight MCP tools are inert
get_skill returns null and add_media returns disabled unless MEMFORGE_SKILL_ARM and Multimodal:Enabled are set. Several ranking behaviours — decay, reinforcement, type weighting, the community arm — are also default-off, so a stock deploy does less than the architecture allows.
-
Only one graph path is verified
Memgraph is the code default and FalkorDB is what the compose file runs and what every benchmark used. The Memgraph path has no automated coverage: the unit tests mock the graph client and the integration tests are FalkorDB-only.
-
Some documented behaviour is not implemented
The README advertises relationship weights: the extraction worker parses one out of the LLM response and then never persists it. It also advertises automatic retry for failed entity extraction; nothing re-enqueues a failed job, and recovery is an explicit API call. Both sit on this page’s kill list rather than on this page.
Fourteen retracted statements are held in the page's claim ledger and the build fails if any reappears in the rendered HTML. Six qualifying phrases are required the same way — the flattering number cannot ship without the sentence that limits it.
Questions
The ones that decide an evaluation.
Receipt spec /
A timestamp records when a row was written. It does not decide anything. The difference is the verdict step: a colliding write is adjudicated, and a SUPERSEDES verdict closes the earlier fact so recall stops returning it. You can get most of the storage shape from a vector store and a validAt column; what you cannot get is something that decides which of two contradicting statements is currently true.
The graph does not — it runs in your Docker network and there is no Cortadel service anywhere. What does leave is whatever you send to the embedding and LLM endpoints you configure, and the shipped compose defaults point at an address on a LAN that is not yours, so set them. Both can be local models. The only telemetry exporter compiled into the API is OpenTelemetry.Exporter.Console, which writes to stdout.
On its own, not much, which is why the corpus size is printed next to it. The number that carries weight is the LongMemEval one, because it is a controlled before-and-after on the same 500 questions with one stage switched off: 75.2% to 93.0%. That measures our reranking, not the dataset.
Because the honest one is unflattering and configuration-dependent. The reranked LongMemEval run averages 10,229 ms per query on CPU against 1,026 ms with the reranker off — a tenfold difference from one setting. Quoting a p50 from a tuned run without that context would be the same trick this project already retracted once.
It is one config key and Memgraph is what the code defaults to. But the FalkorDB path is the one with automated verification and the one every benchmark on this page used, so treat Memgraph as supported-but-unproven rather than an equal option.
No, and none is being built. There is no account, no console and no billing. That is the trade: you run a graph container and two model endpoints yourself, and in exchange there is no third party in the path and no meter.
It stays in the graph with a validTo timestamp. Live reads filter on invalidAt IS NULL so it stops appearing in results, but it is still queryable, still attributable and still part of the history. Entity deletion via the DELETE_ENTITY intent is the one genuinely destructive operation, and it is a DETACH DELETE.
Not yet, honestly. The repository is private, the licence file is uncommitted, and the reranker model is behind a release asset nobody outside can fetch. Everything on this page describes code that exists and was measured; none of it is code you can currently run.
Nothing to sign up for.
There is no waitlist, no newsletter and no demo to book. The source goes public or it does not; until then the most useful thing here is the part that says what does not work.