Skip to content

Repository files navigation

memstrata

Semantic memory for AI agents, stored in strata — retrieve at the shallowest depth that serves the task.

memstrata is a self-hostable MCP (Model Context Protocol) server that gives any MCP client — Claude Desktop, Claude Code, or your own agent — persistent semantic memory backed by PostgreSQL + pgvector. Its two signature ideas: tiered memory depth as a token-economy and privacy mechanism, and briefings — short-lived pinned context that works like temporary RAM.

Reference implementation distilled from a production system I operate privately — a personal memory server that has been in daily use across multiple agent frontends, accumulating and serving thousands of memories. This rebuild keeps the load-bearing ideas, sheds the personal plumbing, and fixes one design gap the production system taught me about (see Design notes).

The problem

Agent memory has a cost problem and a privacy problem, and they're the same problem: retrieval returns too much. Most memory stores return full documents for every hit, so the agent's context fills with raw content when a one-line summary would have answered the question — burning tokens, and exposing detail that didn't need to leave the database. Memory should behave like an organization, not a filing cabinet: most questions are answered from the executive summary, and you only pull the full file when the task demands it.

How it works

Memory in three strata

Every memory can exist at three depths, and search results return only the depth you ask for:

Depth Returns Use case Typical token cost
L1 Summary (or first ~200 chars) "What do I know about X?" — awareness ~10–15% of raw
L2 Full stored content Detailed reads, reasoning over specifics baseline
L3 Raw/archival content (transcripts, dumps) Source-of-truth lookups largest, decays fastest in ranking

The depth parameter is a privacy boundary as much as a cost dial: an agent (or a tier-restricted deployment — see tiergate) can be granted L1 access to a memory set whose raw content it never sees.

Briefings: temporary RAM

A briefing is a memory pinned with a TTL. While active, it gets a relevance boost in every search and surfaces in get_briefings at session start; when it expires, it silently returns to being an ordinary memory. That's working memory in the cognitive sense — "this matters right now" — without polluting long-term ranking forever. Active briefings are capped (default 5) so the mechanism can't degenerate into "everything is important."

Ranking: similarity × decay × use

Results aren't ranked by cosine similarity alone. Inspired by ACT-R memory models:

score = similarity × recency_decay × access_boost

Old memories fade slowly (L3 archival content fades faster, by design); frequently retrieved memories earn a capped boost. The effect in practice: the store stays useful for years without manual curation, because staleness is priced into every query.

Eight tools, one server

Tool Purpose
save_memory Store content + optional summary, category, metadata; embeds on write
search_memories Semantic (pgvector cosine) or keyword (websearch_to_tsquery) via mode; depth via depth
get_memory Full record by ID
update_memory Mutate content/metadata — re-embeds when content changes
delete_memory Irreversible removal
pin_briefing Pin a memory as an active briefing with TTL
get_briefings Active briefings for session start
stats Counts by category, embedding coverage, briefing slots

Embeddings are local-first: nomic-embed-text (768-dim) via Ollama, with an HNSW index for fast nearest-neighbor search. No API key required anywhere in the default setup. A deterministic fake embedder ships for tests and keyless demos.

Quickstart

git clone https://github.com/ConsultRuss/memstrata && cd memstrata
pip install -e .              # Python 3.12; use a venv if you like
docker compose up -d          # postgres + pgvector (+ optional ollama profile)
python demo.py                # seeds synthetic memories, runs the showcase

demo.py walks the whole idea in one sitting: seeds a synthetic corpus, runs the same query at L1 vs L2 and prints the token-count difference, pins a briefing and shows its ranking boost, then shows decay reordering results between an old and a fresh memory.

Connect it to Claude Desktop or any MCP client over HTTP — config snippet in docs/clients.md, including a minimal TypeScript client example.

Measured, not asserted

memstrata ships its retrieval evals. A labeled synthetic corpus (memories + queries + relevance judgments) lives in evals/, and CI reports:

Metric What it measures
precision@5 / recall@10 Semantic search quality against labeled judgments
depth token savings Tokens returned at L1 vs L2 across the eval query set
decay correctness Older/less-used memories rank below fresher equals at equal similarity
briefing boost Pinned memories outrank unpinned equals; expired pins don't

Latest run — 48 synthetic memories, 20 labeled queries, deterministic fake embedder (precision@5 denominator is min(5, |relevant|); recall@10 over the full relevant set):

Metric Value
precision@5 0.688
recall@10 0.817
depth token savings (L1 vs L2) 59.7%
decay correctness PASS
briefing boost PASS

These numbers are reproduced by python -m evals.run and pinned in tests/test_evals.py so CI catches regressions.

A TypeScript port of the retrieval-quality metric (precision@5 / recall@10) also lives in evals-ts/, wired into Braintrust's Eval() SDK instead of a printed table. It reimplements the same fake embedder, ACT-R ranking math, and shared evals/ fixtures, and reproduces these numbers almost exactly (precision@5 68.83%, recall@10 81.67%). Run cd evals-ts && npm install && npm run eval:dry for a keyless local run, or npm run eval with a Braintrust API key to log a real experiment.

pytest tests/ -v          # unit + behavior tests
python -m evals.run       # retrieval-quality report (table printed + saved to evals/results.md)

Design notes

  • Why depth lives in the database, not the prompt. You could ask an LLM to "be brief with retrieved memories" — but that's a request, not a guarantee, and it happens after the tokens were spent and the raw content was exposed. Storing summaries as first-class rows and selecting depth at query time makes the economy and the privacy boundary structural.
  • Why local embeddings. Embedding on save is the hottest path in a memory server. A local 768-dim model makes writes fast, free, and offline-capable, and it keeps memory content from transiting a third party just to become searchable. The model name is stored per-row, so a future model swap is a batch re-embed, not a corruption.
  • The gap the production system taught me: the original implementation did not re-embed when a memory's content was updated — edits silently drifted away from their own embeddings. This rebuild re-embeds on content change and tests for it. Reference implementations should ship the lesson, not just the pattern.
  • Thresholds are tuned, not divined. Defaults — 0.65 similarity for "same topic," 0.92 for duplicate detection — come from production retrieval logs, and the eval harness exists so you can re-tune them against your own corpus instead of trusting mine.

Roadmap

  • LLM-synthesized briefings (compose related memories into one digest at pin time)
  • Duplicate detection & merge tooling
  • OpenTelemetry traces for retrieval calls

License

MIT

About

Semantic memory MCP server (FastMCP + Postgres/pgvector) with tiered recall depth for token economy. Ships a retrieval-quality eval suite — precision@5 0.69, recall@10 0.82.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages