awhm-mcp
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@awhm-mcpRemember that I prefer dark mode for coding."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
AWHM Lite
External long-term memory for LLM agents. No cloud, no API keys, runs entirely local.
AWHM Lite gives any LLM persistent memory across conversations through append-only logging, regex-based pattern matching, a contradiction-aware memory graph, symbolic consolidation (zero LLM calls), and retrieval via lexical + semantic feature fusion.
Status: research prototype. Built February 2026, published August 2026; v0.2.0 hardened it, v0.3.0 added hooks, Stage 2, entity resolution, time travel, SQLite storage and real-corpus evaluation. 145 tests, CI on Python 3.11 to 3.13.
Project docs
docs/awhm-whitepaper.md: the full AWHM architecture paper this is a subset ofdocs/awhm-whitepaper-vs-lite.md: what Lite keeps and what it leaves outdocs/Future Plans.md: the planned next step (silent per-turn middleware)
Related MCP server: claude-memory-mcp
How this was built
The architecture and the ideas behind it are mine. The code was written entirely by AI coding agents (mainly Claude Code) under my direction: I set the design, scoped the tasks, reviewed the output and steered. The whitepaper was produced the same way.
INTERACTION TIME OFFLINE (SESSION END)
──────────────────── ─────────────────────
┌──────────────────┐ real-time log ┌──────────────────────┐
│ PRIMARY AGENT │──────────────────► │ STAGE 1 CONSOLIDATION│
│ (user-facing) │ (middleware, │ (symbolic only, │
└──────┬───────────┘ no LLM) │ zero LLM calls) │
│ └──────────┬───────────┘
│ queries │ writes
▼ ▼
┌──────────────┐ ┌──────────┐ ┌──────────────────────┐
│ RETRIEVAL │◄───│ SESSION │ │ FLAT MEMORY GRAPH │
│ ENGINE │ │ BUFFER │ │ │
│ │◄───┤(checked │ │ nodes: episodic, │
│ BM25 + │ │ first) │ │ semantic, procedural│
│ embedding │ └──────────┘ │ │
│ similarity │◄───────────────────│ edges: typed │
│ │ │ strength: rec + freq│
└──────────────┘ └──────────────────────┘
▲
│ fallback (first ~10 sessions)
┌──────┴───────┐
│ RAW LOGS │
│ (append-only)│
└──────────────┘Install
# Core (numpy, spaCy, dateparser) plus the sentence-transformers embedding model
pip install -e ".[embeddings]"
# spaCy NER model (used in consolidation; without it, entity extraction is skipped)
python -m spacy download en_core_web_sm
# Claude Code MCP integration
pip install -e ".[mcp]"
# Optional: Anthropic SDK client for Stage 2 (the default Stage 2 client is
# the Claude Code CLI and needs nothing extra)
pip install -e ".[anthropic]"sentence-transformers is optional because it pulls in PyTorch. Without it, start
sessions with use_mock_embeddings=True (deterministic hash-based vectors, fine for
tests and for trying the CLI). The real model (all-MiniLM-L6-v2, 22 MB) downloads
on first use.
Quick start
Python API
from awhm import AWHMSession
from awhm.types import Role
# Start a session (also usable as a context manager: `with AWHMSession.start_session() as session:`)
session = AWHMSession.start_session()
# Log messages
session.log_message(Role.USER, "My name is Alice")
session.log_message(Role.ASSISTANT, "Hello Alice!")
session.log_message(Role.USER, "I prefer Python over JavaScript")
session.log_message(Role.USER, "The API endpoint is https://api.example.com/v2")
# Query memory (works immediately via session buffer)
results = session.query("What language does the user prefer?")
for r in results:
print(f"[{r.source}] {r.content}")
# Consolidate into long-term memory graph
session.consolidate_current()
# End session (flushes WAL, saves graph)
session.end_session()Integrating with an LLM
AWHM sits as middleware. It doesn't call any LLM — you wire it into whatever you use:
from awhm import AWHMSession
from awhm.types import Role
session = AWHMSession.start_session()
def handle_message(user_text):
session.log_message(Role.USER, user_text)
# Retrieve relevant memories
memories = session.query(user_text, k=5)
memory_context = "\n".join(f"- {m.content}" for m in memories)
# Inject into system prompt
system = f"Memories from past conversations:\n{memory_context}"
response = your_llm_call(system_prompt=system, user_message=user_text)
session.log_message(Role.ASSISTANT, response)
return response
# At end of conversation:
session.consolidate_current()
session.end_session()CLI
awhm status # Show system stats
awhm query "Python preferences" # Search memory
awhm query "API endpoint" --include-history --trace
awhm consolidate # Run Stage 1 on pending sessions
awhm snapshot create # Backup current graph
awhm snapshot list # List snapshots
awhm snapshot restore --path FILE # Restore from snapshot
awhm delete NODE_ID # Hard-delete a node (privacy)
awhm eval --json # Run built-in benchmark reportClaude Code integration (hooks, recommended)
With hooks, memory works on every turn without the model having to call a tool. Each hook is a separate short-lived process; the session buffer is resumed from its write-ahead log between them.
Event | Command | What it does |
|
| Logs the prompt, retrieves the top memories (BM25 + buffer; add |
|
| Logs the assistant's reply |
|
| Consolidates the session into the graph (add |
awhm hook settings # prints the block to merge into ~/.claude/settings.jsonHooks never block a session: any failure is written to stderr and the process
exits 0. Set AWHM_DATA_DIR to change where memory lives.
Claude Code integration (MCP)
AWHM Lite ships as an MCP server so Claude Code can use it as a tool.
Setup
# Install with MCP support
cd awhm-lite
pip install -e ".[mcp]"
# Register with Claude Code
claude mcp add --transport stdio awhm-lite -- awhm-mcpOr manually add to .claude/settings.json:
{
"mcpServers": {
"awhm-lite": {
"type": "stdio",
"command": "awhm-mcp",
"env": {
"AWHM_DATA_DIR": "~/.awhm"
}
}
}
}Available MCP tools
Tool | Description |
| Search memory for a natural language query ( |
| Log a message to the raw conversation log |
| Extract memories from pending sessions into the graph |
| Show node count, edge count, session count |
| Create a backup snapshot |
| Hard-delete a node + scrub matching snapshot data |
Once connected, Claude Code will automatically have access to these tools and can query/store memories across conversations.
How it works
Raw logs
Every message is appended to a JSONL file (one per session). Append-only, never modified except for privacy hard-deletes. This is the ground truth.
Session buffer
A regex-based pattern matcher runs on every user message in real time, catching:
Corrections: "actually, X is Y", "no, it's X"
Preferences: "I prefer X", "always use X", "never do X"
Facts: "the endpoint is X", "my name is X"
Outcomes: "that worked", "that failed"
Captures ~60-70% of explicit signals with zero LLM calls. The buffer is checked first during retrieval for instant intra-session continuity. During default retrieval, buffer entries that a later statement supersedes (same slot, or an explicit correction a few messages later) are hidden, so corrections win. Persisted via per-session write-ahead logs (30s flush interval, skipped when nothing changed).
Memory graph
A flat directed graph with three node types (episodic, semantic, procedural) and three edge types (temporal, abstraction, association).
Each node now carries contradiction-lifecycle metadata:
canonical_key(slot-style identity, e.g.fact:my preferred language)status(active,superseded,retracted)supersedes(older node IDs replaced by this node)valid_from/valid_toconfidence
Stored as JSON, loaded into memory.
Backward compatibility: older graph files (without lifecycle fields) are auto-migrated in-memory on load.
Strength scoring
Each node has a composite strength score:
S(v) = 0.4 * recency + 0.6 * frequencyRecency uses power-law decay: s_rec = (1 + 0.1 * hours)^(-0.3) — roughly 0.71 at 24h, 0.40 at 7 days, 0.27 at 30 days. Frequency is access count normalized against the 90th percentile.
Consolidation (Stage 1)
Runs at session end, zero LLM calls:
NER via spaCy: people, orgs, places, products. Numeric and time-like labels (CARDINAL, MONEY, DATE, ...) are filtered out; they made noise nodes. Configurable via
ner_labels.Temporal parsing via dateparser: resolve "yesterday", "March 5" to ISO timestamps
Rule-based extraction: same regex patterns as the session buffer, over the new messages
Entity linking: match entities to existing nodes (one cosine matrix product, then entity-type agreement and a string-similarity guard)
Deduplication: identical statements within the batch are collapsed; near-duplicates of existing nodes (cosine > 0.92) reinforce that node instead of creating a new one
Commit: assign canonical keys, supersede contradicted memories, add nodes and edges, refresh strength scores
Contradictions: canonical keys
A canonical key names the slot a statement fills. Two active memories with the same key contradict each other, so the newer one supersedes the older (status=superseded, valid_to set, supersedes link on the new node).
Statement | Key |
"My preferred language is Python" |
|
"I live in Cape Town" |
|
"I prefer dark mode" |
|
"Never use tabs for indentation" |
|
"I use Python for scripting" | none (additive) |
Rules, deliberately conservative because there is no LLM to judge intent:
Same key: always supersedes (the slot is restated with a new value).
Preference / policy families: an explicit correction ("Actually, I prefer Rust") supersedes the previous statement of the same family if it comes within
correction_window_messages(default 3) of it in the same session. Without a correction marker, preferences are additive: "I prefer tabs" and "I prefer dark mode" both stay active.Fact family: only an exact key match supersedes, so a correction about the API endpoint can never clobber your name.
Anything not recognised gets no key and never supersedes.
Entities
Named entities resolve to one node however they are written. Surface forms are normalised (case, possessives, corporate suffixes, domains: "Acme Holdings Ltd" and "acme.com" both become "acme"), then matched by exact alias, by unambiguous token containment ("Acme" inside "Acme Holdings"), and finally by embedding similarity with the same entity type. Every resolved mention is recorded as an alias on the node, and statements get association edges to the entities they mention, so retrieval can walk from "Acme" to everything known about it.
Stage 2 (optional LLM refinement, no API key)
Stage 1 has a hard ceiling: it catches "I prefer Rust" and misses "let's go with Rust then". Stage 2 runs after Stage 1, offline, and asks an LLM to propose the memories the rules did not find. The LLM only proposes: code validates every proposal (schema, cited message numbers must exist, confidence floor), drops anything already captured, and commits through the same slot and supersession rules. Retrieval stays zero-LLM.
The default client shells out to the Claude Code CLI (claude -p with
structured output), so it uses the login you already have and no API key is
stored anywhere. It marks the call so the memory hooks do not fire inside it.
awhm consolidate --stage2 # Claude Code CLI, default model
awhm consolidate --stage2 --stage2-model sonnetfrom awhm import AWHMSession, AWHMConfig
config = AWHMConfig(stage2_enabled=True, stage2_model="sonnet")
with AWHMSession.start_session(config) as session: # builds ClaudeCodeClient
...
session.consolidate_current()Any object with a complete_json(system, user, schema) -> str method works as
a client (llm_client=...). An Anthropic SDK client is included for people
who prefer API billing (stage2_client="anthropic", extra [anthropic]).
Retrieval
Zero LLM calls. Feature-based fusion:
Buffer check: search the session buffer first (instant hits, always ranked above graph results)
Anchor identification: BM25 term overlap plus embedding cosine similarity (union). The BM25 index is built in-process (Lucene-style IDF, so tiny corpora still score sensibly) and cached until nodes change.
History filter: by default, only
status=activegraph nodes are eligibleFeature scoring: semantic similarity + lexical score + strength + confidence, minus a contradiction penalty. Strength is recomputed only for the candidates being ranked.
Return top-k (default 10)
Neighbour expansion: one-hop neighbours of the anchors (linked entities, sequential episodes) join the candidate set with a decayed edge weight, scored via the
associationfeature. Only anchors that are themselves current may expand.Cold-start fallback: for the first ~10 sessions, also runs BM25 over raw logs. Those hits are scaled into
[0, raw_log_score_scale]so they never outrank a real graph match.
Time travel
Facts carry a validity window. Dates introduced with "from"/"since" set
valid_from, "until" sets valid_to, and a supersession closes the older
fact's window. query(..., as_of="2026-03-01") answers with what was true at
that moment, superseded memories included:
awhm query "API endpoint" # what is true now
awhm query "API endpoint" --as-of 2026-02-01 # what was true thenSet include_history=True to surface superseded/retracted memories.
Use with_trace=True to return per-result ranking feature traces.
Evaluation
The built-in benchmark is a synthetic smoke test (three correction-heavy queries plus a deletion audit). Real numbers come from replaying a corpus:
awhm eval # built-in synthetic benchmark
awhm eval --corpus my_sessions.json # native format, see below
awhm eval --corpus longmemeval_s.json --longmemeval --limit 50Both report Recall@k, nDCG@k, contradiction error rate, p50/p95 latency and
per-category recall. The native corpus format is {"sessions": [{"id", "messages": [{"role", "content"}]}], "questions": [{"id", "question", "expected": [...], "forbidden": [...], "as_of", "category"}]}.
LongMemEval instances are consolidated and questioned in isolation, matching
the benchmark protocol. Matching is by answer substring, a deliberate lower
bound: paraphrased hits are not counted.
Measured (Stage 1 only, oracle split, 500 questions): Recall@5 0.196, from 0.40 on single-session user facts down to 0.00 on preferences, at 4 ms per query. That is the regex ceiling made visible; Stage 2 exists to lift it. Full table, caveats and reproduction in docs/benchmarks.md.
Configuration
All parameters are configurable via AWHMConfig:
Parameter | Default | Description |
| 0.3 | Decay rate (power-law exponent) |
| 0.1 | Decay scaling constant |
| 0.4 | Recency weight in strength score |
| 0.6 | Frequency weight in strength score |
|
| Retrieval weighting profile |
| 0.55 | Semantic similarity weight |
| 0.20 | BM25 lexical weight |
| 0.15 | Node strength weight |
| 0.10 | Consolidation confidence weight |
| 0.35 | Penalty for non-active memories |
|
| Include superseded/retracted memories by default |
|
| Emit ranking traces by default |
| 10 | Top-k retrieval count |
| 0.85 | Cosine threshold for entity linking |
| 0.92 | Cosine threshold for deduplication |
| 0.5 | Lexical anchor if score >= ratio x best BM25 score |
| 0.3 | Minimum cosine sim for anchor set |
| 0.5 | Upper bound for cold-start raw-log hit scores |
|
| Pull in one-hop graph neighbours of anchors, with this edge-weight multiplier |
| 0.10 | Weight of neighbour evidence in the blend |
|
|
|
|
| Offline LLM refinement after Stage 1 |
|
|
|
| 60 / 0.5 | Messages per LLM call; proposals below this confidence are dropped |
| 3 | How close an explicit correction must be to supersede a preference/policy |
| PERSON, ORG, GPE, ... | spaCy entity labels that become nodes |
| 30s | WAL persistence interval |
|
| Reserved ANN index mode |
|
| Scrub matching snapshot memory on hard delete |
from awhm.config import AWHMConfig
config = AWHMConfig(
data_dir="~/.my-project-memory",
k=20,
w_rec=0.5,
w_freq=0.5,
)Data directory
~/.awhm/
├── logs/ # Raw JSONL logs (one per session)
│ ├── {session_id}.jsonl
│ └── ...
├── graph/
│ ├── memory_graph.json # The memory graph (storage_backend="json")
│ └── memory_graph.sqlite # ... or one row per node (storage_backend="sqlite")
├── snapshots/
│ └── snapshot_{timestamp}.json # Manual backups
├── wal/
│ └── {session_id}.wal # Per-session write-ahead logs
└── meta/
├── consolidated_sessions.json # Tracks which sessions have been processed
├── deletion_tombstones.jsonl # Deletion tombstones
└── deletion_ledger.jsonl # Deletion audit ledgerTesting
pip install -e ".[dev]"
pytest tests/ -vAll tests use MockEmbeddingService (deterministic across processes, no model download). ruff check . runs the linter; CI runs both on Python 3.11, 3.12 and 3.13.
Dependencies
Package | Size | Purpose |
numpy | ~29 MB | Vector math |
spacy + en_core_web_sm | ~35 MB | NER |
dateparser | ~2 MB | Date parsing |
sentence-transformers (optional, | ~3 MB (+PyTorch ~350 MB) | Embedding model |
mcp (optional, | ~1 MB | Claude Code integration |
BM25 is implemented in-package (about 60 lines), so there is no ranking dependency.
The embedding model (all-MiniLM-L6-v2, 22 MB) downloads on first use to ~/.cache/huggingface.
Project structure
src/awhm/
├── __init__.py # AWHMSession facade (top-level API)
├── config.py # All parameters + path helpers
├── types.py # Enums: Role, NodeType, NodeStatus, EdgeType, BufferEntryType
├── mcp_server.py # MCP server for Claude Code
├── hooks.py # Claude Code hook commands (prompt / stop / session-end)
├── timeutil.py # Timestamp parsing, validity windows
├── eval/ # Built-in benchmark + real-corpus replay (LongMemEval loader)
├── raw_log/ # Append-only JSONL logging
├── session_buffer/ # Regex pattern matching + WAL
├── graph/ # Memory graph, strength scoring, JSON/SQLite stores
├── consolidation/ # NER, temporal, extraction, entities, dedup, Stage 2, pipeline
├── retrieval/ # Embedding, BM25, ranking, retrieval engine
├── snapshots/ # Snapshot create/restore/list
├── deletion/ # Hard-delete cascade
└── cli/ # argparse CLIThis server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceAn MCP server that allows Claude and other LLMs to manage persistent memories across conversations through text file storage, enabling commands to add, search, delete and list memory entries.657MIT
- AlicenseNot gradedqualityCmaintenanceAn MCP server that gives Claude Code cross-session memory persisted to a plain .claude-memory.md file in your repo.MIT
- AlicenseNot gradedqualityDmaintenanceA persistent memory MCP server for Claude Code that enables long-term recall across sessions via hybrid search, code intelligence, and tools for reading/writing memory.231MIT
- AlicenseNot gradedqualityBmaintenanceA MCP server that gives Claude Code and other AI assistants long-term memory by automatically extracting technical knowledge from conversations and retrieving relevant experiences in future sessions.14MIT
Related MCP Connectors
Cloud-hosted MCP server for durable AI memory
Hosted MCP server connecting claude.ai, ChatGPT and other AI apps to your own computer
One memory, every AI: Claude, ChatGPT, Perplexity, Gemini, Cursor, OpenClaw, Hermes, any MCP client.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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/juderosendev/awhm-lite'
If you have feedback or need assistance with the MCP directory API, please join our Discord server