{
  "markdown": "# lean-memory\n\n[![test](https://github.com/Wuesteon/lean-memory/actions/workflows/test.yml/badge.svg)](https://github.com/Wuesteon/lean-memory/actions/workflows/test.yml) [![PyPI](https://img.shields.io/pypi/v/lean-memory)](https://pypi.org/project/lean-memory/) [![Wuesteon/lean-memory MCP server](https://glama.ai/mcp/servers/Wuesteon/lean-memory/badges/score.svg)](https://glama.ai/mcp/servers/Wuesteon/lean-memory)\n\nEmbedded, local-first agent memory. No server, no daemon, no mandatory cloud key.\n\n> **Status (2026-07):** first public release line (0.2.1) is live on PyPI and\n> the MCP Registry (MCP-first launch); the Claude Code plugin ships in this\n> repo (marketplace listing pending).\n> Roadmap and rationale: `docs/superpowers/specs/2026-07-08-strategic-direction-design.md`.\n> Public benchmark runs (LongMemEval/LoCoMo) are deliberately deferred until\n> after launch; the harness is complete (`bench/phase2_*.py`) and the engine\n> flaws it exposed are fixed — see `docs/phase2-learnings.md`.\n\n```python\nfrom lean_memory import Memory\n\nmem = Memory(root=\"./data\")\n\nmem.add(\"user-42\", \"I work at Acme Corp.\")\nmem.add(\"user-42\", \"I now work at Globex.\")          # supersedes Acme automatically\n\nmem.search(\"user-42\", \"where does the user work?\")   # → \"I now work at Globex.\"\n```\n\n![lean-memory quickstart](https://raw.githubusercontent.com/Wuesteon/lean-memory/main/docs/assets/quickstart.gif)\n\nFacts are extracted from natural language, stored in a per-namespace SQLite file, and retrieved with hybrid dense+sparse search. Old facts are never deleted — they're superseded and queryable at any past point in time.\n\n## Install\n\n```bash\npip install lean-memory\n```\n\nRuns fully offline out of the box. Optional extras unlock real model quality:\n\n| Extra | What it adds |\n|---|---|\n| `lean-memory[models]` | Real embedder + reranker (Qwen3-0.6B + Ettin-32M) |\n| `lean-memory[extract]` | GLiNER2 candidate generation for richer extraction |\n| `lean-memory[llm]` | Ollama-backed LLM typing pass |\n| `lean-memory[mcp]` | MCP server bridge for Claude Desktop / Claude Code |\n| `lean-memory[examples]` | Terminal demo agent (requires `anthropic` SDK) |\n\n## Quickstart\n\n```python\nfrom lean_memory import Memory\n\nmem = Memory(root=\"./data\")   # one SQLite file per namespace, stored under ./data/\n\n# Store facts in natural language\nmem.add(\"alice\", \"I work at Stripe.\")\nmem.add(\"alice\", \"I now work at Vercel.\")   # supersedes Stripe automatically\n\n# Retrieve — the superseded Stripe fact drops out; only the current one is returned\nresults = mem.search(\"alice\", \"what does Alice do for work?\", k=3)\nfor hit in results:\n    print(hit.fact.fact_text, hit.final_score)\n# → I now work at Vercel. 0.89\n\n# Point-in-time query — what was true at a specific moment?\nmem.search(\"alice\", \"employer\", as_of=1_700_000_000_000, is_latest_only=False)  # epoch ms\n\n# Always close when done (flushes WAL)\nmem.close()\n```\n\n## Demo Agent\n\nA terminal chatbot showing the full memory loop — add, retrieve, supersede, restart.\nThe demo script lives in the repo (it is not installed with the package):\n\n```bash\ngit clone https://github.com/Wuesteon/lean-memory && cd lean-memory\npip install -e '.[examples]'\nexport ANTHROPIC_API_KEY=sk-ant-...\npython examples/chat.py                  # uses offline stubs by default\npython examples/chat.py --namespace bob  # separate memory tenant, persists across restarts\n```\n\nNo API key? The demo still runs — it echoes the retrieved memory context instead of calling Claude, so you can watch the engine work offline.\n\n## MCP Server — memory for Claude Code / Claude Desktop\n\nGive any MCP agent persistent local memory: three tools (`memory_add`,\n`memory_search`, `memory_clear`), one SQLite file per namespace, nothing\nleaves your machine.\n\n```bash\npip install 'lean-memory[mcp,models,extract]'\n```\n\n> First run downloads three open models (~2.0 GB total: Qwen3-Embedding-0.6B\n> + Ettin-32M reranker for retrieval, plus GLiNER2-base (~0.8 GB) for real\n> extraction — all ungated). Pre-warm once so your MCP client never waits on\n> a download:\n>\n> ```bash\n> python -c \"from lean_memory.embed.sentence_transformer import SentenceTransformerEmbedder; \\\n> from lean_memory.retrieve.rerank import CrossEncoderReranker; \\\n> SentenceTransformerEmbedder().embed_one('warm'); CrossEncoderReranker().score('warm', ['up']); \\\n> from lean_memory.extract.gliner_extractor import Gliner2Generator; from lean_memory.types import Episode; \\\n> Gliner2Generator().generate(Episode(namespace='w', raw='I work at Acme.', t_ref=0, source='user'))\"\n> ```\n\n**Claude Code:**\n\n```bash\nclaude mcp add lean-memory -- lean-memory-mcp\n```\n\n**Claude Desktop** — add to `mcpServers` (or copy `examples/mcp_config.json`):\n\n```json\n{ \"lean-memory\": { \"command\": \"lean-memory-mcp\", \"env\": { \"LM_DATA_ROOT\": \"~/.lean_memory\" } } }\n```\n\nData root: `LM_DATA_ROOT` (default `~/.lean_memory`). Works offline-only too —\nthe server opportunistically upgrades each backend that its extra is installed\nfor (`[models]` → real embedder + reranker, `[extract]` → GLiNER2 extraction)\nand otherwise falls back to deterministic stub backends (fine for CI,\nsemantically meaningless for real use — install `[mcp,models,extract]`).\n\n> **What the optional `[llm]` extra buys.** The canonical `[mcp,models,extract]`\n> install has no LLM typing pass, so the ~15% of candidates that escalate —\n> almost all of them inferential (`derives`) facts — are typed by a\n> deterministic stub instead of a model. Assertional facts are unaffected;\n> inference-type facts are effectively second-class on the default path. Adding\n> `[llm]` (a local Ollama model) upgrades that escalated tier to real\n> constrained typing. See ARCHITECTURE.md → Known Limitations.\n\n## Sleep-time maintenance & review\n\nMemory accumulates cruft: the same fact restated a dozen ways, old records that\nnever come up, clusters begging to be summarized. lean-memory cleans it up the\nway sleep consolidates memory — an **offline job you run off-hours** that dedupes,\nsummarizes, and demotes low-value records, then hands you the judgment calls to\nclick through the next morning, in the web console **or conversationally in\nClaude Code**.\n\n**The CLI** (`lean-memory-maintain`) is the primary trigger. It is **dry-run by\ndefault** — it reports what it *would* do and writes nothing:\n\n```bash\nlean-memory-maintain --root ~/.lean_memory              # dry-run: report only, zero writes\nlean-memory-maintain --root ~/.lean_memory --apply      # auto-apply safe transforms + stage the rest\nlean-memory-maintain --root ~/.lean_memory --auto-only   # with --apply: ONLY the provably-safe band, stage nothing\nlean-memory-maintain --root ~/.lean_memory --json        # one machine-readable object, stable keys\n```\n\n`--root` defaults to `$LM_DATA_ROOT`; add `--namespace NS` to run a single\nnamespace instead of every `*.db` under the root. **Overnight, on a schedule** —\none crontab line runs the safe band nightly at 3am and stages everything else\nfor you:\n\n```cron\n0 3 * * *  lean-memory-maintain --root ~/.lean_memory --apply >> ~/.lean_memory/maintain.log 2>&1\n```\n\n**Next-morning review in Claude Code.** Judgment calls (near-duplicate merges,\nsummaries, evictions) are staged as *proposals* — nothing changes in stored\nmemory until you approve. Run the `/review-memory` plugin command (or invoke the\n`review-memory-maintenance` MCP prompt on the console server) and Claude walks\nyou through the queue,\ngrouped by entity with before/after evidence, recording only the verdicts you\ngive. Four MCP tools back it — `memory_maintenance_run` (dry-run by default,\nlike the CLI), `memory_maintenance_status`, `memory_review_queue`, and\n`memory_review_decide` — available on the core `lean-memory-mcp` server and both\nconsole MCP surfaces. Set `LM_MAINT_AUTO=1` to opt into a background auto-run\n(safe band only) on the first tool call of a stale namespace; it is off by\ndefault.\n\n**Or click through it in the console.** The memory console ships a **Review**\npage: the same queue grouped by entity, before/after evidence per proposal\n(both texts + cosine for near-duplicates, sources + proposed text for\nsummaries, score evidence for evictions), with Approve / Keep /\nEdit-then-approve / Promote verbs, batch-approve per entity, and a\nrun-maintenance button (dry-run by default; apply sits behind a confirm). Both\nfrontends drive the same proposal store with compare-and-set decisions, so\ndeciding in one place shows up as \"already decided\" in the other instead of\ndouble-applying.\n\n**The safety story in one paragraph.** Nothing is ever deleted — maintenance\nonly appends, retires (the same `superseded_by` flip ordinary supersession\nuses), or demotes to a cold tier, so your full history stays queryable as-of any\npast point in time, bit-for-bit identical at the store predicate and pinned by\nexecutable tests. Only two transforms auto-apply: exact-duplicate retirement and\na strict eviction band; everything judgmental is staged for a human, and an\nunreviewed proposal **expires** after 30 days rather than auto-applying —\nsilence is never consent. Cold-demoted facts stay reachable via `as_of` queries\nand `search(..., include_cold=True)`, and promotion back to the hot tier is\nexplicit-only, so a read never durably changes what your agent sees.\n\n## Real Model Quality\n\nThe default backends are offline stubs — deterministic and dependency-free, but semantically meaningless. Swap in real models for production-quality retrieval:\n\n```bash\npip install 'lean-memory[models]'\n```\n\nWith `Qwen3-Embedding-0.6B` + `Ettin-32M` reranker, retrieval jumps from 1/5 to 4/5 on the internal benchmark with zero code changes.\n\n> For benchmark results, architecture decisions, and implementation status see [ARCHITECTURE.md](https://github.com/Wuesteon/lean-memory/blob/main/ARCHITECTURE.md).\n\n## How It Works\n\nEach `mem.add()` call runs a 4-pass hybrid extraction pipeline:\n\n1. **Rules** — regex + dateparser for common predicates (`works_at`, `lives_in`, …)\n2. **GLiNER2** — open-vocabulary NER candidate generation (offline stub by default)\n3. **Router** — recall-biased escalation: low-confidence, coreference, and inferential (`derives`) facts escalate to the LLM pass\n4. **LLM typing** — constrained relation typing via a local Ollama model (stub by default)\n\nContradiction detection runs cheap-first (slot match → cosine → token subsumption → LLM). Conflicting facts are superseded, not deleted — the old fact stays with `is_latest=False` and a `superseded_by` pointer.\n\nEntity identity resolves on a normalized name key — NFC + Unicode case-fold + whitespace collapse — so `Acme`, `ACME` and `acme` are one subject and dedupe/supersession actually apply to them. It is a full Unicode fold, not SQLite's ASCII-only `NOCASE`: `Café`/`CAFÉ` and `ЖУК`/`жук` collate too. Punctuation and diacritics are deliberately *not* folded (`Yahoo!` ≠ `Yahoo`, `Café` ≠ `Cafe`), and the display name keeps the first spelling you used. The trade-off: two genuinely distinct subjects differing only by case (`Mercury` the planet vs `mercury` the metal) collate into one — nothing is deleted, and the retired fact stays readable via `search(as_of=…, is_latest_only=False)`.\n\nRetrieval fuses two-stage Matryoshka dense search (256-dim coarse KNN → full-dim (1024 for the default embedder) re-score) with BM25 sparse, applies RRF fusion, reranks with a cross-encoder, and scores with salience-decay (`0.6·relevance + 0.2·recency + 0.2·importance`).\n\n## Develop\n\n```bash\ngit clone https://github.com/Wuesteon/lean-memory\ncd lean-memory\npython -m venv .venv && source .venv/bin/activate\npip install -e '.[dev]'\npytest -q    # full offline suite, no downloads\n```\n\n## Project Layout\n\n```\nsrc/lean_memory/\n  memory.py                   Memory facade — the public API\n  types.py                    Episode / Fact / RetrievedFact types\n  store/                      Store interface + SqliteStore (vec0 + FTS5)\n  embed/                      Embedder interface, FakeEmbedder, SentenceTransformer\n  extract/                    4-pass extraction pipeline\n  retrieve/                   Reranker interface, retrieval pipeline\nexamples/\n  chat.py                     Terminal demo agent\n  mcp_config.json             Drop-in MCP client config\ntests/                        offline test suite\nbench/                        Retrieval quality + BET-2 ablation harnesses\n```\n\n## License\n\nApache-2.0\n\n<!-- mcp-name: io.github.Wuesteon/lean-memory -->\n\n",
  "bytes": 12422,
  "sha": "a0401780b86eab9a13fd02fe4383f70c3422c20cee6274d4117919c68ec1421d",
  "repo_slug": "wuesteon/lean-memory",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_wuesteon_lean_memory_878f85fb/readme"
}