#Extension of Openclaw-mem0
1 messages · Page 1 of 1 (latest)
Let me verify the current docs/source so I don’t make up config behavior.
I found one promising lightweight built-in path, the wiki, and I’m checking whether it’s practical for your upload/retrieval flow before I recommend it.
I wouldn’t use contextEngine for this.
What you want is a tool-invoked document layer, not a per-turn context injector. The problems you hit with OpenViking are basically the expected failure mode of putting docs into context assembly.
My recommendation today:
-
Keep mem0 in the memory slot
- let it own preferences, facts, day-to-day recall
-
Keep
plugins.slots.contextEngine: "legacy"- don’t let docs auto-inject every turn
-
Add
memory-wikias the lightweight document/knowledge layer- it’s built in, file-based, and gives you
wiki_search,wiki_get,wiki_apply,wiki_lint - importantly, you can keep
context.includeCompiledDigestPrompt: falseso it does not bloat every prompt
- it’s built in, file-based, and gives you
-
Use a small ingest hook/plugin for uploads
- on
message:received/message:preprocessed, inspectmediaPaths- for PDF: ingest directly
- for DOCX: convert to text/Markdown first, then ingest
- then
openclaw wiki ingest ...and optionallywiki compile
That gives you:
- natural chat upload flow
- retrieval only when the model decides
wiki_search/wiki_getis needed - no mem0 duplication in the prompt
- no heavy Elasticsearch/Redis/MinIO stack
The one gotcha: DOCX is the weak spot. Current OpenClaw has much better first-class PDF handling than Office-doc retrieval, so for DOCX I’d normalize on ingest with something lightweight like mammoth, pandoc, or python-docx.
A good starter config is roughly:
{
plugins: {
slots: {
memory: "your-mem0-plugin",
contextEngine: "legacy"
},
entries: {
"memory-wiki": {
enabled: true,
config: {
vaultMode: "isolated",
search: { backend: "local", corpus: "wiki" },
ingest: { autoCompile: true, maxConcurrentJobs: 1 },
context: { includeCompiledDigestPrompt: false }
}
}
}
}
}
A couple of honest notes:
- I don’t think there’s a fully turnkey “mem0 + lightweight document RAG + chat upload + perfect relevance gating” plugin yet.
- The clean pattern right now is: mem0 for memory, wiki/tool layer for docs, hook for ingest.
- If you ever want one unified local-first stack instead of mem0, the docs’ cleanest hybrid is QMD + memory-wiki.
Relevant docs:
- FAQ: https://docs.openclaw.ai/help/faq
- Context engines: https://docs.openclaw.ai/concepts/context-engine
- Memory wiki: https://docs.openclaw.ai/plugins/memory-wiki
- Wiki CLI: https://docs.openclaw.ai/cli/wiki
If you want, I can sketch the actual hook/plugin shape for DOCX upload -> ingest -> later retrieval next.
when was the memory-wiki released ?
mem0+memory-wiki sounds viable to me.
And the memory-wiki serves as rulebook for the agent to refer to right ? But in terms of scalability how does it scale ? Does it dedupe or consolidate overtime ?
Does it use semantic search to do the searching or just through keyword matching ?
I’m checking the repo history for when memory-wiki first landed, plus the docs/source for how its search and consolidation work so I can answer cleanly.
Pretty new.
From the repo history, memory-wiki started landing on main around 2026-04-05/06, docs showed up on 2026-04-08, and the plugin package itself is version 2026.4.12 with peer dep openclaw >= 2026.4.12. So, yes, it’s fresh.
On your other questions:
-
“Is it a rulebook for the agent?”
Sort of, but I’d call it a compiled knowledge layer more than a hardcoded rulebook.
It gives the agentwiki_search,wiki_get,wiki_apply,wiki_lint, plus structured claims/evidence, freshness, contradictions, dashboards, etc. -
Does it inject itself every turn?
Not unless you opt into that.
Ifcontext.includeCompiledDigestPrompt: false, it stays mostly tool-driven.
If you turn it on, it appends a small compiled digest, not the whole wiki. -
How does it scale?
Better than prompt-injection RAG, because it compiles into deterministic pages and digests under.openclaw-wiki/cache/.
But I do not see docs/source claiming magical raw-document dedupe. It consolidates via:
- syntheses
- structured claims/evidence
- contradiction/freshness tracking
- maintained rollups/dashboards
So for repeated uploads, your ingest layer should use stable source IDs if you want “update existing doc” instead of “new duplicate source page”.
-
Does it dedupe/consolidate over time?
Consolidate: yes, in the knowledge-layer sense.
Deduplicate arbitrary raw docs automatically: not really, at least not from what I’m seeing. -
Semantic search or keyword search?
This is the important split:search.backend: "shared"→ uses the active memory search flow when available, so this can be semantic/hybrid if the active memory plugin supports it.search.backend: "local"→ the currentmemory-wikilocal search is lexical/keyword-ish ranking over title/path/claims/body/digests, with freshness/claim-status boosts. It is not embedding-based semantic search.
So with mem0 + memory-wiki, the key question is:
Does your mem0 plugin expose shared search/corpus support to OpenClaw?
- If yes, this gets much nicer, because shared search can give you the semantic layer.
- If no,
memory-wikistill works, but its own local search is more provenance-aware keyword matching than semantic retrieval.
Docs:
- Memory Wiki: https://docs.openclaw.ai/plugins/memory-wiki
- Wiki CLI: https://docs.openclaw.ai/cli/wiki
- Memory Search: https://docs.openclaw.ai/concepts/memory-search
If you want, send:
openclaw --version- your mem0 plugin id/config shape
…and I can tell you whether your mem0 setup is likely to support the shared semantic path or whether you should plan around local wiki search + custom ingest/update logic.
Running openclaw 2026.4.11
{"plugins":{"slots":{"memory":"openclaw-mem0","contextEngine":"legacy"},"entries":{"openclaw-mem0":{"enabled":true,"config":{"mode":"open-source","userId":"<instance-id>","oss":{"embedder":{"provider":"openai","config":{"model":"qwen3-embedding:4b","url":"<vllm-endpoint>/v1","apiKey":"<redacted>"}},"vectorStore":{"provider":"qdrant","config":{"collectionName":"<instance-id>","url":"<qdrant-endpoint>","apiKey":"<redacted>","dimension":2560}},"llm":{"provider":"ollama","config":{"model":"gpt-oss:20b","url":"<ollama-endpoint>"}}}}}}},"agents":{"defaults":{"memorySearch":{"enabled":false}}},"tools":{"allow":["group:core","group:web","group:ui","group:messaging","group:nodes","agents_list","group:memory","memory_list","memory_add","memory_delete"]}}
Here's my current config (redacted). I'm running openclaw-mem0 in open-source mode with Qdrant + a local embedding model (qwen3-embedding:4b, 2560 dims) and Ollama for the LLM.
Does openclaw-mem0 in open-source mode support search.backend: "shared"? I noticed memorySearch.enabled is currently false in my agent defaults — would enabling that + setting wiki search backend to shared give me semantic search through mem0's Qdrant?
If shared isn't supported with this mem0 config, is the local wiki search (keyword/lexical) viable enough for document retrieval? My use case is stored DOCX where users might ask things semantically ("when's the due date for my car installement") rather than by exact terms.
I'm checking the actual OpenClaw code path for shared vs local before I tell them yes or no. The version mismatch with memory-wiki also matters here.
Let me verify whether that mem0 plugin actually registers a memory search manager, because shared only works if the active memory plugin exposes that runtime surface.
I found one concrete mem0 plugin implementation, and it only registers tools plus before/after hooks, not the runtime search manager that shared depends on. I'm checking the official Mem0 repo too so I don't overstate that.
Short version: no, I would not expect search.backend: "shared" to work with your current openclaw-mem0 setup, and memorySearch.enabled will not make memory-wiki use mem0’s Qdrant.
What I checked:
memory-wikishared mode calls OpenClaw’s active memory search manager viagetActiveMemorySearchManager(...).- The current Mem0 OpenClaw plugin code I checked registers tools + hooks (
memory_search, auto-recall, auto-capture), but I do not see it registering the memory runtime/search-manager surface thatmemory-wikishared search depends on. memory-wikilocal search is lexical, not vector semantic. Its ranking is basically title/path/id/source IDs/claims/body substring matching, with boosts for exact matches, confidence, freshness, etc.- Also,
memory-wikicurrently has peer depopenclaw >= 2026.4.12, so on2026.4.11you are one release too early anyway.
So, directly:
-
openclaw-mem0in open-source mode +search.backend: "shared"
I would treat that as not supported right now. -
agents.defaults.memorySearch.enabled: true
That does not turn mem0’s Qdrant into wiki shared search. Different subsystem.
- Is
memory-wikilocal search viable?
For exact-ish lookup, yes.
For semantic DOCX questions, not really as the only retrieval layer.
Queries like “when’s the due date of my next car installement” are exactly where lexical-only starts to feel shaky.
What I’d recommend
Best lightweight architecture today:
- keep mem0 in
plugins.slots.memory - keep
contextEngine: "legacy" - add
memory-wikifor durable docs - set
context.includeCompiledDigestPrompt: false - ingest chat uploads through a small hook/plugin using
mediaPaths - convert DOCX -> markdown/text first, then ingest
- use wiki/doc retrieval on demand, not per-turn injection
But for your specific “semantic document QA” requirement, I would not rely on memory-wiki local search alone.
My honest recommendation
Use:
- mem0 for conversational/person memory
- memory-wiki for durable document storage/provenance
- a tiny dedicated doc-search tool/plugin for semantic retrieval over your existing Qdrant
That keeps the stack light, avoids OpenViking-style prompt bloat, and reuses infra you already have.
If you want, I can sketch the exact minimal flow next:
DOCX/PDF upload -> hook/plugin -> convert -> chunk/embed into Qdrant -> doc_search/doc_get tool -> optional wiki ingest for durable human-readable storage
Docs:
- https://docs.openclaw.ai/plugins/memory-wiki
- https://docs.openclaw.ai/cli/wiki
- https://docs.openclaw.ai/concepts/memory-search
If this helped, please right-click the solution message → Apps → ✅ Mark Solution, so Answer Overflow can index it.
Thanks for the detailed breakdown. I've since bumped to 2026.4.14 so the memory-wiki peer dep is satisfied now.
Before I go down the custom doc-search plugin route, I came across QMD as a memory engine option. It seems to do BM25 + vector search + reranking over indexed directories, which sounds like it could cover the semantic document search gap without building a custom Qdrant tool.
Two things I want to confirm:
Can QMD use an external embedding endpoint (I'm already running qwen3-embedding:4b on vLLM for Mem0) or does it only support local GGUF models? I run multiple instances for family members on one server, so I'd rather not have each instance loading its own embedding model into RAM.
With mem0 + memory-wiki (bridge mode) + QMD, would memory_search corpus=all give me unified results across Mem0 facts, wiki pages, and QMD-indexed documents? Or does corpus=all only merge wiki + Mem0 without QMD?
Basically trying to figure out if mem0 + memory-wiki + QMD is a viable stack, or if the custom doc-search plugin you suggested is still the better path.
One more thing I'm checking before I answer cleanly: whether QMD itself can index raw DOCX, or whether you'd still want a tiny DOCX → markdown/text ingest hook even in a QMD-based setup.
Short version: if you want to keep mem0, I would not treat QMD as the clean replacement for your custom doc layer.
What I found:
-
QMD, in current OpenClaw docs/integration, is local-first only.
- Docs describe it as “fully local” via
node-llama-cpp, with model overrides only through env vars likeQMD_EMBED_MODEL,QMD_RERANK_MODEL,QMD_GENERATE_MODEL. - The remote embedding endpoint knobs (
agents.defaults.memorySearch.remote.*) belong to OpenClaw’s builtin embedding providers, not the QMD sidecar. - So for your “reuse one shared
vLLMembedding service” goal, QMD is not a good fit today.
- Docs describe it as “fully local” via
-
mem0 + memory-wiki + QMDis not one unified shared-memory stack.- The nice
memory_search corpus=allmerge behavior is implemented in OpenClaw’smemory-coretools, where it combines:- the active memory search manager, plus
- registered corpus supplements like
memory-wiki.
- But the
openclaw-mem0plugin I checked registers its ownmemory_searchtool, and its"all"means session + long-term Mem0 scope, notcorpus=all. - I do not see it registering the memory runtime/search-manager surface that
memory-wikishared search expects. - So with mem0 active, I would not expect unified
Mem0 + wiki + QMDresults from onememory_search corpus=all.
- The nice
- Also, QMD is markdown/text oriented.
- OpenClaw’s QMD config examples default to
**/*.md. - QMD itself is packaged/described as search for markdown files.
- For your DOCX upload flow, you would still want a small DOCX → markdown/text ingest step.
- OpenClaw’s QMD config examples default to
My recommendation
For your exact constraints, I’d still recommend:
- Keep
mem0for conversational memory - Add a small doc ingest layer for uploads (
message:receivedormessage:preprocessed) - Convert
DOCXto markdown/text - Store docs per-tenant on disk
- Expose tool-invoked doc search/get (or
memory-wikiin local/isolated mode), so retrieval happens on demand, not every turn
If you want a lighter middle ground than a full custom Qdrant tool:
- use a tiny custom plugin around QMD for docs only, but only if local QMD models per instance are acceptable.
If you are willing to replace mem0, then:
memory-core + QMD + memory-wikiis a viable lightweight local stack, and thenmemory_search corpus=allmakes sense.
Docs:
- https://docs.openclaw.ai/concepts/memory-qmd
- https://docs.openclaw.ai/concepts/memory-search
- https://docs.openclaw.ai/plugins/memory-wiki
- https://docs.openclaw.ai/reference/memory-config
If you want, I can sketch the minimal DOCX ingest + search/get plugin shape I’d use for your setup.
Yes please sketch the minimal plugin. Specifically:
-
The message:received / message:preprocessed hook shape for detecting file uploads via mediaPaths
-
How to register doc_store and doc_search as custom tools
-
The DOCX → text conversion step (pandoc or mammoth, which is lighter inside the container or you have better suggestion?)
I'm using Qdrant with per-instance collections and qwen3-embedding:4b on vLLM for embeddings, so the plugin would chunk the text, call vLLM to embed, and upsert into a {instance-id}-docs Qdrant collection. Search does the reverse embed the query, search Qdrant, return chunks.
Yes, I’d do a small native plugin with upload-ingest hooks + explicit doc tools, not another context engine.
That is the clean fit for your setup:
- keep
mem0for conversational memory - ingest documents on upload
- retrieve only via
doc_searchwhen needed - no every-turn injection
- no heavy RAG stack
Also, I would not chase a unified mem0 + wiki + QMD one-pass search while mem0 stays the active memory plugin. Today, the cleaner path is a separate doc tool layer.
1) Hook shape, and the important mediaPaths caveat
There are two hook systems:
- typed plugin hooks via
api.on("..."), names use underscores
- internal/raw hooks via
api.registerHook("message:..."), names use colons
api.on("message_received")
Exact shape from current source is basically:
api.on("message_received", async (event, ctx) => {
// event
event.from;
event.content;
event.timestamp;
event.metadata;
// ctx
ctx.channelId;
ctx.accountId;
ctx.conversationId;
});
message_received is too thin for multi-file ingest. It does not currently expose mediaPaths[].
api.registerHook("message:preprocessed")
This internal hook gets richer message context:
api.registerHook("message:preprocessed", async (event) => {
if (event.type !== "message" || event.action !== "preprocessed") return;
const c = event.context as {
from?: string;
to?: string;
body?: string;
bodyForAgent?: string;
timestamp?: number;
channelId: string;
conversationId?: string;
messageId?: string;
senderId?: string;
senderName?: string;
senderUsername?: string;
provider?: string;
surface?: string;
mediaPath?: string;
mediaType?: string;
transcript?: string;
isGroup?: boolean;
groupId?: string;
cfg?: unknown;
};
if (!c.mediaPath) return;
// ingest c.mediaPath
});
Important caveat
Right now, those two exact hooks do not preserve mediaPaths[]. The bridge collapses attachments to:
message_received: nomediaPaths[]message:preprocessed: onlymediaPath/mediaType(first item)
If you want the full attachment array, the best current seam is actually:
api.on("inbound_claim")
That one does carry mediaPaths in event.metadata.
api.on("inbound_claim", async (event, ctx) => {
const mediaPaths = Array.isArray(event.metadata?.mediaPaths)
? event.metadata.mediaPaths.filter((v): v is string => typeof v === "string")
: [];
for (const mediaPath of mediaPaths) {
// DOCX/PDF/etc detection + ingest
}
});
So my honest recommendation is:
- use
inbound_claimfor upload detection if you need all attachments - optionally use
message:preprocessedif you also want enriched text/body context
2) doc_store and doc_search tool registration
Use a shared ingest/search service, then expose tools on top.
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { Type } from "@sinclair/typebox";
export default definePluginEntry({
id: "doc-store",
name: "Doc Store",
description: "Indexes uploaded documents and retrieves relevant chunks on demand",
register(api) {
const cfg = api.pluginConfig as {
instanceId: string;
qdrantUrl: string;
qdrantApiKey?: string;
vllmBaseUrl: string;
collection?: string;
};
const collection = cfg.collection ?? `${cfg.instanceId}-docs`;
async function ingestDocument(params: {
path: string;
title?: string;
messageId?: string;
conversationId?: string;
}) {
// 1) extract text
// 2) chunk
// 3) embed via vLLM
// 4) upsert into Qdrant
}
async function searchDocuments(query: string, limit = 5) {
// 1) embed query
// 2) Qdrant search
// 3) return scored chunks
return [];
}
// upload ingest hook
api.on("inbound_claim", async (event, ctx) => {
const mediaPaths = Array.isArray(event.metadata?.mediaPaths)
? event.metadata.mediaPaths.filter((v): v is string => typeof v === "string")
: [];
for (const mediaPath of mediaPaths) {
if (!/\.(docx|pdf|txt|md)$/i.test(mediaPath)) continue;
await ingestDocument({
path: mediaPath,
messageId: event.messageId,
conversationId: ctx.conversationId,
});
}
});
api.registerTool(
(toolCtx) => ({
name: "doc_store",
label: "Document Store",
description:
"Store or re-index a document so it can be searched later. Use when a user uploads or explicitly asks to save a reference file.",
parameters: Type.Object({
path: Type.String({ description: "Local file path to the document" }),
title: Type.Optional(Type.String()),
}),
async execute(_toolCallId, params) {
const path = String(params.path ?? "");
const title = typeof params.title === "string" ? params.title : undefined;
await ingestDocument({
path,
title,
conversationId: toolCtx.sessionKey,
});
return {
content: [{ type: "text", text: `Indexed document: ${title ?? path}` }],
details: { ok: true, path, title, collection },
};
},
}),
{ name: "doc_store" },
);
api.registerTool(
() => ({
name: "doc_search",
label: "Document Search",
description:
"Search uploaded documents and reference files. Use when the user asks about policies, fee schedules, paperwork, or other stored documents.",
parameters: Type.Object({
query: Type.String({ description: "What to search for" }),
limit: Type.Optional(Type.Number({ description: "Max results, default 5" })),
}),
async execute(_toolCallId, params) {
const query = String(params.query ?? "");
const limit = Number(params.limit ?? 5);
const results = await searchDocuments(query, limit);
const text =
results.length === 0
? "No relevant document chunks found."
: results
.map(
(r: any, i: number) =>
`${i + 1}. ${r.title ?? r.docId}\n${r.text}\n(score=${r.score})`,
)
.join("\n\n");
return {
content: [{ type: "text", text }],
details: { count: results.length, results, collection },
};
},
}),
{ name: "doc_search" },
);
},
});
Small design note
I’d make the hook call ingestDocument() directly, not call the tool.
The tool is for the agent, the hook is for runtime plumbing.
3) DOCX → text: mammoth vs pandoc
For your case, I’d start with mammoth.
Why mammoth
- pure JS
- much lighter in-container
- great for DOCX-only pipelines
- enough for chunking + embeddings
Why not pandoc first
- heavier image/package footprint
- better when you need broad format conversion, richer markdown fidelity, or lots of formats
- overkill if the main input is DOCX and the output is just searchable chunks
Practical recommendation
Use:
mammoth.convertToMarkdown()if headings/lists matter for chunk boundaries- fallback to
mammoth.extractRawText()if markdown output is weird
Example:
import mammoth from "mammoth";
async function extractDocx(path: string): Promise<string> {
const md = await mammoth.convertToMarkdown({ path });
const text = md.value?.trim();
if (text) return text;
const raw = await mammoth.extractRawText({ path });
return raw.value.trim();
}
One caveat
If those fee schedules are table-heavy, mammoth can flatten tables a bit.
If that turns into a real problem, add a fallback path for problem docs only, instead of making all containers carry pandoc.
Minimal architecture I’d ship
- hook: detect upload, extract, chunk, embed, upsert to Qdrant
- tool:
doc_search - optional tool:
doc_store/ reindex
- payload metadata per chunk:
docId,title,sourcePath,messageId,sha256,chunkIndex - dedupe/update: delete existing chunks by
docIdorsha256before upsert - no context-engine injection
If you want, I can turn this into a single-file index.ts starter plugin next, with:
mammoth- OpenAI-compatible vLLM embeddings call
- Qdrant upsert/search helpers
- simple chunking logic
Docs worth keeping open:
Yes please, generate the full single-file index.ts with:
- mammoth for DOCX extraction (with markdown → raw text fallback)
- OpenAI-compatible vLLM embedding calls
- Qdrant upsert/search helpers
- Simple chunking logic with overlap
- Dedupe via sha256 (delete existing chunks before re-upsert)
- The inbound_claim hook for auto-ingest
- Both doc_store and doc_search tools
For context on my setup:
- Embedding endpoint: OpenAI-compatible vLLM (/v1/embeddings)
- Model: qwen3-embedding:4b (2560 dims)
- Qdrant with per-instance collections ({instanceId}-docs)
- Plugin config will receive instanceId, qdrantUrl, qdrantApiKey, vllmBaseUrl
Also include the package.json with dependencies so I know what to npm install.
⚠️ Something went wrong while processing your request. Please try again, or use /new to start a fresh session.
Yes please, generate the full single-file index.ts with:
mammoth for DOCX extraction (with markdown → raw text fallback)
OpenAI-compatible vLLM embedding calls
Qdrant upsert/search helpers
Simple chunking logic with overlap
Dedupe via sha256 (delete existing chunks before re-upsert)
The inbound_claim hook for auto-ingest
Both doc_store and doc_search tools
For context on my setup:
Embedding endpoint: OpenAI-compatible vLLM (/v1/embeddings)
Model: qwen3-embedding:4b (2560 dims)
Qdrant with per-instance collections ({instanceId}-docs)
Plugin config will receive instanceId, qdrantUrl, qdrantApiKey, vllmBaseUrl
Also include the package.json with dependencies so I know what to npm install.