Skip to content
Cortadel™

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

    MarMayJulnow
    ›

    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.
    9,129 unit tests
    xUnit, passing on 2026-10-19. Zero skipped; live-graph integration tests are excluded.
    Opt-in telemetry
    No exporter ships enabled. Console output and OTLP both need an explicit setting; /metrics is a local Prometheus scrape.

    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 / Dedup / DedupService.cs

    Append-only

    mem_7f3a “We ship from the Rotterdam warehouse.” retrieved
    pref_012 “Invoices go out on Fridays.”
    mem_9c1e “Fulfilment moved to Memphis.” retrieved
    pref_318 “Support replies within one business day.”

    both rows persist · ranking decides · the model guesses

    Adjudicated

    input “Fulfilment moved to Memphis.”
    verdict Supersedes
    mem_7f3a “We ship from the Rotterdam warehouse.” validTo = set
    mem_9c1e “Fulfilment moved to Memphis.” returned

    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 / Memory / MemoryWriteService.cs

    1. 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

    2. 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

    3. 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

    4. 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

    5. 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 / Clusters / LouvainCommunityDetector.cs

    1/6
    Memory graph, stage Memories: Every write lands as a memory node, anchored to its user.HAS_ENTITYcosine 0.94DUPLICATE — not storedSUPERSEDESvalidTo setINVALIDATEinvalidAt set · no successormem_01mem_02mem_03mem_04mem_05mem_06mem_07mem_08mem_09mem_10mem_11mem_12ENTITYENTITY

    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 / Search / HybridSearchService.cs

    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.

    From ranked rows to a context pack

    POST /api/v1/context — or search_memory with context=true — compiles the ranked graph into a token-bounded, cited pack: focus entities, live triples, atomic claims with their memory ids, and the alternatives when a predicate has more than one live object. It is read-side and zero-LLM: it never invents a relation. The default budget is 1,200 tokens.

    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 / data / evidence / *.jsonl

    Retrieval recall at R@5, R@10, R@20. Raw RRF: 75.2, 80.8, 89.6 percent. With cross-encoder: 93.0, 95.0, 98.0 percent. 0 25 50 75 100 75.2 93.0 R@5 80.8 95.0 R@10 89.6 98.0 R@20
    With cross-encoder Raw RRF LongMemEval-s · n=500 · per-session · recall %
    0 5s 10s 1,026 ms · raw RRF 10× 10,229 ms · with cross-encoder · CPU, concurrency 3
    the lift and its price — mean query latency, same 500 questions
    The reranker's lift and its latency cost, from the same 500 questions
    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 / Program.cs

    Any Streamable-HTTP MCP client — Claude Code, Cursor, VS Code, Cline, Windsurf

    {
      "cortadel": {
        "type": "http",
        "url": "http://localhost:3001/mcp/claude-code",
        "headers": {
          "Authorization": "Bearer <apiKey>",
          "Project": "${workspaceFolderBasename}"
        }
      }
    }
    
    // There is no /sse sub-path and no user segment. {clientName} becomes
    // the memory's app name; the API key identifies the user.

    The REST surface is 259 route attributes across 49 route-bearing controllers; the MCP surface is two tools; the dashboard, the API and the OpenAPI UI are the same process on the same port.

    Two tools over MCP

    answers needs its flag

    Write add_memories

    • plain text

      Intent-classified store: remember, invalidate, delete-entity, touch, resolve.

    • "role: content" turn

      Turns collect into a conversation and are distilled into atomic facts.

    • image URL, data: URI or base64 needs Multimodal:Enabled

      Queued for async capture; answers “not enabled” until the flag is set.

    Read search_memory

    • hybrid recall

      BM25 and vector arms fused with RRF; rerank, graph and session ride behind it.

    • context=true

      Compiles the ranked graph into a token-budgeted, cited entity/triple/claim pack instead of rows; read-side and zero-LLM.

    • browse and tiers

      Omit the query to browse. `headline` scans, then `ids` expands — no second search.

    • skill:<id> expand needs CORTADEL_SKILL_ARM

      Procedural queries inline the top skill; `ids` expands its full procedure.

    Tools only — no MCP resources and no prompts. Conversation distillation, media capture, skill expansion and context compilation are channels of these two, not tools of their own. context=true works on a stock deploy; media and skills need their flags. Entity reconciliation is REST and dashboard only — its LLM judge auto-merges a duplicate pair above 0.95 and queues everything below it for review. 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 only partly do.

    Receipt docs / competitive-analysis-2026-07-27.md

    Capability comparison of Cortadel against Cognee, Graphiti, mem0 and Supermemory, from the repository's 2026-07 survey, with Cortadel's hosted-console row refreshed in 2026-10. Zep is represented by Graphiti, its memory engine.
    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 ◐ ● ○ ● ● a hosted console exists at app.cortadel.ai, but no signup, metering or billing — cognee, mem0 and Supermemory all host. This row stays.

    ● yes ◐ partial ○ no

    competitor cells: 2026-07 survey · Cortadel hosted-console cell: 2026-10 — 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.

    • A graph store

      One FalkorDB or Memgraph container, on your hardware. FalkorDB is what the benchmarks on this page used and the one path with automated coverage; Memgraph is supported but unproven.

    • Docker — no clone, no build

      The server ships as a prebuilt image with the API, the retrieval pipeline and the 543 MiB reranker model compiled in. You pull and run it; there is no server source to build.

    Then three commands

    1. 01 Start a graph store. FalkorDB is what every benchmark on this page used.

    2. 02 Point at your own endpoints — the embedding and LLM endpoints are both mandatory and both can be local. The docs list every key.

    3. 03 Pull and run the image — API, MCP, Swagger and dashboard on port 3001.

    That brings the dashboard, the REST API, Swagger and the MCP endpoint up together on port 3001, with FalkorDB alongside it.

    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
    Free to self-host for personal and development use; a commercial licence covers business use. The SDK and the docs are Apache-2.0; the memory server is proprietary. Community uses FalkorDB or Memgraph under their own licences.
    Per-memory or per-token fee
    Nothing. The self-hosted image has no usage 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.

    What it costs

    Free on your hardware. Metered on ours.

    A price is the one number on this page that cannot be recomputed from an artifact — it is a decision, not a measurement. So every card states whether it can take your money today, and the command in the gutter is how you check it: grep the server for billing code and nothing comes back.

    Receipt docs / pricing-2026-08-17.md

    1. Available

      Free

      $0

      forever, on your hardware

      The Community desktop app or container, running its memory server on your machine with FalkorDB or Memgraph and your own model endpoints.

      • Unlimited memories, unlimited searches, and no usage meter in the image
      • Windows app: supervises the engine as a child process, provisions its own graph store inside WSL2, auth on by default
      • Container: docker run ghcr.io/cortadel/cortadel — anonymous pull, no account, no key
      • Bi-temporal history, reversible entity merge, hybrid recall and the local cross-encoder — nothing is withheld from the free tier
      • SDKs for .NET, TypeScript and Python, Apache-2.0, with the docs

      Ceiling Your own hardware, and business use needs a commercial licence. The desktop build binds 127.0.0.1 and nothing else, so it cannot serve a teammate, a phone or a CI job — that is what the container is for.

      The desktop installer is beta and unsigned: SmartScreen warns on a stranger’s machine until the certificate lands. It is distributed from downloads.cortadel.ai/desktop/latest, and the download page says whether that channel is serving a build — it offers no installer unless its own build-time read of the channel returned one. Either way you supply your own Ollama or LM Studio endpoints; no model ships inside it. The memory server is closed source and ships as an obfuscated binary; the SDKs and the docs are Apache-2.0. Community uses FalkorDB or Memgraph under their own licences; CortadelDb is reserved for private Azure SaaS.

    2. Coming soon

      Cloud

      from $12

      per month

      Metered on the memories you keep, not the tokens spent writing them. Search, rerank, traversal and history are free at any volume.

      • Free 10,000 live memories — two to three times the working ceiling of any free tier in the survey, and reads are not rationed
      • Solo $12 for 100,000 live memories · Team $79 for 1,000,000 and 10 seats
      • Overage $0.40 per 1,000 live memory-months, published at every tier rather than quoted on a call
      • Bring your own model endpoint and ingest never touches our inference, which is why the rate can sit under the category’s
      • A spend cap on by default, set to the price of the next tier up

      Ceiling The spend cap. Reaching it says you have outgrown the tier; it is not a surprise invoice, and reads keep working when writes stop.

      The paid tier is not built: no signup flow, metering, quota or billing. The hosted console at app.cortadel.ai is live and serves per-app keys, but the command in the gutter still returns nothing.

    3. Coming soon

      Enterprise

      Contact us

      self-hosted, your cloud, or ours

      A signed licence key, verified offline, unlocking single sign-on and scale on whichever infrastructure your procurement will sign for.

      • One contract across three delivery shapes — self-hosted, your cloud, or a dedicated instance we run
      • A lapsed licence deactivates SSO and keeps the engine serving read-write — your memory is never held hostage
      • Export un-gated in both directions, into an engine that is free to run
      • An SLA carrying service credits is a separate priced line, because a one-person team cannot honestly bundle one into a base price

      Ceiling Declared instances and regions, agreed in the contract rather than discovered in an invoice.

      Not built yet. There is no OIDC or SAML, and only a single admin role gates operator routes — no multi-role RBAC or SLA automation to sell.

    Why the meter counts memories and not tokens

    Storing one memory takes six model calls — intent, dedup verdict, extraction, a gleaning pass, BM25 synonyms and a gist — or about 4,600 tokens for a 150-word fact. Billing those would run 3.3× faster than cognee’s meter for the same stored fact, so a headline rate below theirs would still produce a larger invoice. It would also price the write path’s remaining optimisations as cuts to our own revenue.

    Counting what you keep inverts both. The bill stops tracking how verbose your agent is, and the meter becomes the one thing in this market you can audit yourself: it is a single Cypher query, and on a self-hosted instance it returns the same number ours does.

    It is also why reads are free rather than discounted. Every other product in this category meters retrieval — mem0 ten times more scarcely than writes, so the read cap binds long before the write cap does. Recall here costs no model tokens at all, because the read path makes no model calls.

    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 / data / claims.json

    1. The server is closed source

      Cortadel Community ships its memory server (API, retrieval pipeline and reranker) as obfuscated binaries — there is no server source to read or build. The SDK, the docs and every benchmark row on this page are open and checkable. Community Docker and desktop use FalkorDB or Memgraph. CortadelDb is reserved for the private Azure SaaS version. The server itself is a product you run, not source you audit — you pull the Community image from ghcr.io/cortadel/cortadel, you do not build it.

    2. Auth is off by default

      Auth:Secret ships empty, which disables the key gate entirely: every REST and MCP route answers unauthenticated. CORS is closed by default — no origins are configured, so a browser on another origin gets no CORS headers. Set a secret and mint a key before this is reachable from anywhere but localhost.

    3. The server is a binary you run

      The server is distributed as a prebuilt container, never as source. The Windows desktop app bundles that binary and the hosted console runs it for you; neither is a source tree you can patch. Configuration is environment variables. Behaviour you cannot reach with a flag, you cannot reach.

    4. Two MCP channels are still inert

      The MCP surface is two tools, and both answer — but the image channel of add_memories reports “multimodal capture is not enabled” and the skill tier of search_memory returns nothing unless Multimodal:Enabled and CORTADEL_SKILL_ARM are set. Context compilation is a third read channel and needs no flag: `context=true` works on a stock deploy. Several ranking behaviours — decay, reinforcement, type weighting, the community arm — are default-off, so a stock deploy does less than the architecture allows.

    5. Memgraph is supported, not proven

      Memgraph is the code default and FalkorDB is what the compose file runs, what every benchmark used, and the path with the default automated coverage. Memgraph integration tests exist but skip unless a live Memgraph is configured, so treat it as supported-but-unproven.

    6. No SSO and no RBAC beyond admin

      An admin role gates operator routes when auth is on. There is no OIDC or SAML and no multi-role RBAC beyond that role. The Enterprise card is a statement of intent, not a shippable tier.

    23 retracted statements are held in the page's claim ledger and the build fails if any reappears in the rendered HTML. 9 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 / 04-claim-ledger.md

    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.

    Both. search_memory returns ranked rows by default; context=true — or POST /api/v1/context — compiles a token-bounded pack of focus entities, live triples and atomic claims, each carrying its memory id, plus the alternatives when a predicate has more than one live object. It is read-side and zero-LLM: it never generates a relation that is not stored, and it stops early when the evidence is already covered.

    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. Telemetry is opt-in: no exporter ships enabled, console output needs OTEL_CONSOLE_EXPORTER=1, OTLP tracing and metrics need an explicit endpoint, and /metrics is a Prometheus scrape on the same port.

    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.

    The paid Cloud tier on the card is not built: there is no signup flow, metering, quota or billing, so nothing could bill you even if you asked it to. The hosted console at app.cortadel.ai is live, serves per-app keys and states its own boundaries — deployment shape, engine, capacity model, regions, and a published HA denial (single-node; HA is out of scope) — at GET /api/v1/hosting, which is key-gated like every /api/v1 route. Its tier caps are unpublished, and the endpoint says so rather than inventing numbers. A price is published for the tier anyway, because the intended number and its state are more useful than a “coming soon” that commits to nothing.

    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.

    Now. The Community image is public — docker run ghcr.io/cortadel/cortadel pulls it and brings the API, MCP, Swagger and dashboard up on port 3001, and a one-command docker compose that bundles FalkorDB and local models is in the public repo. Community Docker and desktop use FalkorDB or Memgraph; CortadelDb stays in the private Azure SaaS version, and Enterprise Docker remains private. The SDK and docs are Apache-2.0 at github.com/cortadel/cortadel and docs.cortadel.ai. The memory server stays closed source: you run its obfuscated executable, you do not build it.

    Point an agent at it.

    One container, two model endpoints, and an MCP URL your client already knows how to speak.