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).
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.
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.
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."
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.
| 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.
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 showcasedemo.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.
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)- 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.
- LLM-synthesized briefings (compose related memories into one digest at pin time)
- Duplicate detection & merge tooling
- OpenTelemetry traces for retrieval calls
MIT