kepta
Allows importing and exporting Obsidian vaults, converting Markdown notes with frontmatter and [[wiki links]] into KEPTA's knowledge graph.
KEPTA — the memory for AI systems
Your AI forgets everything. Every chat starts from zero.
KEPTA is the fix: one local, encrypted memory that every AI reads and writes — Claude, Cursor, Gemini CLI, Cline, any MCP client, and scripts over HTTP. No cloud. No account. One file you own.
Everything an AI system learns goes into an encrypted knowledge base on your own computer — and every AI recalls it in milliseconds, however long the context grows. Documents, decisions, client knowledge, whole chat histories: remembered, linked, contradicted, superseded — never lost.
This repository is the source-available core (BUSL-1.1 — every line public, converts to AGPL-3.0-or-later four years after each release): the memory engine, the MCP server, the HTTP API — and the Python client, which is MIT so any Python project can embed it. Everything that makes the memory smart lives in this free engine. There are no tiers inside the memory — nothing is held back to sell anything.
Questions, security feedback or benchmark critique? → Discussions
flowchart LR
subgraph Apps["Your AI apps — every client, one memory"]
direction TB
A["Claude Desktop · Claude Code"]
B["Cursor · Gemini CLI<br/>Cline · Codex · Zed"]
C["Your scripts"]
end
subgraph KEPTA["KEPTA — this repository"]
direction TB
M["MCP server<br/>8 tools · stdio + Streamable HTTP"]
G["HTTP API · 32 routes<br/>Python client · CLI"]
E["Memory engine<br/>hybrid retrieval: FTS5 + vectors + graph<br/>RRF fusion → local rerank"]
end
D[("One encrypted file<br/>SQLCipher 4 · AES-256<br/>~/.kepta/kepta.db")]
K["OS keychain<br/>the key never leaves"]
A --> M
B --> M
C --> G
G --> E
M --> E
E --> D
D -.-> KRelated MCP server: widemem-ai
⚡ One command, every AI client
Needs Node 22.13 or newer (package.json → engines) on the machine you install on; npm has prebuilt SQLite binaries for Node 22, 24 and 26.
npx -y kepta-mcp demo # six scenes in a throwaway database — nothing of yours is touched
npx -y kepta-mcp setup # connects Claude Desktop, Claude Code, Cursor, Windsurf, VS Code, Gemini CLI, Cline and Roo Code
npx -y kepta-mcp remember "Client Miller signs renewals in December." # write a note from the terminal, no AI client needed
npx -y kepta-mcp recall "who signs renewals" # read it back: 1 match — ranked by relevance
npx -y kepta-mcp stats # size, encryption state, top tags
npx -y kepta-mcp timeline "miller" # what was true, and when
npx -y kepta-mcp import chatgpt <export-folder> # bring your ChatGPT history with you (export → extract → import)What those two lines answer, verbatim — kepta-mcp 3.2.0 from the registry on 10.10.2026, a
throwaway HOME, an empty memory:
$ npx -y kepta-mcp remember "Client Miller signs renewals in December."
✓ remembered — Client Miller signs renewals in December.
type: semantic · id: k-mv2mghs9-fdf75c17
$ npx -y kepta-mcp recall "who signs renewals"
1 match — ranked by relevance
1. Client Miller signs renewals in December. · semantic · today
Client Miller signs renewals in December.
#rememberA note is one thing your memory holds — remember writes one by hand, your AI client writes the same kind through the memory_save tool. Then tell your AI something worth keeping — and watch it arrive. Details in the Quick start.
🖥️ Or download the desktop app
The core has no window of its own: its interface is your AI client. If you want a window, that is the free desktop app — same engine, same encrypted file, same key — and it is downloadable from the releases of this repository. For teams and organizations there is KEPTA Enterprise by agreement. These three sentences are the whole answer; every document here says the same.
The app brings the memory browser, hybrid search with retrieval provenance, the knowledge graph, time travel, dossiers and a chat that reads the memory. Download the DMG for your Mac from the latest release (Apple Silicon or Intel) and drag it into Applications. The build is ad-hoc signed and not notarised, so macOS asks a single time. Two ways through, and both work on every macOS version: xattr -dr com.apple.quarantine /Applications/KEPTA.app, or System Settings → Privacy & Security → the KEPTA message → Open Anyway. Once, then never again. Older guides say right-click → Open; Apple removed that route in macOS 15 and it does nothing on current systems. (What runs from this repository has no window: npm run dev starts the HTTP API and its POST /mcp — a service on your machine, not an interface.)
The app is free, like everything in this repository. KEPTA Enterprise (team and tenant memory, audit chain, human review, point-in-time recovery) is licensed by agreement — write to me on LinkedIn.
🧠 One engine. Free. For every AI.
KEPTA is not a feature inside one app — it is a memory layer that outlives every model, every app and every context window. Three doors lead into the same encrypted file:
Door | What uses it |
MCP — 8 tools, stdio (npm binary and container) + Streamable HTTP (this repository, run from source) | Every MCP client: Claude, Cursor, Gemini CLI, Cline, Codex, Zed, your own agents |
HTTP API — 32 routes, loopback only | Scripts, cron jobs, agent teams, automations |
Python — | Any Python project, embedded |
Free means free: €0, forever — no account, no licence key, no daily limit, no tier inside the memory, nothing locked to sell you later. The best memory engine we can build is the free one; that is the point. This sentence is about money, not about capacity. On one machine the local API stops accepting new notes once KEPTA_MAX_ACTIVE notes are active (5000 by default, server.ts → maxActiveMemories()): a brake on a runaway agent writing your memory full, not a paywall — raise it with that variable and nothing else changes. Nothing in the free core is a locked feature; see SECURITY.md for every such screw.
Teams and organizations — shared team memory, central policies, MDM, organisation-wide audit — get KEPTA Enterprise by request. Coordination is the only thing we ever charge for; memory quality never is. → write to me on LinkedIn
⚡ Quick start
Connect your AI apps
npx -y kepta-mcp setupFinds Claude Desktop, Claude Code, Cursor, Windsurf, VS Code, Gemini CLI, Cline and Roo Code on your computer and adds KEPTA to them. It asks per app (--yes connects all of them), keeps a backup of every file it changes and leaves every other entry alone. Zed, Codex CLI and Continue get the exact snippet to paste (their config formats move too fast to write from outside). Any other MCP client:
{
"mcpServers": {
"kepta": {
"command": "npx",
"args": ["-y", "kepta-mcp"]
}
}
}8 tools: memory_search, memory_save, memory_update, memory_delete, memory_list, memory_graph, memory_consolidate, memory_forget.
HTTP API
git clone https://github.com/DamianTodorovic/kepta.git
cd kepta && npm install && npm run dev
# → http://127.0.0.1:3000
# save a note
curl -s localhost:3000/api/memory -H 'Content-Type: application/json' \
-d '{"title":"Client Miller","content":"Billed quarterly.","tags":["client"]}'
# search
curl -s "localhost:3000/api/memories/search?q=how+does+miller+pay"Docker (MCP server only)
Build the image from a clean clone, before any npm install — a node_modules built for macOS that ends up in the build context breaks the Linux build (ERR_DLOPEN_FAILED). Measured on this state, 12.10.2026 (docs/belege/r1/kontext-groesse-2128.log.txt): the same folder carries a 180 MB build context, 169 MB of it node_modules, where the tracked files alone are 4 MB. (.dockerignore now keeps node_modules and build artefacts out of the context; until it reaches your checkout, a clean clone is the safe route.)
git clone https://github.com/DamianTodorovic/kepta.git && cd kepta
docker build -t kepta-mcp .
# A container has no keychain, so the database key is not optional here: it has
# to come from the environment. Generate it ONCE and keep it — it is the only way
# back into the volume.
KEPTA_DB_KEY=$(openssl rand -hex 32)
docker run --rm -i -e KEPTA_DB_KEY="$KEPTA_DB_KEY" -v kepta-data:/data kepta-mcpNo key, no file. Pass a real value: 64 hexadecimal characters, openssl rand -hex 32, generated once and stored where you keep your secrets. A container cannot fall back to a keychain, so without KEPTA_DB_KEY a new knowledge base is not created — KEPTA stops with a non-zero exit and says what to do instead. The same value has to come back on every later start: a fresh openssl rand -hex 32 the second time means a key that does not fit the volume, and that also stops with nothing changed. The only way to a plaintext database is the switch that says so out loud, KEPTA_ENCRYPTION=off.
One transitional sentence for whoever reads this against an older build: before this rule was in the code, an unusable value (a placeholder copied out of a guide, too short, not hex) did not fail — KEPTA printed one line, created the database in plaintext and exited 0. If your kepta.db starts with the bytes SQLite format 3, that is what happened, and the file is readable by anyone who can read it. Check the version, then re-key or replace the volume. SECURITY.md has the whole rule.
CI, servers and every other headless start
A CI runner has no keychain either, so the key comes in as a secret — generated once, not per job, or every run starts a fresh empty memory:
# GitHub Actions, one step. The secret holds the 64 hex characters from `openssl rand -hex 32`.
- run: KEPTA_DB_KEY="${{ secrets.KEPTA_DB_KEY }}" npx -y kepta-mcp statsThree traps, all measured on this repository:
An empty value is not "no key".
KEPTA_DB_KEY=""is dropped and the run goes looking in the keychain anyway — on your own machine that means it reads the keychain entry you did not mean to touch. Check that the variable is non-empty before starting, and do not let a templating mistake turn it into"".A placeholder is a value too. Copying a
<64-hex>-style token out of any guide is the same case as an empty one.A key in a logged command line is a published key. Pass it through the environment file or the runner's secret mechanism, not through a shell history that CI prints.
If the run cannot reach a key at all, the correct outcome is the one above: no database, a non-zero exit, a sentence naming KEPTA_DB_KEY. A green CI that built a plaintext knowledge base is the failure this rule closes. The full variable list, with defaults and where each is read: docs/ENVIRONMENT.md.
✨ What's inside
🔍 Hybrid search | BM25 full text + vector similarity (Ollama) + knowledge graph, fused with RRF, with local reranking. The three legs are not three equal forces — the numbers this repository measures are under the diagram below |
🔐 Encrypted at rest | SQLCipher 4: AES-256 and an HMAC-SHA512 over every page, the WAL included; the key lives in the OS keychain (macOS Keychain, Windows DPAPI, Linux Secret Service) |
🕐 Time travel | Validity windows ( |
♻️ Superseded, not contradicted | New facts replace old ones ( |
🔗 MCP first | 8 tools, one code path for the API and MCP — agents get the same quality as any client |
📄 File import | PDF (pdf.js with character maps), Markdown with |
📊 Eval |
|
How a query finds its answer
flowchart TD
Q(["Your query"]) --> A["FTS5 · BM25<br/>lexical search"]
Q --> B["Vector KNN<br/>local embeddings (Ollama)"]
Q --> C["Entity match<br/>knowledge graph"]
A --> R["Reciprocal Rank Fusion<br/>k = 60"]
B --> R
C --> R
R --> BO["Recency + confidence boost"]
BO --> T{"Temporal state?"}
T -->|expired| X5["score × 0.5"]
T -->|superseded| X4["score × 0.4"]
T -->|valid| K1["unchanged"]
X5 & X4 & K1 --> R2["Local reranker<br/>term coverage · phrases · title · tags<br/>max boost +0.25 — no network"]
R2 --> OUT(["Top-k, ranked by relevance"])What each leg is worth on its own — npx tsx scripts/ablation.ts on this state, 58 notes / 45 queries, local Ollama, measured in three identical runs on 12.10.2026 (docs/belege/r1/ablation-2326.log.txt): Hit@1 with full text alone 71 %, full text + graph 71 % (+0 percentage points), full text + vectors 78 %, all three 76 %; the graph leg alone reaches 11 %. Read that honestly: full text carries the answer, the vector leg is what adds the seven points and is the only leg that needs a model, and on this corpus the graph leg does not raise the ranking — laid on top of the pair it costs two points. It is there for entities and for GET /api/graph, not for the win. Claiming three equal strengths would sell a number the repository's own script contradicts.
🖥️ HTTP API (32 route paths)
32 distinct paths; counting each HTTP verb on its own they are 35 endpoints, because a path like /api/memories, /api/settings and /mcp answers two verbs. Both numbers come from server.ts itself, and tests/doku-zahlen.test.ts counts the paths — so a new verb on an existing path moves only the second number, a new path moves both.
Area | Routes |
Memories |
|
Search & graph |
|
Import / export |
|
Inbox |
|
Device Sync |
|
Encryption & repair |
|
MCP |
|
System |
|
Every route listens on 127.0.0.1 only, with rate limiting, Helmet and input validation.
This API is the server run from this repository (npm run dev, or npm run build && npm start). The published npm binary and the Docker container speak MCP over stdio only — POST /mcp answers here, not from npx kepta-mcp.
Loopback is the default, not a promise: setting KEPTA_HOST binds the same unauthenticated API to that address, and then every machine on the network can read and delete your memory. What to do instead of setting it is written in SECURITY.md.
What the API asks for before it destroys anything
Five behaviours a script has to know, all of them added because the old default was a data loss waiting for a typo:
POST /api/memories/importwithmode:"replace"needsconfirm:"replace". Without it the call answers400and nothing happens — the whole knowledge base used to go with it, permanently, on a one-line call. Before the purge the core writes an encrypted snapshot to<data folder>/backup/kepta-pre-replace-<timestamp>.dband names it asbackupPathin the response.dryRun: trueshowsaffected: { active, trashed }first and changes nothing. A payload that carries no usable note (an empty array, or notes without content) is refused with400— that was the case that erased everything. To restore: put the.dbinto an empty data folder askepta.dband start with the same key. Those snapshots stay where they are, one per confirmed swap — cleaning upbackup/is yours, not the core's.DELETE /api/memories/:id?permanent=is strict. Only1,0,true,false, or the parameter absent (= trash). Anything else,Trueincluded, is a400and deletes nothing;?permanent=1&permanent=0is a400too.GET /api/settingsno longer names secrets. Keys ending inkey,api_key,token,secret,password,passwort,passphrase,schluesseland theai_settingsblob come back as{ gesetzt: true, ende: "…last four characters…" }— under eight characters there is noende, and a deleted entry is gone from the answer. Writing stays onPUT; the full value stays inside the encrypted database. Showgesetzt+endein any UI, never the value.Error answers are JSON. No stack trace, no absolute paths, no echo of the body you sent.
413names the boundary and what arrived (limitBytes,receivedBytes), a broken body gets a400without repeating itself, and from500up the real reason goes to the server log instead of the response.POST /api/repair/importsaccepts 16 kb. The limit was declared before and never applied (900 KB went through); now anything larger ends in413. Nothing else on this route changed.
Two smaller ones worth a line each: a symlink inside ~/.kepta/inbox/ is not imported — it stays where it is, and POST /api/inbox/scan reports it in skipped: [{ file, reason }] (a link was enough to pull any readable file of yours into the memory an agent sees). And GET /api/graph is capped: up to 500 entities and 2000 relations (src/core/store.ts, getGraph()), with no ORDER BY in front of either cap — which ones come back is decided by row order, not by importance, so a central term can be missing from a big store. One thing those edges do not bring: the relations in the answer carry validFrom and validTo, but the core's own write path leaves both empty (addRelation is called without a window, so the default null stands). A client drawing this graph cannot tell an expired claim from a live one and must not pretend it can.
GET /api/graph/bild — the one that says what it cut
GET /api/graph stays exactly as it is, because outside agents write against its shape. The picture route is the honest sibling: it answers the contract graf-2 with sicht, entitaet, tiefe, knoten, kanten, layout — and no width/height, on purpose. Positions come in world units, and welt.groesse is the side of this picture — a floor of 1000, growing with the stock, computed by the force law and reported per response (src/core/graf-vertrag.ts). welt.nullpunkt is that world's lower-left corner: since layout stand 5 a position is inside nullpunkt + [0, welt.groesse] on each axis and is not guaranteed to start at 0, because an appended note may grow past the floor of the previous picture while every bit-exact anchored node stays exactly where it was — translating the picture to keep a pretty zero would move all of them. So the client maps (x - welt.nullpunkt.x) / welt.groesse and scales by min(width, height), and a window size never enters a layout calculation. Dividing by a hard-coded 1000 — or assuming a floor of zero — is the one thing a renderer must not do: at 3 000 notes the world is ~2 500 units across and the picture would sit as a fifth-scale cloud in an empty canvas. What it computed is not what it shows, and the answer says so itself in kuerzung: the caps it honoured, what is left over (uebrig), why it cut (grund: knoten, kanten, paarBudget, tiefe, privat, papierkorb), how many private and trashed carriers were filtered — numbers, never names — which nodes and edges are orphaned, and a single vollständig flag. ETag and If-None-Match give a 304 without recomputing. If a client needs to know whether the picture is the whole store, that is the field to read; GET /api/graph does not tell you.
The core separates its own discs now (LAYOUT_VERSION 5). A stock is laid out so that no two node discs overlap before anything is drawn, measured on 400 / 1 500 / 3 000 nodes at 27 concepts: 0 intersecting pairs, counted both by the core's own uncapped rule r = 6 + 2,4·√grad and by the surface's rule min(18, 4 + log2(1+grad)·2,2) (tests/graf-ruhe.test.ts; the same field before this state, run through Stand 4's own source — git show 6ae32d3:src/core/graf-kraft.ts — had 37 / 935 / 7 134 overlapping pairs by the core's rule and 15 / 443 / 5 236 by the surface's, with 36 / 221 / 818 places on the world frame; the run is recorded in docs/belege/nacht-12-10/vorher-gleich-12-10.log.txt, which travels with the project papers and not inside the published package, and its figure is reproducible by laying the same field through that commit). Two numbers keep that verdict from being bought with empty space, and both are expected in the test, per size class — measured 2026-10-12 14:44 on a machine the quietness probe judged calm:
Order: median real edge 283 / 385 / 571 world units against median random pair 427 / 572 / 817 → ratio 0,662 / 0,674 / 0,699, under the 0,8 bar this repository already uses for readability (
tests/ui/graph.test.ts; the run:docs/belege/nacht-12-10/ruhe-mit-anteil-1444.log.txt).Scale: the smallest drawn disc covers 0,769 % / 0,572 % / 0,368 % of the picture's world side (note radius 10,16 WE against 1 321 / 1 775 / 2 759 WE) and must stay at or above 0,3 % — a bar measured off our own surface, which today paints 0,414 % at its densest shipped field (
kepta-app/src/lib/graphLayout.ts:521for the world side,src/components/KnowledgeGraph.tsx:1418for the radius). The ratio alone would not catch a uniformly widened world: a rejected variant reached 0 pairs at world side 19 684 with ratio 0,721 — under the bar — but only 0,052 % of a disc (rundocs/belege/nacht-12-10/kleinste-scheibe-2007.log.txt, started 18:07:18 UTC as its own header says — one run measures radius and side and checks the side againstbild-guete.json[28]), which after the client's normalisation is dust, not a picture.
🐍 Python client
A running KEPTA on this machine is the precondition: the desktop app, or npm run dev in this repository. pip install kepta brings the client, never a server — and note that npx kepta-mcp, the stdio server your AI client starts, opens no HTTP port at all, so it is not what this client talks to.
pip install keptafrom kepta import KeptaClient
kepta = KeptaClient()
kepta.save("Carbonara", "Guanciale, pecorino, egg yolk. No cream.", tags=["cooking"])
for hit in kepta.search("carbonara without cream"):
print(f"{hit.score:.2f} {hit.memory.title}")No dependencies — only the Python standard library.
The example above is measured, not decorative: search("carbonara without cream") ranks that note first, vector similarity 0.75 on the default model (nomic-embed-text). What the same model does not do is find a paraphrase that shares no word with the note: search("what do I cook with pasta") leaves the recipe at 0.51, under the vector floor of 0.58 (src/core/engine.ts, VECTOR_FLOOR), while a note that merely contains the word "Pasta" wins at 0.59. Full text is the leg that answers in that case — it matches the one word the question and the note do share. python/README.md names the limit and what it takes to move it.
🏗️ Architecture
src/core/ memory engine (store, search, encryption, MCP protocol)
server.ts HTTP API (Express, 32 routes)
src/mcp-server.ts MCP stdio server (npx kepta-mcp) and the CLI (npx kepta-mcp)
src/ui/graph.ts the graph builder behind the CLI and the demo — `GET /api/graph` reads `store.getGraph` directly instead. No UI: the core has no window of its own, the interface is your AI client, and for a window there is the free desktop app in this repository's releases
npm/ source of the npm package (kepta-mcp)
Dockerfile container for the MCP server
python/ Python client (PyPI: kepta)
scripts/ eval, benchmark, repair
tools/ benchmark harnesses (LongMemEval, latency)
docs/ environment variables and the words the API uses
tests/ Vitest suite with CI-enforced coverage thresholdsOne code path for the HTTP API and the MCP server — agents get the same results as direct API calls.
🔧 Screws
Every variable KEPTA reads from the environment — with its default, what it changes and what can break if you touch it — is in one table: docs/ENVIRONMENT.md. The three that matter most, in short:
| how many active notes the local API accepts (default 5000) before it answers |
| bind address, default |
|
|
The words the API and these documents use — note, memory, scope, dry-run, supersede, the two
meanings of key and restore — are explained once in docs/WORDS.md.
🔐 Encryption
The knowledge base is a SQLCipher 4 database: AES-256, an HMAC-SHA512 over every page, the WAL included. The key — 256 random bits — lives in the operating system's keychain: the macOS Keychain, Windows DPAPI or the Linux Secret Service. On servers and containers without a keychain, set KEPTA_DB_KEY to 64 hexadecimal characters — generate it once with openssl rand -hex 32 and keep it.
What that means on the byte level, measured rather than asserted: one note written with kepta-mcp 3.2.0 from the registry into a throwaway HOME on 10.10.2026 left a ~/.kepta/kepta.db whose first sixteen bytes are 85 95 55 36 a3 ab f4 02 4c 6c f0 5c 0f 0b 4c 69 — not SQLite format 3 — with no trace of the note's text anywhere in the file. head -c 16 ~/.kepta/kepta.db is the whole test, on any store, yours included.
The rule: no key, no file. When KEPTA is about to create a knowledge base and cannot put its key somewhere safe — no keychain, a denied approval, CI with an empty environment — it does not create the file. It stops with a non-zero exit and names the way out (KEPTA_DB_KEY, or KEPTA_ENCRYPTION=off if plaintext is truly what you want). Failing quietly into an unencrypted database was the old behaviour, and it is the failure this rule exists to kill. Opening an existing file is unchanged: a missing or wrong key there already stopped and changed nothing. Standing date: this was decided on 10.10.2026 and is being put into the code now — a build that predates it still writes plaintext and exits 0 on an unusable value, so if you run an older release, test it rather than trusting this sentence.
That leaves exactly one door to plaintext, and it is a door you have to open on purpose: KEPTA_ENCRYPTION=off. It is for tooling that must read raw SQLite — inspectors, one-off migration scripts — not a performance switch, and not a fix for a key problem (on an encrypted file it produces an unreadable-file error instead). Never reach for it to get a container started; that is the case KEPTA_DB_KEY is for. What the encryption covers, what it cannot cover, and how to keep a copy of the key: SECURITY.md.
🧪 Quality
1045 tests (85 files, measured 2026-10-13) with Vitest and v8 coverage — the number of cases a run collected and could execute: a case somebody wrote as it.skip in the source leaves the count, a case the quietness probe refused during the run stays in it, because it is the same tree on the same runner either way (scripts/test-bericht.mjs spells out why: on this desk three timing cases refused at one commit and ran at the next, and a number that moves with the neighbourhood is not a number). npm test writes that run's JSON report, and node scripts/doku-check.mjs then holds both READMEs to exactly that number. tests/doku-zahlen.test.ts listens to the same report and refuses to compare when it cannot be trusted — missing, older than the suite, or written by a partial run — instead of checking a value out of the drawer. The coverage thresholds are a CI gate: a commit that falls below one of them turns CI red. On top: a retrieval eval (Hit@1, Recall@5 capped, MRR) on a fixed corpus, an ablation test per retrieval leg, an encryption eval and a boundary test on the core architecture.
Coverage thresholds (enforced by CI)
Area | Lines | Functions | Branches | Statements |
together, inside the coverage scope — pinned by CI | ≥ 70 % | ≥ 72 % | ≥ 56 % | ≥ 66 % |
| ≥ 87 % | ≥ 89 % | ≥ 74 % | ≥ 85 % |
Measured scope is src/core/** plus server.ts (vitest.config.ts) — the headless
core, which is the product. tools/, scripts/, npm/ and python/ have their own
suites but no threshold, so they are not inside these percentages.
Latency — measured, public
Median search times on one laptop (Apple M4), every run committed in tools/latenz/ergebnisse/: 46.6 ms median at 100,000 memories (p95 87.6 ms, tools/latenz/ergebnisse/latenz-100k-fts-2026-10-01.json), 276.5 ms median at 1,000,000 memories (tools/latenz/ergebnisse/latenz-1m-fts-2026-10-01.json). Three conditions sit in the records themselves and belong with the numbers, not under them. The runs measure the engine with encryption switched off and a deterministic stub embedder — konfiguration.verschluesselung and konfiguration.embedder say so. And konfiguration.vektorChunks is 20000 in both: tools/latenz/bench.ts:38 pins the vector leg to min(20_000, N), so the million-memory median was measured with the same 20,000 chunks as the 100,000 one — a bounded vector leg, not a fully embedded store of that size. A store where every note carries its vectors searches slower than this one number suggests; how much slower is a measurement this repository has not published yet, and tools/latenz/README.md states the 20,000-chunk condition where the runs are listed. The file on disk is always SQLCipher-4 encrypted; the crypto cost is just not inside these two medians.
Benchmark record — LongMemEval-S, fully public
KEPTA ships its memory benchmark as raw per-question records instead of a headline number: every run commits all 500 question records to this repository, in tools/longmemeval/ergebnisse/ — each with its id, type and skill, the judge's verdict, how many notes retrieval returned, which of the retrieved contexts settled it, and whether the reference answer appeared verbatim. The question and answer text is not in the repo: it comes from the pinned upstream dataset, whose SHA-256 every record carries. Fixed dataset (LongMemEval-S, 500 questions, SHA-pinned), fixed judge, fully reproducible locally — see tools/longmemeval/README.md.
The series is committed as raw per-question records — and it is not a current measurement, which is why no score is quoted here. Every record in that folder names a kepta_version between 2.13.2 and 2.13.22 (grep -h kepta_version tools/longmemeval/ergebnisse/*.json), while this repository ships 3.4.0. The two belong together in one sentence and mean one thing: the records stay valid for the engine they were run against, and a version number that moves without a new run says nothing about 3.4.0 — it neither revives those percentages nor invalidates them. Until the chain runs again against the shipped engine, the numbers stay in the records and out of the paper. What does not change: both judges are local (qwen2.5:3b for the continuity series, qwen2.5:14b Q4 for the later runs, both on a consumer GPU), zero cloud, no fine-tuning, and every step of both chains is a run record in the same folder.
What the metric measures — read this before comparing it to anyone else's. It is context sufficiency, not end-to-end answer accuracy: a local judge (qwen2.5:14b, temperature 0, prompt v1) grades whether the retrieved contexts contain the reference answer. There is no generator LLM in the loop, a PARTIAL verdict counts as half a point, and 29.8 % of the questions in that run were settled by a deterministic verbatim match without any judge call at all (tools/longmemeval/README.md, line 104). Zep's published 63.8 % is answer accuracy of a full generation pipeline judged by a cloud LLM — related evidence, not the same metric; both figures and that difference are written out in the cited paper. And one more caveat, stated plainly: every lever in the series (topk, --temporal, --zeitsplit, HyDE, judge size) was selected on these same 500 questions, so the series figure is an in-sample one, biased upward; an untouched holdout split has not been run yet. The raw per-question records are the point of publishing all this — check us, including this framing.
👋 Who builds KEPTA
KEPTA is built by Damian Todorovic and Emil Wagner — a small team from Germany, shipping in public. Questions, ideas, Enterprise for your team — or you simply want to follow where this is going: find us on LinkedIn.
📄 License
BUSL 1.1 — source-available, not OSI-approved open source: every line public, production use inside your own organization always allowed, offering it as a hosted service is reserved, and each version converts to AGPL-3.0-or-later four years after release. The npm package kepta-mcp carries the same license as this repository; the Python client under python/ stays MIT so any Python project can embed it. KEPTA Enterprise for teams and organizations is licensed by agreement — write to me on LinkedIn.
What this repository is — and what it is not. This source-available core is the memory engine: storage, retrieval, MCP — free, for every AI. Forks are legitimate; presenting a fork as KEPTA is not: the KEPTA name and the KEPTA branding belong to Damian Todorovic. Fork it, build with it, ship your own product from it — under a different name, without offering this engine as a hosted service, and with every change you make becoming AGPL-3.0-or-later when the version's Change Date arrives. Private notes stay out of every AI channel — private is a disclosure rule toward agents, not a shield against programs running on your own machine (SECURITY.md).
This server cannot be deployed
Maintenance
Related MCP Connectors
Persistent memory for AI agents. Search and store durable facts, preferences and decisions.
Persistent memory and knowledge graphs for AI agents. Hybrid search, context checkpoints, and more.
Persistent memory for AI agents. Semantic search, memory graph, W3C DID identity.
Persistent memory for AI agents. EU-hosted, privacy-first, hybrid recall, contradiction detection.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceA local memory engine for AI agents. Stores conversation episodes, consolidates knowledge through a neuroscience-inspired lifecycle, and builds a personal knowledge graph — all in a local SQLite database.18MIT
- AlicenseNot gradedqualityAmaintenanceOpen-source AI memory layer for LLM agents. Importance scoring, temporal decay, hierarchical memory (facts, summaries, themes), YMYL prioritization, and active retrieval with contradiction detection. Supports OpenAI, Anthropic, Ollama. Local-first with SQLite + FAISS.129 PyPI49Apache 2.0
- AlicenseAqualityCmaintenanceLocal-first memory for AI agents. On-device hybrid retrieval over a single SQLite file.163Apache 2.0
- AlicenseAqualityAmaintenanceEmbedded, local-first agent memory: facts extracted into a per-namespace SQLite file (vec0 + FTS5) with hybrid retrieval and point-in-time (time-travel) queries. ADD-only history over stdio — no server process, no cloud dependency.743Apache 2.0