#Extension of Openclaw-mem0

1 messages · Page 1 of 1 (latest)

devout adderBOT
gusty bronzeBOT
#

To help others find answers, you can mark your question as solved via Right click solution message -> Apps -> âś… Mark Solution

modern lintelBOT
#

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:

  1. Keep mem0 in the memory slot

    • let it own preferences, facts, day-to-day recall
  2. Keep plugins.slots.contextEngine: "legacy"

    • don’t let docs auto-inject every turn
  3. Add memory-wiki as 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: false so it does not bloat every prompt
  4. Use a small ingest hook/plugin for uploads

#
  • on message:received / message:preprocessed, inspect mediaPaths
    • for PDF: ingest directly
    • for DOCX: convert to text/Markdown first, then ingest
    • then openclaw wiki ingest ... and optionally wiki compile

That gives you:

  • natural chat upload flow
  • retrieval only when the model decides wiki_search / wiki_get is 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:

bronze pebble
#

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 ?

modern lintelBOT
#

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 agent wiki_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.
    If context.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 current memory-wiki local 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-wiki still works, but its own local search is more provenance-aware keyword matching than semantic retrieval.

Docs:

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.

bronze pebble
#

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.

modern lintelBOT
#

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-wiki shared mode calls OpenClaw’s active memory search manager via getActiveMemorySearchManager(...).
  • 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 that memory-wiki shared search depends on.
  • memory-wiki local 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-wiki currently has peer dep openclaw >= 2026.4.12, so on 2026.4.11 you are one release too early anyway.

So, directly:

  1. openclaw-mem0 in open-source mode + search.backend: "shared"
    I would treat that as not supported right now.

  2. agents.defaults.memorySearch.enabled: true
    That does not turn mem0’s Qdrant into wiki shared search. Different subsystem.

#
  1. Is memory-wiki local 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-wiki for 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:

bronze pebble
#

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.

modern lintelBOT
#

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 like QMD_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 vLLM embedding service” goal, QMD is not a good fit today.
  • mem0 + memory-wiki + QMD is not one unified shared-memory stack.

    • The nice memory_search corpus=all merge behavior is implemented in OpenClaw’s memory-core tools, where it combines:
      1. the active memory search manager, plus
      2. registered corpus supplements like memory-wiki.
    • But the openclaw-mem0 plugin I checked registers its own memory_search tool, and its "all" means session + long-term Mem0 scope, not corpus=all.
    • I do not see it registering the memory runtime/search-manager surface that memory-wiki shared search expects.
    • So with mem0 active, I would not expect unified Mem0 + wiki + QMD results from one memory_search corpus=all.
#
  • 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.

My recommendation

For your exact constraints, I’d still recommend:

  1. Keep mem0 for conversational memory
  2. Add a small doc ingest layer for uploads (message:received or message:preprocessed)
  3. Convert DOCX to markdown/text
  4. Store docs per-tenant on disk
  5. Expose tool-invoked doc search/get (or memory-wiki in 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-wiki is a viable lightweight local stack, and then memory_search corpus=all makes sense.

Docs:

If you want, I can sketch the minimal DOCX ingest + search/get plugin shape I’d use for your setup.

bronze pebble
#

Yes please sketch the minimal plugin. Specifically:

  1. The message:received / message:preprocessed hook shape for detecting file uploads via mediaPaths

  2. How to register doc_store and doc_search as custom tools

  3. 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.

modern lintelBOT
#

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 mem0 for conversational memory
  • ingest documents on upload
  • retrieve only via doc_search when 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: no mediaPaths[]
  • message:preprocessed: only mediaPath / 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_claim for upload detection if you need all attachments
  • optionally use message:preprocessed if 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 docId or sha256 before 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:

bronze pebble
#

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.

modern lintelBOT
#

⚠️ Something went wrong while processing your request. Please try again, or use /new to start a fresh session.

bronze pebble
#

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.