SEQ 00 hash a1f0c2d4 prev 00000000
Aristotelian Persuasion · Exordium

Local-first memory for agents Single Rust Binary

Every fact it holds, and every fact it decided not to hand over.

MemVault stores what you give it and explains what it hands back. One binary, no service to operate, no cloud egress, no network unless you ask for one.

$ memvault --data-dir ./my-memory search --namespace project --query "deploy script" --max-tokens 20 retrieval_id: f96a229c-4d63-4db8-834d-1b98877611dd
fact_id ann_rank ann_dist bm25_rk bm25_score rrf decay_wt final outcome tokens
34e2001b-1603-45c5-abbe-f5fbcf31d4a8 0 0.3518 0 2.2232 0.0328 1.0000 0.0328 Injected 15
fdaa5149-440e-4a42-b2e6-d67653f2871d 1 0.6311 - - 0.0161 1.0000 0.0161 CutByBudget 17
b5aa16a8-aa14-48cb-917d-1bc5d048f184 2 1.0000 - - 0.0159 1.0000 0.0159 CutByBudget 11
Selected: Injected
Row 1 (34e2001b): Ranked #0 in vector search (dist: 0.3518) and #0 in keyword search (BM25: 2.2232). Fused RRF = 0.0328, decay discount 1.0000. Cost: 15 tokens within budget (20). Handed to agent.
Click any row to inspect decision math
ann_rank / ann_dist
Where the fact placed on the vector axis, and how far out. A dash means it never surfaced there at all.
bm25_rk / bm25_score
The same, for the keyword axis. A row ranked on both axes was found two different ways.
rrf
The two ranks fused. Ranks only, never raw scores, since a cosine distance and a BM25 score are not on the same scale.
decay_wt
How much age discounted it. 1.0000 is no discount, and a pinned fact never decays.
final
rrf multiplied by decay_wt. This is what the ordering is by.
outcome
Injected reached the agent. CutByBudget lost to the token budget, CutByK to the k limit, FilteredByTime was no longer valid at query time.
tokens
What that fact costs you in context.
SEQ 01 hash 6b39e07c prev a1f0c2d4
The Human Struggle · Narratio

The Macro Problem Pathos

Stateless models, opaque cloud caches, and silent amnesia.

Language model calls are pure functions of their context window. Everything learned in a previous session vanishes unless written down outside. The common workarounds fail silently: truncation discards critical context, rolling summarisation burns expensive tokens while blurring specifics, and giant context windows drive latency and degrade middle-attention.

When teams adopt cloud memory microservices, they trade amnesia for three harder problems:

The Cloud Memory Black Box Failure Modes
  • ×
    Staleness & Obsoleted Truth: Models time as a single timestamp. When a project lead changes or a deploy script moves, stale facts confidently surface forever.
  • ×
    Total Opacity: Returns a list of facts with no candidate ranks, no rejected items, and no explanation of what the token budget cut. Debugging is guesswork.
  • ×
    50-Second Tail Latency: Putting LLM extraction and reranking on the hot path introduces catastrophic p95 tail latencies (measured up to 59s on LOCOMO benchmarks).
  • ×
    Privacy & Host Violations: Demands that desktop agents and sandboxed coding assistants ship proprietary code and private databases off-host to a third-party service.
MemVault Local Ledger Deterministic Fix
  • ✓
    Bitemporal Modelling: Two clocks on every fact: Valid Time (when true in the world) and Belief Time (when learned). Know what was true vs. what the agent believed.
  • ✓
    Explainable by Construction: Every retrieval returns the full candidate matrix, vector & BM25 ranks, decay discounts, and the exact reason an item was kept or cut.
  • ✓
    3ms Predictable Latency: No LLM on the hot path. Retrieval is bounded by CPU and disk, not inference queues or external API limits. Zero per-operation cost.
  • ✓
    100% Local-First: Single static binary embedded in-process or over stdio. Zero network calls, zero external databases, 100% host containment.
SEQ 02 hash d84c1fa2 prev 6b39e07c
First Principles & Architecture · Logos

How It Works Divisio

Two indexes, fused by rank, packed to a budget.

A vector index (usearch) and a keyword index (tantivy) answer every query independently. Their ranks are fused via Reciprocal Rank Fusion (RRF), weighted by decay, and packed to fit the token budget you set.

The write path only ever appends. The read path queries both indexes, fuses their ranks, and writes its own decision back into the ledger, which is why an old retrieval can be replayed instead of re-run.

The write path and the read path memory_write appends to the hash-chained ledger, and the ledger builds both the vector index and the keyword index. memory_search queries the two indexes; their ranks are fused, weighted by decay and packed to a token budget before the context reaches the agent. That decision is appended to the ledger as a retrieval record, which memory_explain replays by retrieval_id. memory_write hash-chained ledger redb, append-only memory_search vector index usearch keyword index tantivy RRF fusion decay + budget context handed to the agent memory_explain retrieval record replay by retrieval_id
SEQ 03 hash 2e75b9c1 prev d84c1fa2
Provable Guarantees · Ethos

Cryptographic Erase Confirmatio

Erase a fact and the chain still verifies.

An append-only ledger appears to forbid deletion, while privacy compliance mandates it. MemVault resolves this through cryptographic erase: every record is encrypted with its own key.

When you call forget, the engine destroys the key. The ciphertext becomes mathematically unrecoverable, the content hash and sequence position remain intact, and verify confirms the ledger was never tampered with.

