# al-buddy-memory [![Listed on mcpservers.org](https://mcpservers.org/badge.svg)](https://mcpservers.org/servers/flytomoon/al-buddy-memory) > **Al Buddy** — that's *Al*, a name, said like "pal". Not A.I. ### A memory that's yours. **Portable, governed, model-agnostic memory for AI agents.** A fact is invalidated, never overwritten; on a governed handle, erasure runs through policy and is audited. Raw text is the source of truth and cannot be edited. Embeddings are a disposable, model-tagged cache. Every fact and every link exports to one documented format. The memory outlives whatever model, runtime or company produced it. --- ## Why this exists Every agent-memory product on the market answers one question well: *what does the agent recall?* None of them answers the four questions that decide whether you can trust and keep that memory: | Question | This library | Letta | Mem0 | Zep | |---|---|---|---|---| | **Where did this fact come from, and who asserted it?** | Provenance on every node and edge (`UserInput` / `AIInferred` / `GuardianAdded` / `SystemGenerated`) | Memory-file git history | Metadata field | Graph episodes | | **When was it true, and what replaced it?** | Two queryable axes: `validAt` answers what was true at Y; `getNodeAsOf` / `snapshotAsOf` answer what the store believed at X from full before/after versions. They combine in one call (`snapshotAsOf(X, { validAt: Y })`), a read the history cannot vouch for is marked, and erasure removes the history too | Git history of files, not a fact model | Change history per memory (`history()`: old value, new value, event, timestamps) — transaction history, not valid time | Temporal graph (its real strength): Graphiti edges carry `valid_at` / `invalid_at` alongside `created_at` / `expired_at` | | **Can I take it with me, losslessly, to another runtime?** | One versioned JSON export with a published schema and conformance tests | `.af` (agent state, framework-shaped, archival memory not yet included) | Cloud export | Cloud-only since 2025 | | **Does it run with no vendor, no key, no server?** | SQLite on disk, on-device embeddings | Self-host possible; cloud is the product | Self-host possible (Apache-2.0, local vector stores); needs an LLM for extraction; cloud is the product | Zep is cloud; Graphiti self-hosts (graph DB + LLM key required) | Sources for the cells above, each checked against the project's own code or announcement on 2026-09-18: Mem0's per-memory history is a SQLite table with `old_memory`, `new_memory`, `event`, `created_at`, `updated_at` ([`mem0/memory/storage.py`](https://github.com/mem0ai/mem0/blob/main/mem0/memory/storage.py)); Mem0 is Apache-2.0 and runs against local vector stores including Qdrant, Chroma, pgvector and FAISS ([vector-store docs](https://docs.mem0.ai/components/vectordbs/overview)), with an LLM called to extract facts on the default `add()` path. Zep stopped maintaining Community Edition on 2025-04-02, in [its own words](https://blog.getzep.com/announcing-a-new-direction-for-zeps-open-source-strategy/): *"we've decided to stop maintaining and releasing Zep Community Edition."* [Graphiti](https://github.com/getzep/graphiti), the engine under Zep, is Apache-2.0 and self-hosts, and its stated requirements are a graph database (Neo4j, FalkorDB, Amazon Neptune, or the deprecated Kuzu) plus an LLM key — it "defaults to OpenAI for LLM inference and embedding." **The three local, keyless ones people will name**, read against their own code on 2026-09-21. They answer the fourth question the way this library does — your machine, your files, no account — and they are the honest comparison, not the cloud products. - **[basic-memory](https://github.com/basicmachines-co/basic-memory)** (AGPL-3.0; Markdown notes on disk are the truth, SQLite is a rebuildable index). **It has valid time, and that is worth saying plainly:** an observation can carry `@effective[2026-06-10,2026-07-27)` in the note itself, parsed into a time index ([`models/knowledge.py`](https://github.com/basicmachines-co/basic-memory/blob/main/src/basic_memory/models/knowledge.py)) and rebuilt from the file, so the file stays the source of truth. What it does not carry on a local install is who asserted a fact: `created_by` / `last_updated_by` exist and hold a cloud user id, null for local and CLI use. Portability is the strongest kind and the least specified: the notes *are* the format, so nothing is trapped, and there is no versioned schema to import them into something else. Capture and full-text search need no key; semantic search is opt-in and does. - **[The MCP memory server](https://github.com/modelcontextprotocol/servers/tree/main/src/memory)** (the reference implementation most agents meet first; one JSONL file). Entities, relations and observations, and nothing else: no field for who said it, no timestamps at all, and `deleteObservations` filters the old value out of the array, so what was true before is gone rather than closed. The file is its own export. Nothing calls a model; nothing leaves the machine. - **[Memori](https://github.com/GibsonAI/Memori)** (Apache-2.0; your own SQL database — SQLite, Postgres, MySQL, Oracle). A fact in `memori_entity_fact` has content, an embedding, a count and a last-seen date; who asserted it is only recoverable by walking the foreign keys back to a conversation message, not a property of the fact, and there is no valid time — `date_updated` replaces. "Your data stays in your database" is the portability story, with no interchange format published. It is local in the sense that matters for your data, but capture is not keyless: extraction and embeddings call an LLM, and the default SDK path expects a Memori account as well. **Also worth naming: [OpenMemory MCP](https://github.com/mem0ai/openmemory/tree/main/openmemory-archive)** (Mem0, launched May 2025) shipped the same distribution idea — one local memory store shared across MCP clients — and Mem0 archived it; its README now opens "This project has been archived." Its schema is the contrast this table is about: [`models.py`](https://github.com/mem0ai/openmemory/blob/main/openmemory-archive/api/app/models.py) gives a memory `content`, `created_at`, `updated_at`, `archived_at`, `deleted_at` and a state, with no field for who asserted the fact and no valid time, and `content` is rewritten in place on update. It does keep a state-transition history and an access log, and it does have a ZIP export — so it is not the absence of portability that separates the two, it is provenance, valid time and an immutable raw. Recall benchmarks (LOCOMO, LongMemEval, DMR) measure what an agent remembers. **None of them scores a memory system on provenance, invalidation or portability.** This library is built for that axis, and the conformance scorer below is one attempt at measuring it. The table is our reading of each project's own code and public docs, dated above; if we have a cell wrong, a PR with a link fixes it. We run LongMemEval anyway, so a recall number people recognise sits beside the conformance score rather than in place of it, along with a small benchmark of our own for the part recall benchmarks skip: when a fact changes, does recall return the value that is true now? See [Benchmarks](#benchmarks). ## Start here An empty memory gives an assistant nothing to stand on. [docs/STARTER.md](docs/STARTER.md) seeds yours in ten minutes: pin who the person is and how they want to be treated (including "never a yes-person"), choose the rules the store enforces, and let it derive the rest nightly. Upgrading from an earlier version: [CHANGELOG.md](CHANGELOG.md) marks anything that changes what an existing caller gets back. 0.4.0 has breaking changes (immutable content, governed erasure, a new `listNodes` on the store interface, the MCP exports moved to `al-buddy-memory/mcp`), so read that entry before you upgrade. ## What is in the box - `MemoryStore`: a storage-agnostic interface; `SqliteMemoryStore` and `InMemoryStore` ship, with a **conformance suite** any backend can run against itself. - `ProjectMemory`: one brain scoped by project or person, each in its own SQLite file. - `HybridRetriever`: lexical + semantic recall with decay-aware confidence; embeddings on-device via transformers.js (no API key). Recall can be scoped (memory type, tags, minimum confidence, privacy and retention tiers), and the scope applies to the keyword and the vector side alike. Two opt-in aids for questions whose answer is spread across conversations, both computed at read time and never stored: a `reranker` (`LocalReranker`, an on-device cross-encoder) that rereads the question with each candidate and reorders them, and `expand`, which reads a question's time and counting cues (`analyzeQuery`: "in April", "the past two weeks", "how many", "A and B") and recalls for each. - `exportPortable` / `importPortable`: the lossless interchange format, versioned, with a [JSON Schema](docs/portable-format.schema.json). - `PinnedBlocks`: a size-capped tier of facts that belong in every prompt, editable by the agent itself, on top of the governed store. - `consolidate`: a sleep-time pass that reads recent raw memory and writes **new** derived facts with provenance edges back to their sources; the raw is never rewritten and nothing is summarised away. - `verifyDerived`: every conclusion stores the exact words it rests on (`evidence`), checked when it is written; this re-checks them any time, and retracts, never deletes, one whose evidence no longer holds. - Mental models: standing questions with answers kept current in the background, so reading one costs no model call (`defineMentalModel`, `refreshMentalModels`, `getMentalModel`). Every answer quotes the facts it rests on, keeps its history ("what did we think in June"), and reads as stale the moment one of those facts stops being true or is erased. - Current state: "where does X stand now?" held as one live memory per subject (`recordState`, `currentStates`, `statesMentionedIn`). A newer state closes the one it replaces and keeps it as history; a state that arrives late never overturns a newer one. Recall can also weigh recency (`freshness`), so the newest of several matching notes comes first. - `explainFact`: why a fact is believed, in one call — provenance and origin, validity and what replaced it, a conclusion's evidence with each quote checked, and its history. Through a governed handle a source the reader may not see is named as withheld, never shown. - Conclusions go with their facts: a fact that stops being true retracts what was concluded from it (kept, marked); a fact that is erased takes everything built from it (SPEC §8a). - `listConsolidations` / `undoConsolidation`: review what each pass concluded, with the evidence for every fact, and take back one pass's conclusions. Undo retracts (`validTo`) and records who withdrew each fact and why; it never deletes, so the history still shows what was believed, when it was withdrawn, and the reason. - `buildSourceProvenance` / `readSourceProvenance`, `renderMemoryBlock`, `exportMemoryMarkdown`, decay helpers. - Integrations: `al-buddy-memory/ai-sdk` (Vercel AI SDK tools + middleware), `al-buddy-memory/langchain` (a LangGraph long-term memory store + LangChain tools), `al-buddy-memory/mastra` (an input processor + tools). The frameworks are optional peer dependencies; every write is governed and records which agent made it. Guides in [docs/integrations](docs/integrations/). Node ≥ 20. One runtime dependency (`better-sqlite3`, with prebuilt binaries for Node 22, 24, 25 and 26 on macOS, Linux and Windows; on Node 20 it compiles from source, which needs Python and a C++ toolchain). Postgres (`pg`), the MCP SDK and the on-device model (transformers.js) are optional: they load only if you use them, and `npm install --omit=optional` leaves them out. Prior art: the Letta project published the idea of a pinned memory tier and a background pass over memory (memory blocks; sleep-time agents). What is different here: every derived fact must cite the raw it rests on or it is refused, the raw is never rewritten, and a whole pass can be reviewed and undone, with the undo and its reason kept on record. ```ts import { SqliteMemoryStore, ProjectMemory, PinnedBlocks, exportPortable } from "al-buddy-memory"; const store = new SqliteMemoryStore("./brain.db"); await store.addNode({ provenance: "UserInput", memoryType: "Lesson", content: { text: "Chris prefers decisions over options." }, /* …governance fields… */ }); const pins = new PinnedBlocks(store); await pins.pin({ text: "Never present options without a recommendation.", label: "rule" }); const snapshot = await exportPortable(/* … */); // → docs/portable-format.schema.json ``` Full contract: [docs/SPEC.md](docs/SPEC.md). Design record: [docs/DECISION-2026-07-07.md](docs/DECISION-2026-07-07.md). --- ## Python, and other languages The MCP server and the portable format are the language-neutral surface: a Python agent can use the governance server today, and any language can read the export (it is plain JSON with a [schema](docs/portable-format.schema.json)). A native Python package is planned; open an issue if you need it sooner and say what you would use first. ## License Apache-2.0. See [LICENSE](LICENSE). ## Governance is enforced, not implied The vocabulary — Public / Private / Sensitive / Sealed, retention tiers, provenance on every fact and relation — ships with the store. `govern()` is what enforces it: policies in front of every write, update, read and export, and an append-only audit trail of who read what and why. ```ts import { SqliteMemoryStore, govern, personalDefaults, storeAudit } from "al-buddy-memory"; const inner = new SqliteMemoryStore("brain.db"); const store = govern(inner, { policies: [personalDefaults({ owner: "chris" })], context: () => ({ actor: currentActor() }), // The trail goes in the database, hash-chained, in the same transaction as // the fact it describes. `new ChainedAudit("audit.jsonl")` puts it in a file // instead — for a store that is not SQLite, or when you want it outside the // file it describes. See docs/GOVERNANCE.md for what each one proves. audit: storeAudit(inner), }); ``` To make sure nothing is ever erased by accident, add `memoryLock()` to the policies: while it is there, erasure is refused for everyone, the owner included, until you take it out (or flip the switch it reads). Invalidating a fact still works; that is not erasure. The raw store and the database file are outside any policy, so keep backups. And to make a deletion something you can take back, pass `recentlyDeleted: { days: 14 }` to `govern()`: a delete then moves the fact out of recall for 14 days, `restoreDeleted` brings it back, and `purgeDeleted` erases it for good once the days are up (asking the policies again, so the lock still wins). Nothing runs on a timer, and until it is purged the fact is still in exports and backups. Check the trail with `al-buddy-memory verify-audit brain.db`. It names the first event that was edited, removed, inserted or reordered. What it establishes, and the two things it does not, are written out in [docs/GOVERNANCE.md](docs/GOVERNANCE.md#what-verification-establishes) — short version: the chain is tamper-*evident*; a tail cut off a **log file** is invisible to it, a tail cut off the **table** is caught as long as nobody resets the table's own counter, and only a head you anchor somewhere else catches a rewrite or a restored backup. Three policies ship to copy: personal defaults (secrets auto-classified Sensitive; Sensitive facts never reach, leave with, or get erased by anyone but the owner in person; only the owner changes a fact), guardian mode (only a guardian may write or change a guardian's fact), enterprise audit (low-confidence inferences hidden from non-reviewers; exports gated to exporters). A policy is a plain object with five optional hooks; see [docs/GOVERNANCE.md](docs/GOVERNANCE.md). Without any policy the store still guarantees: Sealed facts never surface in a search unless asked for by classification; `provenance`, `nodeId`, `encryptionKeyRef`, raw `content` and the anchor trail are immutable after write, through every path including import; every instant is stored in one canonical UTC spelling. Two things it does not do, said plainly: provenance is what the writer asserts (immutable once written, not verified — bind actors to provenance in a policy); and `encryptionKeyRef` names a key you manage, it does not encrypt the file. The governed handle is the boundary. `govern(store, …)` puts policies in front of every operation that can change a fact or reveal one — including erasure, which is refused unless a policy explicitly allows it. Whoever holds the inner store is not governed by anything, so hand out the governed one. The rules an assistant on this memory is held to are published in [docs/policies](docs/policies/README.md): ethical behaviour, user sovereignty and privacy, lifecycle and guardians, data stewardship — and [an honest ledger](docs/policies/ENFORCEMENT.md) of what the code enforces, what a prompt carries, and what is still a person's decision. They change in the open. ## Enterprise The same store for an organisation: a shared database, rules about who sees what, encryption the operator controls, and erasure on request. SQLite stays the default, and nothing here changes a local setup. - **Postgres.** `PostgresMemoryStore` (pgvector 0.8 or newer) sits behind the same `MemoryStore` interface and passes the same conformance suite. Every read and write is confined to one tenant key, and the audit chain is kept per tenant in the same transaction as each governed write; `verify-audit --postgres --tenant ` checks it. A write reads and writes only the rows it acts on, so its cost does not grow with the tenant: measured flat from 1,000 to 100,000 facts (about 1–3 ms a write, 3–6 ms governed and audited, in-process PGlite). [docs/POSTGRES.md](docs/POSTGRES.md) - **Boundaries.** A policy can declare who sees what as data: `readBoundary`, fact labels against the asking actor's attributes, with equality, membership, AND and OR. SQLite and Postgres compile it into their own `WHERE`, before ranking and before any `limit`, so facts outside it never take a place in a page. It fails closed, and `beforeRead` still has the final word. [docs/policies/boundaries.md](docs/policies/boundaries.md) - **Sign-in.** The HTTP server can accept OIDC/JWT bearer tokens from your own identity provider (Okta, Microsoft Entra, Google Workspace, any OIDC issuer) instead of the owner's passphrase. Signature (the issuer's JWKS), issuer, audience and expiry are checked with jose; configured claims become the actor attributes boundaries read, and one claim can name the tenant. Any failed check is a 401. [docs/SIGN-IN.md](docs/SIGN-IN.md) - **Encryption at rest** is not something this library does. On Postgres it belongs to the database: storage encryption with KMS-managed keys, encrypted backups, TLS. Locally the SQLite file is plaintext, owner-only (0600 in a 0700 directory), so use full-disk encryption (FileVault, BitLocker, LUKS). For file-level encryption, a SQLCipher build of SQLite is the route, but it is **not built or tested here**. `encryptionKeyRef` names a key you manage; it encrypts nothing. - **Erasure.** `eraseWhere({ label: "subject", equals: "person-42" })` erases every fact carrying that label that the actor can see, along with what was concluded from them. Each fact goes through the erase policies, so a memory lock still refuses and Recently deleted still holds. The call returns a receipt: selector, actor and time; counts erased, refused (with reasons) and held; ids hashed. Its digest is chained into the audit trail, so `verify-audit memory.db --receipt receipt.json` checks it. **Backups and exports made before the erasure are outside it**, and the receipt says so. It behaves the same on SQLite and Postgres. [docs/ERASURE.md](docs/ERASURE.md) ## Enterprise edition The core stays free and Apache-2.0, including everything above. A commercial enterprise edition adds: - multi-process safety for serverless deployments - data-API query executors - database row-level security - an enterprise readiness suite The seams these plug into are public, and so is read audit: `PostgresMemoryStore` with `accessAudit: { sink, reads: true }` hears every read and write of a tenant, inside the call's own transaction. [docs/EXTENSION-POINTS.md](docs/EXTENSION-POINTS.md) To ask about it, contact the repository owner, [@flytomoon](https://github.com/flytomoon). ## Limits, measured One SQLite file, one process, one writer. Measured on an M1 Pro laptop with 100,000 facts (`bench/bench.mjs`, better-sqlite3, WAL): | Operation (100,000 facts) | Measured | |---|---| | Insert, one fact per call | 5,400–5,900 facts/s (17–18 s for all 100k) | | Keyword recall, top 10 (FTS5 + decay re-rank) | 30–50 ms median, ~150 ms worst of five terms; first query after open ~320–380 ms (cold cache) | | Keyword recall through a governed handle, top 10 | ~75 ms for a word in 10% of facts, ~135 ms for two such words, ~850 ms for a word in every fact — see below | | Recall by filters only, top 10 | 0.5–1 ms | | Get by id | 0.1 ms | | Invalidate a fact | 0.5 ms | | Reconstruct `snapshotAsOf` | 2.19 s with 100,001 versions | | File size | 69 MB for the 100,000 facts; each recorded change adds ~738 bytes (140 MB after one update to every fact) | | Audit event into `audit_events`, in the fact's own transaction | +0.04 ms per governed write, +0.5 ms per governed read (a read is audited too, so it takes the write lock briefly) | | Checking the chain — `verify-audit ` | linear, ~3 µs/event: 83 ms at 20,000 events, 325 ms at 100,000, 1.5 s at 500,000. Each process pays it once, before its first governed write and **outside** the write transaction, so it delays that process and blocks no other. Constant memory (the walk streams) | | How fast the trail grows | one event per governed write, one or two per governed read — a `remember` is +2, a `recall` +1. Nothing prunes it. At 500,000 events the trail is ~128 MB, which can exceed the facts it describes; if you drive a store that hard, keep an eye on it | Ranges are three runs of the same script on 0.4.0. Paging is exact: a page of ten is the first ten of the full ordered read. When facts have genuinely decayed, the store may have to read past its 200-row candidate pool to keep that promise — the worst case is a full read of the matching facts (~300 ms at 100k), and it only happens when a decayed fact and a fresher one would otherwise trade places. The transaction-time measurement is one run on the same M1 Pro: 100,000 facts, one recorded reinforcement per fact, and one earlier invalidation. Reconstructing all 100,000 facts from 100,001 versions took 2.19 s. It is an in-memory linear reconstruction, not an indexed point lookup. A governed keyword search is slower on purpose. It reads every match, keeps the ones the actor may see, and ranks them with word rarity counted over those visible matches alone: the store's BM25 counts rarity across all facts, hidden ones included, so it would let a hidden fact reorder visible results. At a personal memory's size (a few thousand facts) the speed difference does not show, and a test holds its quality: twelve facts asked for in plain questions ("what is the wifi login") among two hundred distractors all land on the first page. What that means: a personal assistant or a single-tenant service will not notice the store. Hosted deployments can use `PostgresMemoryStore` with explicit tenant keys; SQLite remains the default. Node/TypeScript only for now; the optional on-device embedder is a 25 MB model download. ### The semantic path costs more Everything above is the keyword path. Recall with an embedder wired goes through a **brute-force linear scan**: every stored vector is read, decoded and scored against the query. Measured the same way (`bench/bench-vectors.mjs`, 384-dimension float32 vectors, the width and number type of the default on-device model), on the same laptop, since 0.8.1 stores vectors as float32 bytes (the 0.8.0 JSON-text figures in brackets): | With an embedder wired | 20,000 facts | 100,000 facts | |---|---|---| | File size (facts + vectors) | 55 MB (172 MB) | 277 MB (864 MB) | | Per vector, on disk | 1,536 bytes (~8,000) | 1,536 bytes (~8,000) | | Semantic recall, top 10 — first of a session | 488 ms (765 ms) | 1,619 ms (3,200 ms) | | Semantic recall, top 10 — thereafter | 22 ms median (36 ms) | 113 ms median (187 ms) | `gaugeStore` measures any store file this way and `checkBudgets` says which numbers are over budget. The history of this section is worth keeping: on 2026-09-19 it measured the JSON cost, named float32 storage as the fix, and filed it here as a weak spot. It was built six days later, when a question about backups surfaced it. A measurement with no budget is a fact nobody owns; that is why the gauge exists. **Where the time goes, 0.8.0 → 0.8.1.** On 2026-09-19, one cold call at 100,000 facts spent 978–1,182 ms reading the vector table and 1,418–1,744 ms in `JSON.parse`, against 85–127 ms for the cosine scan itself: reading and parsing 8 KB text rows was 95% of the cost, and the scan everyone suspects was 4%. Storing float32 bytes shrank the read about 5× and removed the parse; what remains of the 1.6-second first call is reading 154 MB and turning it into JavaScript arrays. Two consequences: - **`sqlite-vec` is still not the next move at this size**; the scan is not the cost. - **The vector cache is disposable and model-tagged.** Vectors live in their own table keyed by `(nodeId, model)`; deleting them loses nothing but time, and a vector from a different model is skipped rather than compared. Upgrading the embedder is a re-index, never a migration. The scan is still linear in the number of facts, and a session's first call pays for the whole vector table; later calls reuse a 60-second in-process cache. ### Backups, restores and synced folders A SQLite database in WAL mode is **three** files — `brain.db`, `brain.db-wal`, `brain.db-shm` — and two ordinary habits will quietly lose a person's memory: - **Restoring a backup:** stop the server first, then delete `brain.db-wal` and `brain.db-shm` before you copy the backup into place. A `-wal` left beside a restored file is replayed over it on the next open, so the restore appears to succeed, reports no error, and leaves you with the data you were trying to replace. Verified, 2026-09-19. - **Which backup command:** `sqlite3 brain.db ".backup out.db"` and `VACUUM INTO 'out.db'` are the safe ones — both take a consistent snapshot of a live database, verified while another process was writing. `sqlite3 .dump` works too, but it does not carry `user_version`; before 0.4.2 a restored dump could not be opened at all (`duplicate column name: valid_from`), and now it migrates cleanly. Never back up by `cp`-ing a live database: a copy taken mid-write can be unreadable, and a readable one can still fail `PRAGMA integrity_check`. - **Synced folders:** never put the database in iCloud, Dropbox, OneDrive or Google Drive. WAL mode assumes one machine coordinating its own locks; a sync client copying the three files independently, or two machines writing through one folder, corrupts the file rather than conflicting visibly. Back the folder up by all means — copy it out on a schedule, or use `.backup`/`VACUUM INTO` — but do not let a sync client own the live file. ## The conformance score **Try it: [albuddy.com](https://albuddy.com/)** — paste any memory export, nothing leaves your browser. Recall benchmarks are saturated. Nobody scores whether a memory system can say **who** asserted a fact, **since when**, whether it is **still true**, and whether the fact **survives leaving the vendor**. This does, on any export you paste in: ```sh npx al-buddy-memory conformance my-export.json # format auto-detected npx al-buddy-memory conformance agent.af --format blocks # block-style agent files npx al-buddy-memory conformance memories.json --format records # flat memory records npx al-buddy-memory conformance --demo # a small governed store, for comparison ``` Seven dimensions, each 0–100% with the reason spelled out; a dimension the sample cannot prove (no retired facts present, say) is reported as unproven and left out of the total instead of counted as a failure. The reference score: | Export | Provenance | Since when | Retire without erasing | Confidence | Relations | Portability | Grade | |---|---|---|---|---|---|---|---| | al-buddy-memory (demo store) | 100% | 100% | 100% | 100% | 100% | 100% | **A** | Score your own export the same way: `--format blocks` for block-style agent files, `--format records` for flat memory records, or paste it into the demo at [albuddy.com](https://albuddy.com). Where the numbers come from, said plainly, because this is a scorer we also score ourselves with. **Five of the seven** — provenance, since when, retire without erasing, confidence, relations — are counted off the records in the file you paste; change the sample and they move. **Two are not.** Whether a system keeps a retired fact, whether its schema is published, and whether it itemises facts are properties of the *system*, which no single export can prove, so the adapter author declares them in `src/conformance/adapters.ts` and they show up verbatim in the report's reason line. The one part that is executed rather than asserted is the round-trip: for our own format the scorer imports your artifact into a fresh store, exports it again and compares — on your file, and it says `lossy` if that fails. The rulebook, including which dimension is which and what the score does **not** measure (recall quality, truth, latency), is [docs/SCORING.md](docs/SCORING.md). The adapters are written against export *shapes*, not vendors. If a system starts recording provenance, its score goes up — that is the point. Add an adapter for your shape and open a PR; if you think we declared a trait wrongly for yours, that is a one-line PR too. ## Benchmarks The conformance score measures what recall benchmarks leave out; it does not replace them. Two harnesses in [bench/](bench/README.md) put numbers beside it, each with its result file, commit and settings in [bench/results/](bench/results/): - **[LongMemEval](bench/longmemeval/README.md)**, the public recall benchmark: 500 questions over chat histories of about 50 sessions each. Every question gets a fresh store holding its history, one memory per message through the public API, nothing extracted or summarised; recall chooses what the reader sees. The reader and judge prompts, the yes/no rule and the retrieval metrics are the official ones, held to the official code by tests. The reader and judge are Claude, run through the Claude Code CLI on a subscription, so a run spends no metered API money — and the judge is therefore not the official gpt-4o, which is why every result ships its answers in the official format for anyone to re-judge. `node bench/longmemeval/run.mjs --limit 50` is a smoke run. Latest full run (2026-09-30, all 500 questions, no reranker): **92.6% overall, 94.4% task-averaged**, with the recall call itself at **11.7 ms p50 / 33.5 ms p95** on a laptop — side by side with the all-levers run in [bench/results/README.md](bench/results/README.md). Re-graded with the **official judge (gpt-4o-2024-08-06, the official prompts, temperature 0): 92.8% overall, 94.4% task-averaged** — the number to compare with other systems; the Claude judge gave 92.6% / 94.4%. - **[Stale facts](bench/README.md#stale-facts)**, our own: things about a person that change over time, told to the same store three ways — append-only, append-only recalled with freshness, and through `recordState`, where a newer state closes the old one. It asks whether recall returns the value true now, and the value true at a past instant. No model is called; it is deterministic. Stale facts, as measured ([result](bench/results/2026-09-29-stale-facts.json); 24 questions about now, 11 about a past instant; keyword recall, no embedder; first 5 results): | | append-only | append-only + freshness | recordState | |---|---|---|---| | Now: an outdated value comes first | 50.0% | 4.2% | 0.0% | | Now: an outdated value in the first 5 | 87.5% | 37.5% | 0.0% | | Now: the current value comes first | 16.7% | 25.0% | 33.3% | | Then: a value not true then in the first 5 | 27.3% | 27.3% | 18.2% | | Then: the value true then comes first | 27.3% | 9.1% | 27.3% | Read it for what it is. Invalidation is what takes the outdated answer off the table: half the "where do things stand" questions put an old value first in an append-only store. Freshness hides that without removing it — the old value is still in the first five more than a third of the time — and it mistakes an old fact mentioned late for a new one: asked what car I drive, it answers with the 2019 Civic mentioned last. The current value coming first a third of the time at best is keyword recall, for every strategy alike: short questions ("what phone do I have?") match rare words in unrelated facts, and "I accepted an offer from Cobalt Robotics" shares only "I" with "where do I work?". The two past-instant questions where `recordState` still shows a wrong value are a gap this benchmark found: a state learned late is closed at the next state still live, not the next state in time, so a 2018 home mentioned after the 2025 one reads as valid alongside the homes in between. ## The governance MCP server Most memory MCP servers hand the agent a fact. This one hands it a fact **it can weigh**: every `recall` result carries `provenance`, `validFrom`, `validTo`, `current`, `confidence`, which assistant wrote it and which retired it, and — for a superseded fact — the id of what replaced it. `invalidate` closes a fact's validity and keeps the record; the server has no erase tool. It serves a governed store: the owner's `personalDefaults` with the AI client as the audience, so a secret an agent writes is classified Sensitive and kept out of any AI's recall, and every call is audited — into the database itself, in the same transaction as the fact. ```json { "mcpServers": { "memory": { "command": "npx", "args": ["-y", "--package=al-buddy-memory@0.12.0", "al-buddy-memory-mcp"] } } } ``` `al-buddy-memory-mcp` is an executable *inside* the `al-buddy-memory` package, not a package of its own, so `--package=` is what tells npx where to find it — `npx al-buddy-memory-mcp` looks for a package by that name and gets a 404. Drop the `@0.12.0` to track the latest release instead of the one you tested. > **Releasing?** This pin is a documented version and goes stale the moment a new one > publishes — the example would then install an older server than the page describes. > **Advance it in the same commit as the version bump** — it has been, at 0.4.2 and 0.5.0, and was > `@0.4.1` while 0.4.1 was current. Running the pinned command against a newer release > returns the older server's handshake, which is how a reader ends up reading documentation > that does not match what they just installed (Astra release review, 2026-09-19). The memory lands in `~/.al-buddy-memory/brain.db`; set `AL_BUDDY_MEMORY_DB` to put it somewhere else. Give it an **absolute path** — a JSON config is not a shell, and a `~` in it is expanded by this server but not by everything else that may read the value. The audit trail goes inside that database, in `audit_events`, appended in the same transaction as the fact it describes — one hash chain however many assistants are running. Check it with `al-buddy-memory verify-audit ~/.al-buddy-memory/brain.db`. A server from before the table left one log per process in `brain.db.audit/`, and one before that a single `brain.db.audit.jsonl`; those are neither adopted nor extended (they attest to a period the table cannot, and vice versa) and the same command reports them alongside the table. `AL_BUDDY_MEMORY_AUDIT` still names a JSONL file to use instead — one process per file if you do. A `recall` result looks like this — every field an agent needs to decide how much to trust the fact: ```json { "id": "…", "text": "Lives in Tokyo", "provenance": "UserInput", "validFrom": "2026-06-01T00:00:00Z", "validTo": null, "current": true, "confidence": 1, "supersededBy": null, "derivedFrom": [], "recordedAt": "2026-06-01T00:00:00Z", "origin": { "app": "claude-desktop", "appVersion": "1.2.3", "via": "mcp" }, "retiredBy": null } ``` `origin` is which assistant wrote the fact, taken from the MCP handshake rather than from the model, and `retiredBy` is which one closed it — so on memory genuinely shared between assistants, a retired fact is an event with an actor. Both are `null` when the host knew nothing. **`remember` answers "what might this replace?"** It returns the fact it stored plus `mayConflictWith`: up to three current facts that read like the new one, each with its id, text and `validFrom`. ```json { "id": "…", "text": "Lives in Berlin", "…": "…", "mayConflictWith": [ { "id": "…", "text": "Lives in Tokyo", "validFrom": "2026-06-01T00:00:00Z" } ] } ``` Nothing is retired automatically — the client reads them and calls `invalidate` on the ones that stopped being true. That is the whole invalidate-never-overwrite loop, and until 0.4.2 nothing in the surface ever prompted it. Treat the list as facts to *read*: they are the best lexical matches, not conflicts that were proven. The **first** `recall` of a connection also returns the pinned tier — the person's standing rules — as a second content block, so the tier that claims to be in every prompt gets there without spending any of the 512-character handshake. Tools: `remember`, `recall`, `history`, `explain`, `invalidate`, `pin`, `unpin`, `pinned`, `mental_model`, `define_mental_model`, `export`, and for team memory `promote` and `withdraw` (the personal server's policy refuses them; see [docs/policies/scopes.md](docs/policies/scopes.md#promotion)). `mental_model` reads a standing question's pre-written answer with its freshness and evidence (the host refreshes answers on its own schedule). `explain` answers why a fact is believed: who asserted it, when it was true, what ended it, and for a conclusion the exact words it rests on, each checked now. `remember` takes at most 4,000 characters and `pin` 500; a `recall` query 1,000, an `invalidate` reason 500, and an id 128. `pin` refuses text that reads like a secret, because the server would store it Sensitive and no assistant could see it. `invalidate`'s `replacedBy` must name a fact the caller can see. SQLite on disk, no service, no key. The tool bodies are a plain function over a `MemoryStore` (`governanceTools(...)`, exported from `al-buddy-memory/mcp`), so they run against any backend and test without a transport; `pin` there needs `pinning: true`, which only a host on the user's own machine should pass. **Recall is hybrid** — keyword and meaning, with the query's time and counting cues read (`expand`), the setting the LongMemEval result was measured with. The server starts at once on keyword recall and loads the on-device embedding model beside it; from the call after it loads, recall uses both. The model (all-MiniLM-L6-v2, about 90 MB) is downloaded once from the Hugging Face hub to `~/.al-buddy-memory/models` (`AL_BUDDY_MEMORY_MODEL_CACHE` moves it) and then runs offline; facts stored before it was there are embedded in the background, up to 5,000 per start (`AL_BUDDY_MEMORY_INDEX_LIMIT`). Where it cannot load — `@huggingface/transformers` not installed, no onnxruntime binary for the platform (Intel Macs, for one), no network on first run — recall stays keyword-only and the server says so once on stderr. The model loads and runs in a worker thread, and the background pass gives the event loop back every 20 ms, so the server keeps answering while it indexes. `AL_BUDDY_MEMORY_SEMANTIC=off` never loads it. The cross-encoder reranker is not switched on: on the full LongMemEval run it moved the overall score by nothing (92.6% with and without) and costs a second model. **Export.** The `export` tool returns the memory in the [portable format](docs/portable-format.schema.json) — inline when it is under 50 KB, otherwise written to an absolute `.json` path you name that does not exist yet (it never overwrites). It exports what the assistant could recall: the policy keeps Sensitive and Sealed facts out. Your complete backup, every fact included, is the command line's, and import restores one: ```sh al-buddy-memory export --out ~/memory-backup.json # or --format markdown for a readable mirror al-buddy-memory import ~/memory-backup.json # checks the whole file first; safe to re-run ``` Both read `AL_BUDDY_MEMORY_DB` (or `--db`) like the server. `al-buddy-memory context` prints the short briefing the Claude Code plugin shows at session start, and `al-buddy-memory status` the counts, the database path, the last write and whether semantic search is available. ### As a remote connector in Claude and ChatGPT `al-buddy-memory-http` serves the same tools over Streamable HTTP for one owner, signed in with OAuth 2.1 (PKCE S256, dynamic client registration, rotating refresh tokens). Each tool declares `title`, `readOnlyHint`, `destructiveHint` and `openWorldHint`, the annotations both assistant directories ask for. It listens on 127.0.0.1; put a tunnel in front (e.g. `tailscale funnel --bg --https=8443 http://127.0.0.1:8787`) and give the apps its URL: ```sh echo 'a long passphrase' | al-buddy-memory-http --set-passphrase # stores a scrypt hash, 0600 AL_BUDDY_MEMORY_PUBLIC_URL=https://you.example.ts.net:8443 al-buddy-memory-http ``` In Claude: *Customize → Connectors → Add custom connector*, URL `https://…:8443/mcp`. In ChatGPT: *Settings → Security and login → Developer mode*, then add the same URL. Each app opens a consent page once; the passphrase allows it. Only hashes of codes and tokens are kept on disk. Five wrong passphrases from one app lock that app out for 15 minutes, and twenty from all apps within 15 minutes lock every app out; each lock is logged. A fact an app writes records it as the fact's `origin`, by the name the app registered with and the consent page showed, and the audit event for each call names it too. Two things stay on your machine: `export` answers inline and never writes a file, and `pin` is refused, because a pinned rule rides every assistant's prompt; pin from an assistant on the local server (`al-buddy-memory mcp`), and the remote apps get those rules with their first `recall` after signing in, and again with the next one after the rules change. For an organisation, the same server can be signed in by your own identity provider instead: [docs/SIGN-IN.md](docs/SIGN-IN.md). ## Use it as a Claude Code plugin The repository is also a Claude Code plugin marketplace. In Claude Code: ```text /plugin marketplace add flytomoon/al-buddy-memory /plugin install al-buddy@al-buddy ``` or from a shell, `claude plugin marketplace add flytomoon/al-buddy-memory` and `claude plugin install al-buddy@al-buddy`. There is nothing to configure. Getting started, commands and troubleshooting: [docs/claude-plugin.md](docs/claude-plugin.md) (and [albuddy.com/claude.html](https://albuddy.com/claude.html)); privacy: [docs/PRIVACY.md](docs/PRIVACY.md). Commands, all under the plugin's `al-buddy:` namespace: `/al-buddy:recall `, `/al-buddy:remember `, `/al-buddy:forget ` (retire it; kept in history), `/al-buddy:status`, `/al-buddy:export [file]`, `/al-buddy:import ` and `/al-buddy:help`. The plugin ([plugin/](plugin/README.md)) brings: - the governance MCP server above, started with `npx` and pinned to an exact version — the first start downloads the package (about 500 MB installed, most of it the optional model runtime); - two skills: `recall` (search memory before answering about past work, decisions, people or preferences) and `remember` (store durable facts — never secrets, credentials or small talk); - a SessionStart hook that adds a briefing of at most 2,000 characters when a session starts, is cleared or is compacted: your pinned rules, facts mentioning the project folder's name, and the most recently learned facts, as the assistant is allowed to see them. It never installs anything itself, so the very first session — while the server's first install runs — gets no briefing. **Where the data lives.** `~/.al-buddy-memory/brain.db`, created when the server first starts; set `AL_BUDDY_MEMORY_DB` to an absolute path in the environment Claude Code starts from to keep it elsewhere. The embedding model sits beside it in `~/.al-buddy-memory/models`. Nothing leaves the machine except the two one-time downloads (the npm package and the model). **Export, back up, remove.** Ask Claude to export your memory (the `export` tool), or run `al-buddy-memory export --out .json` for the complete owner backup. Uninstalling the plugin (`/plugin uninstall al-buddy@al-buddy`) leaves your memory in place; delete `~/.al-buddy-memory/` to remove it, after exporting it if you want to keep it. **The Claude desktop app.** A one-click extension, `al-buddy-memory.mcpb` (macOS), is attached to each [GitHub release](https://github.com/flytomoon/al-buddy-memory/releases/latest) from the next one on, built by `npm run build:mcpb`. It carries the server and its dependencies, including the SQLite engine compiled for the Node.js built into Claude Desktop, and it uses the same `~/.al-buddy-memory/brain.db` as the plugin, so the two apps share one memory. Steps in [docs/claude-plugin.md](docs/claude-plugin.md#the-claude-desktop-app). **Hosts other than Claude Code.** The plugin is the same server: any MCP client can use the `npx` configuration in [The governance MCP server](#the-governance-mcp-server). Codex and the ChatGPT desktop app share one configuration; from a shell, `codex mcp add al-buddy-memory -- npx -y al-buddy-memory mcp`, or in the desktop app *Settings → MCP servers → Add server → STDIO* with that command. Details in [docs/claude-plugin.md](docs/claude-plugin.md#codex-and-the-chatgpt-desktop-app). `al-buddy-memory status` prints how many facts are current, retired and pinned, where the database is, when it was last written, and whether semantic search can run on this machine (counts only). Needs Node.js 22 or later on the `PATH`; Node 20 works only where `better-sqlite3` can compile from source (it ships no Node 20 binary). ### Make it automatic in the Claude apps In the Claude desktop app, and on the web or phone through a [remote connector](#as-a-remote-connector-in-claude-and-chatgpt), one paste makes capture always on: Claude saves every exchange that has substance without being asked — the question, its answer or recommendation and why, and the decision — and saying "remember" only makes it save at once. In the app's settings, paste this where Claude asks for your personal preferences (the instructions it follows in every chat): ```text Use Al Buddy memory automatically, without being asked. At the start of each conversation, call recall for my pinned rules and anything relevant. After each exchange that has any substance, save it with remember: the date, what I asked or said, what you answered or recommended and why, and what we decided. Mark my own words as UserInput and your answers or recommendations as AIInferred. Save the substance and the reasoning, not filler or small talk. If I say remember, save it right away. Never save secrets or passwords. When something earlier stops being true, invalidate it instead of saving a contradiction. ``` Then, where the app sets Al Buddy's tool permissions (or when it first asks to use a tool), let `recall` and `remember` run without asking each time, and keep `invalidate`, `unpin` and any tool that erases asking you first. Why: in the Claude apps a connector is used only when something tells it to, and there is no hook that captures a conversation automatically — in our test on 11 October 2026, Claude saved to memory only when asked, even with the server's own instructions in place. This instruction is what makes it continuous. The same step with a copy button: [albuddy.com/claude.html](https://albuddy.com/claude.html#automatic). ## Roadmap - [x] The spec and the portable format, published and versioned (this repo) - [x] `al-buddy-memory conformance `: score any memory export on provenance, invalidation and portability, with adapters for block-style agent files and flat memory records (v0.2.0) - [x] The governance MCP server: a memory server that returns provenance and validity with every fact (v0.2.0) - [x] Governance hooks with an audit trail and three sample policies; provenance immutable at runtime; measured limits at 100k facts (v0.3.0) - [x] The audit event committed in the same transaction as the fact it describes, as one chain many processes share (v0.4.2) - [x] A comparison table and a live paste-your-export demo (albuddy.com) - [x] Transaction time, the second half of bi-temporal: "what did we believe at X", including a fact held wrongly and later corrected (v0.5.0) - [x] A Postgres backend behind the same `MemoryStore` interface, for multi-tenant and hosted deployments (SQLite stays the local-first default; the backend runs the shared conformance suite) - [x] Framework integrations: Vercel AI SDK, LangChain/LangGraph, Mastra (v0.7.0) ## Development ```sh npm ci npm run check # typecheck, tests, and the browser bundle — exactly what CI runs ``` The tests include a behavioural conformance suite every backend runs against itself.