Skip to main content
Glama

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)

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 report

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
A
license - permissive license
Not graded
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

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

  • A
    license
    Not graded
    quality
    D
    maintenance
    A 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.
    23
    1
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    A 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.
    14
    MIT

View all related MCP servers

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.

View all MCP Connectors

Latest Blog Posts

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