A ledger record before and after forgetting The same ledger record, before and after a forget. The record keeps its sequence number, its fact id and its content hash; only the plaintext goes, because the key that decrypted it was destroyed. Verify still reports the chain intact from seq 0. as written seq 0 kind Assert fact_id 34e2001b content_hash 76d44f5d ciphertext_len 56 the deploy script lives in ops/deploy.sh forget --reason "customer deletion request" after: the key is destroyed seq 0 kind Assert fact_id 34e2001b content_hash 76d44f5d ciphertext_len 56 undecryptable (key destroyed or never existed) memvault verify chain verified from seq 0

The record does not move and the hash does not change, so the history stays honest. Anyone holding the original plaintext can still prove what this record said; nobody can recover it from here.

SEQ 04 hash 90fa3d68 prev 2e75b9c1
Temporal Integrity · Logos

Bitemporal Mechanics Two Clocks

What was true, and what the engine believed.

Two clocks, both queryable. Ask what held in the real world at a moment (Valid Time), or ask what MemVault believed held at that moment (Belief Time). They are not the same question, and a memory engine that conflates them cannot explain past decisions.

Valid time against belief time Two axes. The horizontal one is valid time, when the fact held in the world. The vertical one is belief time, from the moment MemVault was told. The shaded block is one fact on both axes, and a query fixes a point on each axis: asking about moment T as the engine understood things at moment U. staging runs postgres 16 T U valid time: when the fact held belief time: when we were told as_of(valid T, belief U)

Move T and you ask a different question of the world. Move U and you ask the same question of an earlier MemVault. A memory that collapses the two axes cannot tell you which one changed.

SEQ 05 hash 47c8e215 prev 90fa3d68
Ergonomics & Integration · Logos

Run It Four Surfaces

Four ways in, one engine behind them.

An MCP server over stdio, the standalone CLI, Python in-process, and an optional gRPC surface. Every one of them returns the exact same provenance.

MCP

Point your client at memvault-server with a data directory as its one argument. Or run memvault mcp-config to generate the absolute path JSON automatically. Save this as .mcp.json in your project root for Claude Code / Cursor, or merge it into claude_desktop_config.json for Claude Desktop:

{
  "mcpServers": {
    "memvault": {
      "command": "/absolute/path/to/memvault/target/release/memvault-server",
      "args": ["/absolute/path/to/my-memory"]
    }
  }
}

Use absolute paths for both. The server inherits whatever working directory the client happened to launch it from, which is rarely the one you expect.

Restart the client and the agent has seven memory tools:

memory_write Assert a fact. Pass an existing fact_id to supersede that fact instead; it has to be one of this namespace’s own.
memory_search Hybrid retrieval. Returns the injected facts with their content, best first, plus the full provenance table as rows. Every tool answers in structured JSON.
memory_get One fact’s current version by id.
memory_as_of What was true, or what the engine believed, at a given moment.
memory_supersede Close a fact’s interval without asserting a replacement.
memory_forget Cryptographic erase.
memory_explain Reconstruct any past retrieval from its retrieval_id.

On startup the server runs recovery and verifies the chain, logging the result to stderr. Stdout is the protocol channel and carries nothing else.

SEQ 06 hash bb1d6094 prev 47c8e215
Intellectual Honesty · Refutatio

Known Limits Refutatio

Stated plainly, because each one otherwise looks like a bug.

Embeddings come from outside MemVault runs no model. The MCP and Python surfaces accept an embedding; the server can fetch one from an OpenAI-compatible provider you configure, and without either it falls back to keyword-only retrieval.
Namespaces share a candidate pool A search never returns another namespace’s facts, but a namespace holding far more facts than its neighbours can crowd them out of the pool and cost them recall.
Token counts are estimates by default Ciphertext bytes / 4, not a tokenizer. Close enough for English prose, drifting on code. Build with --features tokenizer for a real cl100k_base count; the vocabulary is compiled in and no model runs.
Decay measures from a fact’s own start Not from last access, so retrieval does not yet reinforce a fact against decay.
No retrieval-quality numbers yet The LongMemEval and LOCOMO harnesses run with a real embedding model if you give them one, but no scored run has been published. The figures here are latency and cost.
The Climax & Action · Peroratio

Install

Hand it to your agent in sixty seconds.

Paste one prompt into Claude Code, Cursor, or Claude Desktop and let it clone, build, wire up the MCP config, and prove the round trip works.

Install MemVault as an MCP server for this project.

Prerequisites, check both before you start and stop if either is missing:
Rust 1.85 or newer (the workspace is edition 2024), and a C++ toolchain,
because the vector index builds from source. (If either is missing, the
release page at https://github.com/swap-mitra/memvault/releases has a
prebuilt archive per platform; unpack it and skip step 1.)

1. git clone https://github.com/swap-mitra/memvault
   cd memvault
   cargo build --release

2. If the build fails, stop and report the error to me verbatim. Do not
   improvise a fix, and do not substitute another memory tool.

3. Write .mcp.json in my project root. Use absolute paths for both the
   binary and the data directory, because the server inherits whatever
   working directory the client launched it from:

   {
     "mcpServers": {
       "memvault": {
         "command": "<abs path>/target/release/memvault-server",
         "args": ["<abs path>/my-memory"]
       }
     }
   }

4. Tell me to restart the client, then prove the round trip: call
   memory_write with namespace "project" and some fact about this repo,
   call memory_search for it, and report the retrieval_id and each
   candidate's outcome you got back.

It clones, builds, writes the config with absolute paths, and then proves the round trip rather than declaring victory. If the build fails it is told to stop and hand you the error.

chain verified from seq 0 MIT OR Apache-2.0 · github.com/swap-mitra/memvault