← Back to Plugins
Voice

Mem0 Multi Pool

jamebobob By jamebobob 👁 14 views ▲ 0 votes

N:M memory pool routing for @mem0/openclaw-mem0. Per-agent capture/recall with boundary enforcement and fail-closed defaults.

GitHub

Configuration Example

{
  "agentMemory": {
    "main": {
      "capture": "private",
      "recall": ["private", "shared"]
    },
    "social": {
      "capture": "shared",
      "recall": ["shared"]
    }
  }
}

README

# openclaw-mem0-multi-pool

Per-agent memory pool isolation for the `@mem0/openclaw-mem0` plugin. Adds configurable N:M capture/recall routing so different agents can share some memory pools while keeping others private.

Built for [openclaw-agent-privacy](https://github.com/jamebobob/openclaw-agent-privacy). Works standalone if you just need pool routing without the full privacy framework.

## What It Does

Stock `openclaw-mem0` uses a single `userId` for all agents. Every memory goes to one pool, every agent recalls from that pool. In a multi-agent setup, your private DM assistant and your group chat agent see the same memories.

This patch adds an `agentMemory` config block that maps each agent to its own capture pool and recall list:

```json
{
  "agentMemory": {
    "main": {
      "capture": "private",
      "recall": ["private", "shared"]
    },
    "social": {
      "capture": "shared",
      "recall": ["shared"]
    }
  }
}
```

Main agent captures to "private", recalls from both. Social agent captures to "shared", recalls from "shared" only. Social agent cannot access the "private" pool through any code path.

## Features

- **N:M pool routing.** Not just 1:1 silos. Configure any agent to read from any combination of pools.
- **Boundary enforcement on every tool.** All five memory tools (`memory_search`, `memory_store`, `memory_list`, `memory_get`, `memory_forget`) validate `userId` and `memoryId` parameters against the agent's allowed pools. A tool call targeting a pool outside the agent's recall list returns "Access denied" from the code, not from model judgment.
- **Fail-closed defaults.** Unknown agent identity gets `undefined` capture pool, empty recall list, `false` from every authorization check. No fallback to a default pool.
- **Race condition mitigation.** Concurrent agent turns can overwrite module-level state. The patch uses `ctx.agentId` from hook context, a `before_tool_call` hook to refresh agent identity before each tool execution, and synchronous snapshots in every tool handler before any `await`.
- **Provenance metadata.** Every captured memory is tagged with `is_private`, `source_channel`, `conversation_type`, `chat_id`, and `agent_id`. Not used for filtering today (pool routing handles that), but available for forensics and future trust scoring.
- **No pool labels in output.** The model never sees which pool a memory came from. The `<relevant-memories>` injection contains plain memory text with no pool identifiers. Labels are a leakage vector.
- **Backwards compatible.** Without `agentMemory` in config, the plugin behaves identically to stock. All helpers fall back to `cfg.userId`.

## Requirements

- OpenClaw v2026.3.2+ (needs `before_tool_call` hook with `ctx.agentId`)
- `@mem0/openclaw-mem0` v0.1.2 (the patch targets this version's `index.ts`)
- Qdrant (for payload indexes and provenance metadata)

## Installation

### 1. Backup

```bash
cp ~/.openclaw/extensions/openclaw-mem0/index.ts ~/.openclaw/extensions/openclaw-mem0/index.ts.bak
cp ~/.openclaw/extensions/openclaw-mem0/openclaw.plugin.json ~/.openclaw/extensions/openclaw-mem0/openclaw.plugin.json.bak
```

### 2. Migrate Existing Memories

If you have existing Qdrant memories, tag them before applying the patch. See [qdrant-migration.md](qdrant-migration.md) for the full guide.

```bash
# Create payload indexes
python3 create-indexes.py

# Tag all existing memories as private (conservative default)
python3 migrate-memories.py
```

### 3. Apply Patches

The patches are applied sequentially. Each builds on the previous:

```bash
# v3: Core multi-pool architecture (15 patches)
python3 patches/multipool-patch-v3.py --dry-run
python3 patches/multipool-patch-v3.py

# v4: Race condition fix + fail-closed defaults (15 patches)
python3 patches/multipool-patch-v4.py --dry-run
python3 patches/multipool-patch-v4.py

# v5: Hardening (10 patches)
python3 patches/multipool-patch-v5.py --dry-run
python3 patches/multipool-patch-v5.py
```

Always dry-run first. If any patch reports "anchor not found", your `index.ts` version may differ from v0.1.2. Each patch verifies its anchors match exactly once and runs a final verification check on all expected code markers.

### 4. Patch the Plugin Manifest

The gateway validates plugin config against `openclaw.plugin.json` before plugin code loads. The `agentMemory` key must be declared in both places.

```bash
jq '.configSchema.properties.agentMemory = {
  "type": "object",
  "additionalProperties": {
    "type": "object",
    "properties": {
      "capture": {"type": "string"},
      "recall": {"type": "array", "items": {"type": "string"}}
    },
    "required": ["capture", "recall"]
  }
}' ~/.openclaw/extensions/openclaw-mem0/openclaw.plugin.json > /tmp/plugin-patched.json \
  && mv /tmp/plugin-patched.json ~/.openclaw/extensions/openclaw-mem0/openclaw.plugin.json
```

### 5. Add Config

Add `agentMemory` to your `openclaw.json` under `plugins.entries.openclaw-mem0.config`:

```bash
jq '.plugins.entries["openclaw-mem0"].config.agentMemory = {
  "main": {"capture": "private", "recall": ["private", "shared"]},
  "social": {"capture": "shared", "recall": ["shared"]}
}' ~/.openclaw/openclaw.json > /tmp/oc-patched.json \
  && mv /tmp/oc-patched.json ~/.openclaw/openclaw.json
```

Adjust pool names and agent IDs to match your setup.

### 6. Restart

```bash
sudo systemctl restart openclaw
```

### 7. Verify

```bash
journalctl -u openclaw --since "2 min ago" --no-pager | grep mem0
```

You should see pool names in the log output:

```
openclaw-mem0: injecting 5 memories into context (pools: private, shared)
openclaw-mem0: auto-captured 1 memories (pool: private)
openclaw-mem0: auto-captured 3 memories (pool: shared)
```

If main shows `(pools: private, shared)` and social shows `(pools: shared)`, pool routing is working.

## What the Patches Change

40 targeted `str.replace` operations across three scripts. No lines are deleted, only modified or added.

**v3 (15 patches):** Core architecture.
- Adds `agentMemory` to config type, ALLOWED_KEYS, and config parser
- Adds `metadata` passthrough to `OSSProvider.add()` and `buildAddOptions()`
- Adds helper functions: `extractSessionInfo()`, `getCapturePool()`, `getRecallPools()`, `isPoolAllowed()`
- Rewrites all 5 memory tools with pool-aware routing and boundary validation
- Rewrites `before_agent_start` (recall) and `agent_end` (capture) hooks with pool routing and provenance
- Adds cron session key handling

**v4 (15 patches):** Race condition and fail-closed.
- Adds `currentAgentId` module-level variable
- Adds `before_tool_call` hook to refresh `currentAgentId` from `ctx.agentId`
- Adds synchronous `const agentId = currentAgentId` snapshot as first line of every tool handler
- Changes `getCapturePool()` to return `undefined` when agent unknown
- Changes `getRecallPools()` to return `[]` when agent unknown
- Changes `isPoolAllowed()` to return `false` when agent unknown
- Adds guards in `before_agent_start`, `agent_end`, and `memory_store` for undefined pools

**v5 (10 patches):** Hardening.
- Guards session-scope pool lookups in `memory_search` and `memory_list` against undefined pools
- Snapshots `ctx.sessionKey` in `agent_end` for race-safe provenance metadata
- Adds `isPoolAllowed` check before auto-delete in `memory_forget` by query

Each patch creates a backup (`.bak.multipool-v3`, `-v4`, `-v5`).

## npm Update Warning

Updating `@mem0/openclaw-mem0` via npm will overwrite `index.ts` and `openclaw.plugin.json`, destroying all patches. After any npm update:

1. Check if patches are gone (`grep "isPoolAllowed" ~/.openclaw/extensions/openclaw-mem0/index.ts`)
2. If gone, reapply all three patch scripts in order
3. Re-patch `openclaw.plugin.json` (Step 4 above)
4. Verify a fresh Qdrant point still has provenance metadata in its payload

## How Pool Routing Works

Pool identity comes from `user_id` in Mem0's API. Setting `user_id: "private"` vs `user_id: "shared"` creates separate vector spaces in Qdrant.

**Capture:** When a memory is stored, `getCapturePool(agentId)` determines which `user_id` to use. Main agent stores to "private", social stores to "shared".

**Recall:** When memories are searched, `getRecallPools(agentId)` returns the list of pools to query. Main agent searches both "private" and "shared", social searches only "shared". Results are merged, deduplicated by ID, sorted by relevance, and capped at `topK`.

**Boundary enforcement:** Every tool parameter that accepts a `userId` or `memoryId` is checked against `isPoolAllowed()` before the operation proceeds. A social agent calling `memory_search({ query: "secrets", userId: "private" })` gets rejected at the code level.

## Design Decisions

| Decision | Choice | Reasoning |
|----------|--------|-----------|
| Pool discriminator | Mem0 `user_id` | Creates separate Qdrant namespaces. No vendor patching needed. |
| Two searches vs MatchAny | Two parallel searches per pool | Simpler. No Qdrant client dependency for MatchAny. Optimize later if latency matters. |
| Pool labels in output | None | Filtering already happened. Labels give the model information it doesn't need and create a leakage vector. |
| topK across pools | Global cap after merge | Prevents inflated results for single-pool agents. |
| Fail-closed default | Empty pools, undefined capture | Unknown agent gets zero access, not maximum access. |
| Race condition fix | before_tool_call + snapshot | `ctx.agentId` is turn-scoped. Snapshot before any `await` prevents concurrent overwrite. |
| Metadata passthrough | Undocumented but verified | Mem0 OSS `add()` destructures `metadata` from config and spreads it into Qdrant payload. Confirmed in source. |

## Audit History

This patch went through a six-layer review before deployment:

1. **Design + v1:** Opus chat. Architecture, first draft, self-audit.
2. **Mechanical audit:** Claude Code. Ran patches against real source. Found 4 blockers: 5 whitespace anchor mismatches, 3 tool-level pool 

... (truncated)
voice

Comments

Sign in to leave a comment

Loading comments...