MemCraft is an OpenClaw memory integration plugin. Its goal is to connect major LLM Memory baselines to OpenClaw and provide a local-first, reproducible, and extensible unified memory framework.
- Table of Contents
- Use open-source memory systems locally.
- Reduce privacy risks by not relying on cloud memory services.
- DIY your own memory system based on your needs.
- Quickly plug in memory baselines proposed in research.
- Evaluate baseline performance in real OpenClaw interaction scenarios.
- Identify limitations of existing methods through a unified interface and improve them.
- MemCraft/: OpenClaw lifecycle plugin (Node.js)
- MemoryServer/: memory backend service
- bm25_memory: lightweight local BM25 + LLM-compressed storage
- amem_memory: A-Mem
- mem0_memory: mem0
- memoryos_memory: MemoryOS
The baseline implementations are reproduced with reference to MemoryBench.
git clone https://github.com/bebr2/OpenClaw-MemCraft.git
cd OpenClaw-MemCraftcd MemoryServer
pip install -r requirements.txt
cp .env.bm25 .env
python app.pyDefault URL: http://127.0.0.1:8765
openclaw plugins install memcraft-openclaw-plugin@latestCopy the entire ./memcraft-openclaw-plugin folder into the OpenClaw extension directory (usually ~/.openclaw/extensions/). Then add this in OpenClaw plugin config (usually ~/.openclaw/openclaw.json):
{
"plugins": {
"installs": {
"memcraft-openclaw-plugin": {
"source": "path",
"installPath": "path/to/.openclaw/extensions/memcraft-openclaw-plugin"
}
},
"entries": {
"memcraft-openclaw-plugin": {
"enabled": true
}
}
}
}For both install methods, ensure this exists in ~/.openclaw/openclaw.json:
{
"gateway": {
"http": {
"endpoints": {
"chatCompletions": {
"enabled": true
}
}
}
},
}Then restart gateway:
openclaw gateway restart- MemoryServer/.env.bm25
- MemoryServer/.env.amem
- MemoryServer/.env.mem0
- MemoryServer/.env.memoryos
- MemCraft/.env.memcraft-plugin
Add plugin env configuration into OpenClaw .env (usually ~/.openclaw/.env; create it if missing). Restart OpenClaw gateway after any plugin config change.
- MEMORY_STORE_MODULE: choose baseline (
bm25_memory/amem_memory/mem0_memory/memoryos_memory) - MEMORY_SERVER_HOST, MEMORY_SERVER_PORT: service bind address
- MEMORY_DATA_DIR: memory data directory
- MEMORY_TOP_K: retrieval size
- MEMORY_RETRIEVE_MODEL: embedding retrieval model (for stores that support embeddings)
- MEMORY_LLM_GATEWAY_BASE_URL, MEMORY_LLM_GATEWAY_TOKEN: gateway endpoint and auth
- MEMORY_LLM_AGENT_ID: compression agent id (recommended to align with plugin exclusion list)
- MEMCRAFT_SERVER_URL: backend URL
- MEMCRAFT_TOP_K: retrieval size
- MEMCRAFT_STORE_GRANULARITY:
session_endoragent_end, defines when memory storage is triggered - MEMCRAFT_NAMESPACE: memory namespace
- MEMCRAFT_EXCLUDE_AGENT_IDS: exclude agents from memory hooks (for example
memory-compressor) - MEMCRAFT_STRIP_HISTORY_MEMORY: strip injected memory blocks from history; default true to save tokens and keep only current-turn-related memory
- OpenClaw triggers plugin at
before_prompt_build. - Plugin calls
POST /retrieveto getmemory_context. memory_contextis injected into prompt before main model execution.- At
session_endoragent_end, plugin callsPOST /store. - Backend compresses and persists memory.
MemoryServer includes a lightweight dashboard for memory inspection and debugging:
- Entry:
GET / - Data source:
GET /memories
The dashboard includes:
- Total Documents
- Avg Doc Length
- Recent 7-day memory trend chart
- Recent memory list (session/source/time/category, depending on store implementation)
DIY lets you connect your own memory algorithm into real OpenClaw conversation flow, for example:
- Research: compare retrieval strategies in real multi-turn conversations
- Engineering: replace storage backend (JSONL/SQLite/vector DB/graph DB), or customize storage intervals (for example store at multi-turn boundaries rather than each turn/session)
- Product: add domain-specific memory (task memory, user profile memory, workflow state memory)
The plugin does not need to know your internal implementation. As long as your store follows the interface contract, the system works.
Call chain:
- OpenClaw plugin calls
POST /retrievebefore conversation turn - MemoryServer route calls
store.retrieve(...) - Plugin injects returned
memory_contextinto prompt - At turn/session end, plugin calls
POST /store - MemoryServer route calls
store.store(...) - Dashboard/debug page calls
GET /memories - MemoryServer route calls
store.list_memories(...)
Required methods:
-
retrieve(conversation, top_k, filters)Purpose: extract query from current conversation and return prompt-injectable text.conversation["prompt"]contains full prompt for LLM;conversation["messages"]contains message history. Called by:POST /retrieveKey return fields:query,memory_context -
store(conversation, metadata)Purpose: persist current conversation content.conversation["messages"]contains messages since last store event. Called by:POST /storeKey return fields:stored=true/false,item_id(or equivalent id) -
list_memories(limit, namespace)Purpose: provide memory list for dashboard/debug endpoints. Called by:GET /memoriesSuggestion: return stable dict rows including timestamp/content/namespace. -
_load_memories()Purpose: load persisted memories into in-memory structures/indexes on startup. Called by: store initialization. Typical implementation: restore from jsonl/sqlite/vector DB. -
_save_memories(...)Purpose: flush changes to persistent storage after store operations. Called by: typically inside yourstoreimplementation. Typical implementation: append jsonl, write sqlite, write vector DB and flush.
Create your_memory.py under MemoryServer/memory:
from __future__ import annotations
import json
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from .base_memory import BaseMemory
class YourMemory(BaseMemory):
def __init__(
self,
data_dir: str,
llm_client=None,
default_top_k: int = 5,
context_max_chars: int = 4000,
**kwargs,
) -> None:
self.data_dir = Path(data_dir)
self.data_dir.mkdir(parents=True, exist_ok=True)
self.memory_file = self.data_dir / "your_memory.jsonl"
self.llm_client = llm_client
self.default_top_k = max(1, int(default_top_k))
self.context_max_chars = max(500, int(context_max_chars))
self.documents: list[dict[str, Any]] = []
self._load_memories()
def retrieve(
self,
conversation: dict[str, Any],
top_k: int = 5,
filters: dict[str, Any] | None = None,
) -> dict[str, Any]:
query = self._default_extract_retrieve_query(conversation)
if not query:
return {"query": "", "memory_context": ""}
namespace = (filters or {}).get("namespace")
rows = self.documents
if namespace:
rows = [x for x in rows if x.get("namespace") == namespace]
picked = list(reversed(rows))[: max(1, int(top_k or self.default_top_k))]
context = "\n".join(f"- {x.get('summary', '')}" for x in picked).strip()
if len(context) > self.context_max_chars:
context = context[: self.context_max_chars] + "..."
return {
"query": query,
"memory_context": context,
}
def store(self, conversation: dict[str, Any], metadata: dict[str, Any] | None = None) -> dict[str, Any]:
namespace = conversation.get("namespace") or "default"
messages = conversation.get("messages") or []
if not messages:
return {"stored": False, "reason": "empty_messages"}
text = "\n".join(str(m.get("content") or "") for m in messages).strip()
item = {
"id": str(uuid.uuid4()),
"created_at": datetime.now(timezone.utc).isoformat(),
"namespace": namespace,
"session_id": conversation.get("session_id"),
"summary": text[:200],
"compressed_memory": text[:1000],
"meta": metadata or {},
}
self.documents.append(item)
self._save_memories(item=item)
return {"stored": True, "item_id": item["id"]}
def list_memories(self, limit: int = 200, namespace: str | None = None) -> list[dict[str, Any]]:
rows = self.documents
if namespace:
rows = [x for x in rows if x.get("namespace") == namespace]
rows = rows[-max(1, int(limit)):]
return list(reversed(rows))
def _load_memories(self) -> None:
if not self.memory_file.exists():
self.documents = []
return
rows: list[dict[str, Any]] = []
with self.memory_file.open("r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
try:
rows.append(json.loads(line))
except json.JSONDecodeError:
continue
self.documents = rows
def _save_memories(self, **kwargs: Any) -> None:
item = kwargs["item"]
with self.memory_file.open("a", encoding="utf-8") as f:
f.write(json.dumps(item, ensure_ascii=False) + "\n")Set in MemoryServer/.env:
MEMORY_STORE_MODULE=your_memoryMEMORY_STORE_CLASS=YourMemory(required only if multiple store classes exist in one module)
Then restart the service.
- Keep
retrieveoutput concise and readable; avoid injecting full raw history back into prompt. - Add noise filtering and deduplication in
store, otherwise memory quality degrades quickly. - Keep
list_memoriesfields stable for better UI/debug compatibility. - Add resilience in
_load/_saveso one corrupt row does not break full load.
GET /healthreturnsok=truePOST /retrieveis triggered during conversationPOST /storeis triggered at turn/session endGET /memoriesshows persisted rows