awhm-mcp

awhm-mcp

An MCP server that gives Claude Code persistent, fully local long-term memory across conversations, with tools to query, log, consolidate, snapshot, and delete memories without any LLM calls.

Category
访问服务器

README

AWHM Lite

CI

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 of
  • docs/awhm-whitepaper-vs-lite.md: what Lite keeps and what it leaves out
  • docs/Future Plans.md: the planned next step (silent per-turn middleware)

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 report

Claude 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
UserPromptSubmit awhm hook prompt Logs the prompt, retrieves the top memories (BM25 + buffer; add --semantic to also use embeddings) and returns them as hidden context
Stop awhm hook stop Logs the assistant's reply
SessionEnd awhm hook session-end Consolidates the session into the graph (add --stage2, or set AWHM_STAGE2=1, to also run Stage 2 through claude -p)
awhm hook settings          # prints the block to merge into ~/.claude/settings.json

Hooks 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-mcp

Or manually add to .claude/settings.json:

{
  "mcpServers": {
    "awhm-lite": {
      "type": "stdio",
      "command": "awhm-mcp",
      "env": {
        "AWHM_DATA_DIR": "~/.awhm"
      }
    }
  }
}

Available MCP tools

Tool Description
memory_query Search memory for a natural language query (include_history, with_trace optional)
memory_log Log a message to the raw conversation log
memory_consolidate Extract memories from pending sessions into the graph
memory_status Show node count, edge count, session count
memory_snapshot_create Create a backup snapshot
memory_delete_node 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_to
  • confidence

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 * frequency

Recency 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:

  1. 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.
  2. Temporal parsing via dateparser: resolve "yesterday", "March 5" to ISO timestamps
  3. Rule-based extraction: same regex patterns as the session buffer, over the new messages
  4. Entity linking: match entities to existing nodes (one cosine matrix product, then entity-type agreement and a string-similarity guard)
  5. 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
  6. 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" fact:my preferred language
"I live in Cape Town" fact:i live in
"I prefer dark mode" preference:dark
"Never use tabs for indentation" policy:use:tabs
"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 sonnet
from 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:

  1. Buffer check: search the session buffer first (instant hits, always ranked above graph results)
  2. 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.
  3. History filter: by default, only status=active graph nodes are eligible
  4. Feature scoring: semantic similarity + lexical score + strength + confidence, minus a contradiction penalty. Strength is recomputed only for the candidates being ranked.
  5. Return top-k (default 10)
  6. Neighbour expansion: one-hop neighbours of the anchors (linked entities, sequential episodes) join the candidate set with a decayed edge weight, scored via the association feature. Only anchors that are themselves current may expand.
  7. 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 then

Set 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 50

Both 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
alpha 0.3 Decay rate (power-law exponent)
beta 0.1 Decay scaling constant
w_rec 0.4 Recency weight in strength score
w_freq 0.6 Frequency weight in strength score
retrieval_profile "balanced" Retrieval weighting profile
w_semantic 0.55 Semantic similarity weight
w_lexical 0.20 BM25 lexical weight
w_strength 0.15 Node strength weight
w_confidence 0.10 Consolidation confidence weight
contradiction_penalty 0.35 Penalty for non-active memories
include_history_by_default False Include superseded/retracted memories by default
trace_retrieval False Emit ranking traces by default
k 10 Top-k retrieval count
entity_link_threshold 0.85 Cosine threshold for entity linking
dedup_threshold 0.92 Cosine threshold for deduplication
bm25_anchor_ratio 0.5 Lexical anchor if score >= ratio x best BM25 score
embed_threshold 0.3 Minimum cosine sim for anchor set
raw_log_score_scale 0.5 Upper bound for cold-start raw-log hit scores
neighbor_expansion / neighbor_decay True / 0.6 Pull in one-hop graph neighbours of anchors, with this edge-weight multiplier
w_association 0.10 Weight of neighbour evidence in the blend
storage_backend "json" "json" (one file) or "sqlite" (incremental saves)
stage2_enabled False Offline LLM refinement after Stage 1
stage2_client / stage2_model "claude-code" / None claude-code (CLI, no key) or anthropic; model alias, None = client default
stage2_max_messages / stage2_min_confidence 60 / 0.5 Messages per LLM call; proposals below this confidence are dropped
correction_window_messages 3 How close an explicit correction must be to supersede a preference/policy
ner_labels PERSON, ORG, GPE, ... spaCy entity labels that become nodes
buffer_flush_interval 30s WAL persistence interval
ann_index_type "none" Reserved ANN index mode
delete_snapshots_on_hard_delete True 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 ledger

Testing

pip install -e ".[dev]"
pytest tests/ -v

All 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, [embeddings]) ~3 MB (+PyTorch ~350 MB) Embedding model
mcp (optional, [mcp]) ~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 CLI

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选