Skip to main content
Glama

mnemos

CI

A persistent memory framework for LLM agents. It gives an agent episodic memory (what was said, and when), semantic memory (distilled facts about the user), procedural memory (which retrieval strategy actually works for this user), and reflection (consolidating and forgetting facts over time) — backed by a choice of three storage backends, exposed over a FastAPI surface, and observable through a React dashboard. It ships with a benchmark that measures whether the agent actually still remembers something 30 simulated days after it was mentioned once — not just whether the demo looks good.

Most "agent memory" projects on GitHub are a vector store wrapped around a chat loop with no way to tell if it's actually working. This one tries to answer that question directly: seed a fact once, ask about it later, measure recall and answer accuracy with and without memory, and report the delta.

See it work

Recorded live, against a real running dashboard, a live Postgres backend, and a real hosted LLM (Groq, Llama 3.3 70B) — not staged, not a mockup. Demo data was seeded across 7 simulated conversation sessions spanning 10 simulated days: a chat turn correctly recalls a fact from one earlier session (the user's dog) and a different fact from another earlier session (the deployment stack), then the walkthrough moves through retrieval scoring, memory stats, and the reflection audit trail.

Static screenshots of each view, for anyone who wants to pause and read the numbers:

It actually remembers across sessions, and shows exactly which memories it used and what it just learned.

Retrieval isn't a black box. Every retrieved memory ships with its real similarity, recency, and combined score — this is the actual ranking the chat loop uses, not a simplified illustration.

That transparency isn't just a dashboard view here — it's now a standalone library, memlens, that traces retrieval for any backend (mem0, raw pgvector, or mnemos itself): every candidate considered, its full score breakdown, and whether it was included or excluded and why.

The system prunes itself. Reflection has merged 4 pairs of near-duplicate facts into single consolidated statements (via real LLM tool-use calls) and decayed stale facts' confidence; a low-value fact that crossed the forget threshold is now archived, not silently deleted.

It knows what's in memory, and how that's changing. Counts by type and status, plus a growth-over-time chart, computed from the same demo data: 18 episodic memories, 18 active facts, 4 merged, 1 forgotten.

It adapts its own retrieval strategy. After live turns, the system already has empirical uses/success-rate data on which similarity/recency weighting works best for this user — the mechanism, not a placeholder.

Related MCP server: cortex-engine

Quickstart

git clone https://github.com/vishalbanwari26/mnemos && cd mnemos
cp .env.example .env   # fill in ANTHROPIC_API_KEY or GROQ_API_KEY + LLM_PROVIDER
docker compose up -d   # Postgres + pgvector
uv sync --extra dev
uv run alembic upgrade head
uv run python -m mnemos.cli.demo   # terminal chat; restart it to test cross-session recall

Not on PyPI yet, so git clone is the install for now. No Docker, want the full HTTP API + dashboard, or want to try the Qdrant/Neo4j backends instead? See Running it below for all of that.

Why this exists

Chat agents without persistent memory forget everything between sessions. Ask ChatGPT-style tools about something you told them yesterday and, unless it happened to land in a fixed context window, it's gone. The interesting systems question isn't "can I bolt a vector DB onto an LLM" — it's how do you decide what to keep, how do you retrieve it cheaply and correctly, how do you forget responsibly, how do you adapt your own retrieval strategy from feedback, and how do you prove any of it is working over time, not just in a single session.

This project started as a deliberately scoped v1 — two memory types, one storage backend, one retrieval strategy, a benchmark — to prove the core loop actually works before building anything else on top of it. It then grew into the full system: procedural memory, reflection/forgetting, three interchangeable storage backends, and a dashboard. See What's intentionally out of scope for what's still not here, and why.

Architecture

flowchart LR
    U[User message] --> CM[ConversationManager]
    CM -->|1. choose strategy| PM[ProceduralMemory]
    CM -->|2. recall| ME[MemoryEngine]
    ME --> RET[MemoryRetriever]
    RET -->|similarity + recency| SB[(StorageBackend:<br/>Postgres / Qdrant / Neo4j)]
    CM -->|3. build context| PB[prompt_builder]
    PB --> LLM[LLMClient]
    LLM -->|4. reply| CM
    CM -->|5. store turn| ME
    ME -->|6. periodically| EX[extraction]
    EX -->|distilled facts| SB
    CM -->|7. resolve prior turn| PM
    RE[ReflectionEngine] -.on demand.-> SB
  • MemoryEngine (src/mnemos/memory/engine.py) is the single seam everything else talks to. It wraps a StorageBackend and an EmbeddingClient. Nothing outside this file touches a backend or the retriever directly — that's what let procedural memory, reflection, and two extra storage backends get added later without rewiring every caller.

  • ConversationManager (src/mnemos/agent/conversation.py) is the per-turn loop: pick a retrieval strategy → recall relevant memories → build a system prompt → call the LLM → store the new episode → periodically extract semantic facts → resolve the previous turn's strategy outcome. The CLI and the API both call the same handle_message().

  • LLMClient (src/mnemos/llm/) is a small provider-agnostic interface (complete() with optional tool use). Two real implementations — AnthropicLLMClient and GroqLLMClient (the latter translates the Anthropic-shaped tool schemas used throughout the app into OpenAI's format internally, since Groq's API is OpenAI-compatible) — plus MockLLMClient, deterministic and offline, used by every test and by --llm mock dev runs.

  • EmbeddingClient (src/mnemos/embeddings/) is the same pattern for embeddings. Anthropic has no embeddings endpoint, so v1 defaults to a local sentence-transformers model (all-MiniLM-L6-v2) — free, deterministic, no second API key, and it keeps the benchmark reproducible without embedding-call cost or latency variance.

Data model

Episodic and semantic memory live in whichever StorageBackend is active (see below). Procedural memory and the reflection log are small operational metadata that always live in Postgres, regardless of STORAGE_BACKEND — they're not the "memory content" the backend comparison is about:

  • episodes — one row/node/point per conversational turn. occurred_at is a logical event time, separate from created_at (real insert time). That's what lets the benchmark simulate 30 days passing without waiting 30 days: seed data is inserted "now" but timestamped as if it happened weeks ago.

  • facts — distilled facts ("prefers FastAPI over Flask"), extracted from episodes via an Anthropic tool-use call, deduplicated against existing facts by cosine similarity before being written. Each fact tracks status (active / merged / forgotten), confidence, and last_reinforced_at — the fields reflection and forgetting operate on.

  • procedural_strategies (Postgres only) — per-user, per-strategy uses/successes counters.

  • reflection_log (Postgres only) — audit trail of every merge/decay/ forget action reflection has taken.

Retrieval

A query (the latest user message, or a benchmark probe question) is embedded and matched against both episodes and facts independently, then merged:

score = similarity * w_sim + recency_factor * w_recency   (episodic only)
score = similarity                                          (semantic facts)
recency_factor = exp(-age_days / 30)

w_sim/w_recency default to 0.7/0.3, but aren't fixed — see Procedural memory below for how they get chosen per turn. Semantic facts are treated as durable knowledge and ranked mostly on similarity; episodic turns get an exponential recency boost so recent context still surfaces even when it's a slightly weaker semantic match. Intentionally simple — a documented default, not a research contribution.

Storage backends

Episodic and semantic memory sit behind a StorageBackend interface (src/mnemos/storage/base.py) — nothing in MemoryEngine, agent/, or api/ knows which one is active. Three real implementations exist, selected via STORAGE_BACKEND=postgres|qdrant|neo4j:

  • Postgres + pgvector (default) — HNSW cosine similarity search.

  • Qdrant, embedded local mode (QdrantClient(path=...)) — no server, no Docker, a single directory-locked process. Qdrant combines payload filtering (user_id) with ANN search in one query.

  • Neo4j — a real graph, not a vector table wearing a costume: episodes and facts are nodes owned by a User via SAID/KNOWS edges, and a fact's provenance is a first-class DERIVED_FROM edge to its source episodes, instead of Postgres's source_episode_ids JSON array. Its native vector index has no pre-filter, so this backend over-fetches candidates and filters by user_id in Cypher afterward — a real, measurable cost, not hidden.

The same behavioral test suite (tests/test_storage_contract.py) runs against all three, so "swap the backend" is proven, not just asserted. A separate, LLM-free comparison script reuses the benchmark dataset to measure write/search latency and confirm recall parity:

uv run python -m benchmark.compare_backends

Backend

Write p50 / p95 (ms)

Search p50 / p95 (ms)

Recall@K

postgres

14.9 / 28.8

14.5 / 25.1

100%

qdrant

11.6 / 18.8

11.1 / 14.3

100%

neo4j

28.5 / 46.2

20.5 / 22.1

100%

All three retrieve the same relevant memories (100% recall on this dataset). Qdrant is fastest — an embedded, in-process client has no network round trip. Postgres is close behind. Neo4j is consistently slowest here: each write also polls its vector index to confirm the new node is searchable before returning (Neo4j's vector index updates asynchronously in the background, unlike Postgres/Qdrant which are immediately consistent — this poll is the only way to give this backend the same read-your-writes guarantee the other two have for free), and its lack of native pre-filtering adds Cypher-side filtering overhead on search. Reproduce this yourself with the command above — numbers will vary with hardware, but the ranking has been stable across repeated local runs.

Procedural memory

The original brainstorm's idea of "procedural memory" (a multi-step task workflow that improves over time) assumes an agent that executes multi-tool procedures. mnemos is a memory-augmented chat loop, not a task-executing agent, so bolting on a fake version of that would be dishonest. Instead, procedural memory here tracks a real recurring decision the system actually makes every turn: which retrieval weighting works best for this specific user.

Three named strategies, different (similarity_weight, recency_weight) pairs:

Strategy

similarity

recency

semantic_heavy

0.9

0.1

balanced (v1 default)

0.7

0.3

recency_heavy

0.4

0.6

Each turn, ProceduralMemory.choose_strategy() picks the empirically best-performing strategy for that user (epsilon-greedy, ε≈0.15 chance of exploring another). The outcome is scored on the user's next message in the same session: if it contains a correction cue ("no,", "that's wrong", "actually,", ...), the previous turn's strategy is marked a failure; otherwise a success. Crude, heuristic, and fully inspectable in src/mnemos/memory/procedural.py — not a hidden metric. The dashboard's Procedural Strategies view shows live uses/success-rate per strategy.

Reflection and forgetting

One consolidation pass per user (ReflectionEngine.run(), src/mnemos/memory/reflection.py), triggered on demand — CLI command or a dashboard button, not a background scheduler, because there's no task queue in this project and pretending otherwise would misrepresent what's actually running:

  1. Merge — active facts are greedily clustered by pairwise cosine similarity above a looser threshold than extraction's own dedup check; each multi-fact cluster becomes one Anthropic tool-use call that produces a single consolidated statement, written with the union of the cluster's source episodes, while the originals are marked status="merged" (soft — never hard-deleted, so there's always an audit trail).

  2. Decay — any fact whose last_reinforced_at is older than REFLECTION_DECAY_DAYS has its confidence multiplied by REFLECTION_DECAY_FACTOR.

  3. Forget — a fact whose confidence drops below REFLECTION_FORGET_CONFIDENCE_THRESHOLD becomes status="forgotten" (still soft — visible in the Memory Browser's "forgotten" filter).

Every action writes a reflection_log row — the dashboard's Reflection Log view is that table, verbatim. Reinforcement is the inverse of decay: MemoryEngine.recall() bumps last_reinforced_at (and nudges confidence up slightly, capped at 1.0) for every fact that actually makes it into a retrieval result — facts you keep needing survive, facts nobody asks about fade, implemented rather than narrated.

"Memory compression" and "dreaming" from the original brainstorm aren't separate features — compression is literally the merge step above, and "dreaming" would just be this same pass on a timer, which is exactly what the on-demand framing is honest about not having.

uv run python -m mnemos.cli.reflect --user demo-user

Time travel

MemoryEngine.recall(..., now=<a past datetime>) scores recency as though the query were asked at that point in simulated time — this already existed to make the benchmark's 30-day-gap questions work. The dashboard's Retrieval Trace view exposes the same parameter as an "as of" date picker: pick a past date, see exactly what would have been retrieved and why, with per-item similarity/recency/score. Not a separate subsystem — the same mechanism, a UI on top of it.

Dashboard

dashboard/ — React 19 + Vite + TypeScript, talking to the FastAPI backend over HTTP (@tanstack/react-query, react-router). Six views:

  • Chat — the product itself, not just an inspector.

  • Memory Browser — episodic list + semantic facts filterable by active/merged/forgotten.

  • Retrieval Trace — type a query (+ optional "as of" date for time travel), see the ranked result with per-item similarity/recency/score.

  • Timeline & Stats — episodic/semantic/merged/forgotten counts and a growth-over-time chart.

  • Reflection Log — audit trail + a "run reflection now" button.

  • Procedural Strategies — live uses/success-rate per strategy.

uv run uvicorn mnemos.api.main:app --port 8000   # backend, one terminal
cd dashboard && npm install && npm run dev        # frontend, another terminal

Then open the Vite dev URL (http://localhost:5173 by default). The backend must run on port 8000 to match dashboard/.env's VITE_API_BASE_URL and the CORS origin configured in src/mnemos/api/main.py.

The benchmark

benchmark/run_benchmark.py is the credibility check: it seeds 18 synthetic conversations across simulated days 0–18, each mentioning exactly one fact about a fictional user, then asks 18 probe questions later — some same-day, some a week later, some 30 days later — and compares two conditions:

  • With memory: the normal retrieve → build context → answer path.

  • No memory: the same question, same LLM, empty context (a stateless baseline).

It scores two things: retrieval recall@K (did the relevant fact or episode actually get retrieved — isolates the memory engine from the LLM's phrasing) and answer accuracy (keyword match against the generated reply — the end-to-end number), both broken down by how much simulated time had passed. It also records latency (p50/p95) and token usage/estimated cost.

uv run python -m benchmark.run_benchmark --llm anthropic   # the real, reported number
uv run python -m benchmark.run_benchmark --llm groq         # same, on a Groq-hosted model
uv run python -m benchmark.run_benchmark --llm mock         # fast offline smoke test of the harness only

--llm mock proves the plumbing works (seeding, simulated time, scoring, report generation) but the mock client ignores its context entirely, so its accuracy numbers are meaningless by construction — that run is for verifying the harness, never for reporting a result. The --llm anthropic/--llm groq runs are the ones that produce a real with-memory-vs-no-memory delta; they require ANTHROPIC_API_KEY/GROQ_API_KEY respectively and aren't included in this repo's history because they cost real API calls to generate — run one yourself and the result lands in benchmark/results/.

Using mnemos as a coding assistant's memory (MCP)

src/mnemos/mcp_server.py exposes mnemos as an MCP server, so a tool-calling assistant (Claude Code, or anything else that speaks MCP) can use it as persistent memory across sessions instead of — or alongside — whatever memory mechanism it already has. Five tools:

Tool

Does

mnemos_remember(fact)

Store a durable fact worth recalling later

mnemos_recall(query, top_k=5)

Retrieve memories relevant to a query

mnemos_list_memories(limit=50)

List active memories with their IDs

mnemos_forget(fact_id, reason)

Archive a specific memory (soft-delete, audited)

mnemos_reflect()

Run a consolidation pass (merge/decay/forget) on demand

All memories are stored under one fixed user (MNEMOS_MCP_USER_ID, default claude-code) — this is a single-user personal memory store, not a multi-tenant one. mnemos_remember/mnemos_recall skip the episodic layer and LLM-based extraction entirely: the calling assistant is already the one deciding what's worth keeping, so re-running extraction over its own summary would be redundant. It respects STORAGE_BACKEND exactly like the CLI, API, and dashboard do, so whatever gets remembered here shows up there too.

uv run python -m mnemos.mcp_server                          # run it directly, stdio transport
claude mcp add mnemos --scope user -- \
  /path/to/mnemos/.venv/bin/python3 -m mnemos.mcp_server     # register with Claude Code

Does it actually help, or is it just plumbing that runs? Two separate checks, because "the tool call succeeds" and "the memory survives a real session boundary" and "the recalled context actually improves the answer" are three different claims:

  1. Cross-process persistence (benchmark/verify_cross_session_persistence.py) — the binary, unfakeable check. Two fully independent OS processes, sharing nothing but the external storage backend: process 1 calls mnemos_remember and exits completely; process 2 starts cold afterward and calls mnemos_recall. This is exactly what a real Claude Code session boundary looks like (each session spawns the MCP server as its own fresh subprocess), not a simulation within one running process.

    uv run python -m benchmark.verify_cross_session_persistence
    # Process 1 — a 'session' that learns something, then exits completely...
    #   Remembered [...]: The user is verifying that mnemos persists memory...
    # Process 2 — a brand-new, independent 'session', shares no state with process 1...
    #   - The user is verifying that mnemos persists memory... (similarity 0.29)
    # PASS — memory written by process 1 was recalled by process 2.
  2. Does recalled context improve answer quality (benchmark/eval_claude_code_memory.py) — the same with-memory-vs-no-memory methodology as the main benchmark above, run through the real mnemos_remember/mnemos_recall tool functions instead of ConversationManager. Ten realistic things a user might tell a coding assistant across past sessions (testing framework, dependency manager, deploy target, editor, code-review preferences...), then ten questions asked cold, scored before vs after:

    Condition

    Answered correctly

    Before mnemos (no memory, today's baseline)

    40%

    After mnemos (recall via MCP)

    100%

    Delta

    +60%

    Real run, --llm groq (openai/gpt-oss-120b), retrieval hit rate 100%. The 40% baseline isn't zero — a couple of questions have a defensible generic best-practice answer (e.g. "should new code have type hints?") that happens to match without any memory at all; the rest are simply unknowable without it. Reproduce with:

    uv run python -m benchmark.eval_claude_code_memory --llm groq   # or --llm anthropic

To actually verify this from inside a real Claude Code session rather than a script: register the server (command above), start a new session (MCP servers load at session start, so an already-running session won't pick it up), ask it to remember something with mnemos_remember, then in a different, later session ask it to recall it — the only test that exercises the real product end to end rather than a proxy for it.

Running it

cp .env.example .env   # fill in ANTHROPIC_API_KEY or GROQ_API_KEY + LLM_PROVIDER

# Postgres + pgvector, via Docker...
docker compose up -d
# ...or a local install (see below if you don't have Docker)

uv sync --extra dev
uv run alembic upgrade head
uv run pytest                                    # unit tests: instant, fully offline
uv run python -m mnemos.cli.demo                 # terminal chat; restart it to test cross-session recall
uv run python -m mnemos.cli.reflect --user demo-user   # manual reflection pass
uv run uvicorn mnemos.api.main:app --port 8000   # full HTTP API (needed for the dashboard)
uv run python -m benchmark.run_benchmark --llm anthropic

Without Docker, install Postgres 16+ and pgvector locally (e.g. on macOS, brew install postgresql@17 pgvector, since Homebrew's pgvector bottle targets newer Postgres major versions than 16), create a mnemos role/db, CREATE EXTENSION vector; as a superuser, and point DATABASE_URL in .env at it. Integration tests default to spinning up an ephemeral Postgres+pgvector via testcontainers (works out of the box in CI); without Docker locally, set MNEMOS_TEST_DATABASE_URL to a scratch database instead and they'll use that.

Postgres is the default backend (STORAGE_BACKEND=postgres, or unset). To try Qdrant, no setup is needed — it runs embedded (set STORAGE_BACKEND=qdrant in .env or the environment). For Neo4j: brew install neo4j, neo4j-admin dbms set-initial-password <password> before first start, brew services start neo4j, then set STORAGE_BACKEND=neo4j and NEO4J_PASSWORD to match.

API surface

POST   /users/{user_id}/messages                chat turn (choose strategy -> recall -> LLM -> store -> maybe extract -> resolve prior turn)
GET    /users/{user_id}/memories/episodic        list stored turns
GET    /users/{user_id}/memories/semantic        list facts (?status=active|merged|forgotten)
DELETE /users/{user_id}/memories/semantic/{id}   explicit forgetting of one fact
POST   /users/{user_id}/reset                    wipe a user's memory
POST   /users/{user_id}/seed                     write pre-scripted turns (used by the benchmark)
POST   /users/{user_id}/retrieval-trace          run recall() without calling the LLM; ?as_of= for time travel
GET    /users/{user_id}/stats                    counts by type/status + growth-over-time buckets
POST   /users/{user_id}/reflect                  run a reflection pass now
GET    /users/{user_id}/reflection-log           audit trail of merge/decay/forget actions
GET    /users/{user_id}/procedural                per-strategy uses/success-rate

Tests

uv run pytest                 # unit tests (mocked LLM/embeddings where the test isn't about real quality)
uv run pytest tests/unit/test_retrieval.py  # runs against real local embeddings, not mocks —
                                              # the point of that file is proving semantic ranking works

63 tests total: the storage backend-contract suite (11 behavioral tests × 3 backends), retrieval ranking (real embeddings), extraction + dedup, procedural strategy selection/outcome-scoring, reflection merge/decay/ forget/reinforce, the conversation loop, the full HTTP API via httpx.AsyncClient, and benchmark scoring logic. Neo4j's contract tests skip gracefully if no local instance is reachable rather than failing the suite.

What's intentionally out of scope

A few things stayed out on purpose, to keep every feature above honestly and fully implemented rather than half-built:

  • Two real LLM providers (Anthropic, Groq) plus a mock — the LLMClient interface supports adding OpenAI/Gemini/local models later without touching agent or memory code, but a third real client isn't built.

  • No background scheduler — reflection runs on demand (CLI/dashboard), not on a timer. Adding one is an infra decision (cron, a task queue) orthogonal to the memory logic itself.

  • Heuristic-only signals — procedural memory's "success" signal (correction-cue keyword matching) and reflection's clustering/decay thresholds are documented defaults, not tuned or validated against a labeled dataset. They're real and inspectable, not research-grade.

License

MIT

A
license - permissive license
A
quality
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Related MCP Servers

  • A
    license
    A
    quality
    C
    maintenance
    Long-term memory for AI agents. Compiles conversations into a structured knowledge base with Claim/Evidence model, source provenance, append-only timeline, and contradiction detection. Multi-path retrieval (Exact + BM25 + Graph + weighted RRF + reranker) — 96.6% R@5 on LongMemEval-S, zero vector dependencies.
    8
    3
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Long-term memory for AI agents over MCP — episodic + semantic memory, a temporal knowledge graph, and a dialectic user model, exposed as 32 tools (recall, remember, context, graph, dreaming, peers). Zero dependencies, runs fully offline; leads the LoCoMo benchmark at ~35x fewer LLM calls.
    2
    Apache 2.0
  • A
    license
    Not graded
    quality
    B
    maintenance
    Persistent long-term memory for AI agents via MCP, saving 80-90% memory-related token costs by enabling on-demand recall instead of always-injecting context.
    10
    7
    MIT

View all related MCP servers

Related MCP Connectors

  • Long-term memory for AI agents: semantic facts, episodic events, and procedural workflows

  • Persistent memory and knowledge graphs for AI agents. Hybrid search, context checkpoints, and more.

  • Persistent memory for AI agents — verbatim conversations, searchable by meaning.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/vishalbanwari26/mnemos'

If you have feedback or need assistance with the MCP directory API, please join our Discord server