The opposite of amnesia (Greek hypermnesia — abnormally complete recall). Self-hosted long-term memory for AI coding agents — a Postgres-backed store that gives an agent (Claude Code, or any MCP client) two things:
- Architectural memory (doc-RAG + Tier 0/1) — your repos' docs made searchable, plus a component -> constraint map that resolves a file path to the rules covering it, so the applicable invariants reach the agent before it edits, without a search.
- Personal memory — durable facts, preferences and decisions distilled from work sessions and available in later ones, instead of being re-explained.
One store (Postgres + pgvector), local-model friendly (embeddings via Ollama or TEI), no cloud dependency. Runs on a laptop, one server, or Kubernetes.
Two interfaces, and they are not the same thing. Search, the map and memory are exposed as MCP tools, so any MCP client can ask for them. The automatic half — constraints injected before an edit, memory recalled per prompt, sessions captured — is a set of Claude Code hooks. Another client gets the data through the same tools, but only when the agent decides to call one, which is the weakness the hooks exist to remove.
A coding agent repeats mistakes when the rule, the earlier decision or the stated preference is not in its context at the moment it acts. Having it there is not a guarantee — an agent can be handed a rule and break it anyway, and it can get the same case right unaided. Missing context just makes the mistake much more likely. Three common gaps:
- The rule was written down and not read. Your repo documents that only the data layer talks to Postgres. The agent opens a handler, writes a query, and the rule was two directories away in a file it had no reason to open. In that case it did not disobey; it never saw it.
- You explain yourself again every session. The preference you stated last week, the decision you took last month, the reason the old approach was abandoned — all of it left with the context window.
- Search does not fire when it matters. Retrieval only helps if something calls it, and an agent mid-edit does not stop to wonder whether it should. Storage is solved. Delivery is not.
The usual answer is one big instructions file, and it loses for a specific reason: every rule in it costs tokens on every request whether or not the file being edited has anything to do with it, so the file gets trimmed to the rules that apply everywhere — and those are the vaguest ones. It also goes stale without saying so. Nothing tells you a path in it moved.
The map here is written by hand too. What is automatic is the selection and the delivery. You
maintain the components and their invariants; from there a file path is matched against the
component globs, and that component's must rules — plus those reached through one hop of the
dependency graph — are injected before the edit by a hook, rather than waited for. Documentation
and past decisions stay reachable behind that, by search.
A hand-authored map rots, so the rot is made visible rather than assumed away: globs that match
no file are reported, a store that will not answer says so instead of resembling a project with
no rules, and ./hm doctor names the faults that leave an install answering normally with a
missing index, half its embeddings, or a scope that matches nothing.
If you want to see it rather than read about it: docs/DEMO.md — two minutes, real output, no install beyond a Postgres.
flowchart TB
subgraph store["🗄️ One Postgres + pgvector"]
direction LR
DOC[("doc chunks<br/>embedding + tsvector")]
MAP[("component / constraint<br/>map + graph")]
MEM[("mem.* personal memory<br/>bi-temporal, supersede")]
end
subgraph ingest["📥 Ingest · offline"]
MD["repo *.md"] --> CH["chunk by heading"]
CH --> EMB["embed · bge-m3<br/>Ollama / TEI"]
EMB --> DOC
CH --> TS["composite tsvector<br/>(stem || simple)"] --> DOC
SEED["hand-authored<br/>Tier 0/1 seed"] --> MAP
end
subgraph ask["🔎 Agent asks · per request"]
FP["file path"] -->|"deterministic"| T01["Tier 0/1: resolve<br/>path → component"]
T01 --> RULES["must / should constraints<br/>+ 1-hop graph"]
Q["query"] --> QE["embed query"]
QE --> RRF["Tier 2: RRF fuse<br/>vector cosine + FTS"]
RRF --> RRK["cross-encoder rerank<br/>bge-reranker-v2-m3"]
RRK --> TOPK["top-k docs"]
end
subgraph pm["🧠 Personal memory · background"]
SESS["session transcript"] --> EX["extract · LLM<br/>durable facts only"]
EX --> MEM
MEM --> RC["recall → inject<br/>into the prompt"]
MEM --> CO["consolidate<br/>merge / supersede · review-gated"]
end
MAP -.-> T01
MAP -.-> RULES
DOC -.-> RRF
MEM -.-> RC
- Tier 2 search fuses dense (bge-m3 embeddings, HNSW) and lexical (composite
tsvector, works for code identifiers and non-English) via Reciprocal Rank Fusion, then an optional cross-encoder reranker (bge-reranker-v2-m3) reorders the top candidates. (It measurably helped on a private evaluation set; the figure is in eval/README.md with what it is and is not — one corpus, not a benchmark.) - Personal memory is bi-temporal (event time vs ingestion time), supersede-not-overwrite (corrections don't destroy history), with an abstention gate (an irrelevant query returns nothing, not noise). A background pass consolidates near-duplicates; low-confidence merges wait in a review queue for you.
- Capture/recall run as Claude Code hooks: session profile injected at start, relevant memories injected per prompt, transcripts distilled to memories by a small LLM on a schedule.
- Constraint injection is a hook too: a
PreToolUsehook (hooks/arch_invariants.py) resolves the file you're about to edit to its component and injects the applicablemustinvariants before the edit — so Tier 1 is delivered deterministically, not left to the agent to ask for. - Diagnostics, and what happens when something breaks. A hand-authored map and a
self-hosted store both fail in ways that still answer, so the failures are reported rather than
inferred:
ci/freshness.pyflags globs matching no file and documents indexed at an older commit, and refuses a scope nothing is mapped under;./hm doctor(and thestatusMCP tool, which runs the same script) checks the ANN index, embedding coverage, model consistency and the scope name; search says when it fell back to lexical-only or to plain RRF order; a cached map carries its staleness and expires; an empty enumeration refuses to write rather than emptying the store. Each of those, and why it exists, is in docs/DIAGNOSTICS.md. - Timeouts, output limits and cache expiry. Every child process the MCP server spawns is on a clock, a document comes back capped with the cut announced, and the structural map is re-read on a TTL rather than held for the life of the process.
HyperMnesia indexes docs, the architecture map, and memory — not code symbols. Live code
structure ("where is foo defined, who calls it") is best answered by a language server, which
already keeps a precise index and updates it as you type. Pair HyperMnesia with an
LSP-backed symbol MCP such as Serena: both run as MCP servers
in the same client, with no overlap —
| Agent's question | Answered by |
|---|---|
| where is a symbol defined / who calls it / its type | Serena / LSP (live, no re-embed) |
| what rules apply to this file, before I edit it | HyperMnesia Tier 0/1 |
| where's the doc, and what do I know about this project/owner | HyperMnesia Tier 2 + memory |
Live code → the LSP layer; anything you want to remember or that lives in prose → HyperMnesia.
See docs/ARCHITECTURE.md for the full system picture, an example
.mcp.json pairing both, and how it relates to managed memory offerings.
| Path | What |
|---|---|
hm |
one wrapper over the documented steps: init (compose + schema), ingest (ingest -> embed -> ANN index, incremental when the scope already exists), doctor |
sql/ |
schema: doc-RAG (documents/components/constraints/relationships/chunks) + personal memory (mem.*) |
ingest/ |
markdown chunker, embedder (Ollama/TEI), hybrid RRF search, mem_ops; incremental re-ingest via --known-hashes (unchanged docs keep their embeddings) |
rerank/ |
optional cross-encoder reranker service + search orchestrator |
hooks/ |
Claude Code hooks: constraint inject (arch_invariants), profile inject, per-prompt recall, capture, extract, consolidate, reflect (per-project knowledge pages) |
ci/ |
doctor.py — health check for faults that leave an install answering normally (missing index, partial embeddings, mixed models, wrong scope); latency.py — where the time goes (hook, embedder, database, reranker); freshness.py — map-staleness / orphan-glob checker (run against a target repo); check_graph_sql_parity.py — keeps the Python and Rust copies of the graph query identical |
tests/ |
contract tests, all wired into CI: hook I/O, ingest enumeration, incremental ingest, chunk bounds, glob parity, query hygiene, doctor, hm ingest — all DB-free except test_memory_sql.py, which asserts the mem.* view (supersede, validity window) and the abstention gate against a live pgvector, with no embedder |
mcp-server/ |
Rust MCP server exposing project map / constraints / search / memory / status tools |
console/ |
the operator's side: a menu-bar tray and four command-line tools (stats, jobs, settings, setup) over any deployment — volumes, scheduled jobs, tunables, and a first-run wizard |
deploy/ |
docker-compose (single box) + Kubernetes manifests |
examples/ |
an example structural-tier seed for a project |
skills/ |
onboard-project — the six steps to connect a new repo; just — answer-only / audit mode (agent-readable skills) |
See docs/INSTALL.md for the four deployment options (laptop, single server, Kubernetes, CPU-only-minimal) and the hardware / OS / software requirements table.
TL;DR (single box). This ends at the first thing you can see: an invariant arriving before an edit.
cp deploy/docker/.env.example deploy/docker/.env # POSTGRES_PASSWORD: openssl rand -hex 24
./hm init # compose up, wait for Postgres, load both schemas
export DATABASE_URL=... # init prints the exact line
./hm ingest /path/to/your/repo myrepo # ingest -> embed -> ANN index -> doctorhm is the recommended path because the order of those steps is load-bearing and getting it
wrong is silent: the ANN index must be built after the first bulk embed, and a re-ingest without
a known-hashes snapshot deletes the scope's documents and every embedding with them.
docs/INSTALL.md has the same steps by hand, the non-Docker and Kubernetes
paths, and the flags (--walk, --known-hashes) that matter on later runs.
The Tier 0/1 map is what defines which files belong to each component and which constraints apply
to them. Nothing can generate it honestly from a directory listing, so writing it is the work —
start from examples/seed_example.sql, load it under your own scope,
then point your MCP client at mcp-server and register the hooks
(docs/INSTALL.md).
To see a rule reach an edit, run the hook by hand against a file in your repo. It lives in the
HyperMnesia checkout, and cwd must be the project being edited, so give both explicitly:
HM=/path/to/hypermnesia # this checkout
PROJ=/path/to/your/repo # what you ingested as `myrepo`
printf '{"hook_event_name":"PreToolUse","tool_name":"Edit","cwd":"%s",
"tool_input":{"file_path":"%s/src/api/users.py"}}' "$PROJ" "$PROJ" \
| HM_REPO=myrepo python3 "$HM/hooks/arch_invariants.py"A hookSpecificOutput block naming your invariant confirms that the hook resolves that path
against the map and returns the rule. It does not confirm that Claude Code is running the
hook, or that your MCP client reached the server — for the first, make an edit from Claude Code
and look for the same block; for the second, call the status tool, which answers from the store.
docs/DEMO.md walks the same path in two minutes with real output.
Everything above answers questions asked of it. The console is the other direction: what the store holds, whether the scheduled passes are running, and what the tunables are set to — without writing a query.
cd console && cargo build --release # two dependencies, both only for the tray
./target/release/hypermnesia-setup # the walkthrough: from nothing to a menu-bar iconIt reaches the database exactly one way: a command that receives SQL on stdin. Direct psql,
docker exec, kubectl exec, ssh to a machine that has kubectl — all of them are one string with
different contents, which is why there is one setting and not five. The wizard tries the command
before writing it, because an untried setting is a guess, and a console showing an empty screen
cannot be told from an empty store.
| Command | What |
|---|---|
hypermnesia |
the menu-bar tray: volumes, a Run-now button per job, schedules, refresh |
hypermnesia-stats |
the same numbers on stdout |
hypermnesia-jobs |
scheduled passes: what is configured, when each last worked, run one now, change a schedule |
hypermnesia-settings |
the tunables, each with the value in force and where that value came from |
hypermnesia-setup |
the first-run walkthrough, and --connect / --show / --test on their own |
The rule it is written to is the one this project is about: stale must not look fresh, and missing must not look empty. A reading that failed keeps the old numbers and labels them with their age; a job that never ran says so rather than showing launchd's zero as success; a settings file the hooks refuse is reported as refused, not displayed knob by knob as if it applied.
- docs/ARCHITECTURE.md — the whole system: the LSP/code layer + HyperMnesia, how to pair them, and related work.
- docs/DESIGN.md — architecture and the reasoning behind the tiers.
- docs/MEMORY.md — the personal-memory model (bi-temporal, supersede, consolidation).
- docs/COMPARISON.md — where HyperMnesia fits vs. neighbours, and honest non-goals/limitations.
- skills/onboard-project/SKILL.md — connecting a repository: ingest, the Tier 0/1 map, pairing with Serena, verification, and what changes per deployment.
- skills/just/SKILL.md —
/just: answer the question literally with read-only tools and stop; the contract is checkable from the tool log. Session-wide as "audit mode".
MIT — see LICENSE.
