feat(openclaw): add per-agent memory isolation for multi-agent setups (#4245)
This commit is contained in:
+30
-4
@@ -30,6 +30,26 @@ The agent tools (`memory_search`, `memory_list`) accept a `scope` parameter (`"s
|
||||
|
||||
All new parameters are optional and backward-compatible — existing configurations work without changes.
|
||||
|
||||
### Per-agent memory isolation
|
||||
|
||||
In multi-agent setups, each agent automatically gets its own memory namespace. Session keys following the pattern `agent:<agentId>:<uuid>` are parsed to derive isolated namespaces (`${userId}:agent:${agentId}`). Single-agent deployments are unaffected — plain session keys and `agent:main:*` keys resolve to the configured `userId`.
|
||||
|
||||
**How it works:**
|
||||
|
||||
- The agent's session key is inspected on every recall/capture cycle
|
||||
- If the key matches `agent:<name>:<uuid>`, memories are stored under `userId:agent:<name>`
|
||||
- Different agents never see each other's memories unless explicitly queried
|
||||
|
||||
**Explicit cross-agent queries:**
|
||||
|
||||
All memory tools (`memory_search`, `memory_store`, `memory_list`, `memory_forget`) accept an optional `agentId` parameter to query another agent's namespace:
|
||||
|
||||
```
|
||||
memory_search({ query: "user's tech stack", agentId: "researcher" })
|
||||
```
|
||||
|
||||
Resolution priority: explicit `agentId` > explicit `userId` > session-derived > configured default.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
@@ -87,11 +107,11 @@ The agent gets five tools it can call during conversations:
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `memory_search` | Search memories by natural language |
|
||||
| `memory_list` | List all stored memories for a user |
|
||||
| `memory_store` | Explicitly save a fact |
|
||||
| `memory_search` | Search memories by natural language. Optional `agentId` to scope to a specific agent. |
|
||||
| `memory_list` | List all stored memories for a user. Optional `agentId` to scope to a specific agent. |
|
||||
| `memory_store` | Explicitly save a fact. Optional `agentId` to store under a specific agent's namespace. |
|
||||
| `memory_get` | Retrieve a memory by ID |
|
||||
| `memory_forget` | Delete by ID or by query |
|
||||
| `memory_forget` | Delete by ID or by query. Optional `agentId` to scope deletion to a specific agent. |
|
||||
|
||||
## CLI
|
||||
|
||||
@@ -107,6 +127,12 @@ openclaw mem0 search "what languages does the user know" --scope session
|
||||
|
||||
# Stats
|
||||
openclaw mem0 stats
|
||||
|
||||
# Search a specific agent's memories
|
||||
openclaw mem0 search "user preferences" --agent researcher
|
||||
|
||||
# Stats for a specific agent
|
||||
openclaw mem0 stats --agent researcher
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* Regression tests for per-agent memory isolation helpers.
|
||||
*
|
||||
* Addresses review feedback: targeted coverage for auth/session state,
|
||||
* malformed input, and the resolveUserId priority chain.
|
||||
*/
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
extractAgentId,
|
||||
effectiveUserId,
|
||||
agentUserId,
|
||||
resolveUserId,
|
||||
} from "./index.ts";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// extractAgentId
|
||||
// ---------------------------------------------------------------------------
|
||||
describe("extractAgentId", () => {
|
||||
it("returns agentId from a well-formed session key", () => {
|
||||
expect(extractAgentId("agent:researcher:550e8400-e29b")).toBe("researcher");
|
||||
});
|
||||
|
||||
it("returns undefined for the 'main' sentinel", () => {
|
||||
expect(extractAgentId("agent:main:abc-123")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined for undefined/null/empty input", () => {
|
||||
expect(extractAgentId(undefined)).toBeUndefined();
|
||||
expect(extractAgentId("")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined for non-agent session keys", () => {
|
||||
expect(extractAgentId("user:alice:xyz")).toBeUndefined();
|
||||
expect(extractAgentId("some-random-uuid")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("handles keys with extra colons after the UUID portion", () => {
|
||||
expect(extractAgentId("agent:beta:uuid:extra:stuff")).toBe("beta");
|
||||
});
|
||||
|
||||
it("returns undefined when agentId segment is empty", () => {
|
||||
// pattern: agent::<uuid> — empty agentId
|
||||
expect(extractAgentId("agent::some-uuid")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined when key is only 'agent:' with no trailing colon", () => {
|
||||
expect(extractAgentId("agent:")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("is case-sensitive (Agent != agent)", () => {
|
||||
expect(extractAgentId("Agent:researcher:uuid")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("handles whitespace-only agentId as truthy string", () => {
|
||||
// " " is a non-empty match — returned as-is (validation is caller's job)
|
||||
expect(extractAgentId("agent: :uuid")).toBe(" ");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// effectiveUserId
|
||||
// ---------------------------------------------------------------------------
|
||||
describe("effectiveUserId", () => {
|
||||
const base = "alice";
|
||||
|
||||
it("returns base userId when sessionKey is undefined", () => {
|
||||
expect(effectiveUserId(base)).toBe("alice");
|
||||
expect(effectiveUserId(base, undefined)).toBe("alice");
|
||||
});
|
||||
|
||||
it("returns namespaced userId for agent session keys", () => {
|
||||
expect(effectiveUserId(base, "agent:researcher:uuid-1")).toBe(
|
||||
"alice:agent:researcher",
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to base for 'main' agent sessions", () => {
|
||||
expect(effectiveUserId(base, "agent:main:uuid-2")).toBe("alice");
|
||||
});
|
||||
|
||||
it("falls back to base for non-agent session keys", () => {
|
||||
expect(effectiveUserId(base, "plain-session-id")).toBe("alice");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// agentUserId
|
||||
// ---------------------------------------------------------------------------
|
||||
describe("agentUserId", () => {
|
||||
it("produces the correct namespaced format", () => {
|
||||
expect(agentUserId("alice", "researcher")).toBe("alice:agent:researcher");
|
||||
});
|
||||
|
||||
it("handles empty agentId (caller is responsible for validation)", () => {
|
||||
expect(agentUserId("alice", "")).toBe("alice:agent:");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// resolveUserId — priority chain
|
||||
// ---------------------------------------------------------------------------
|
||||
describe("resolveUserId", () => {
|
||||
const base = "alice";
|
||||
|
||||
it("prefers explicit agentId over everything else", () => {
|
||||
expect(
|
||||
resolveUserId(
|
||||
base,
|
||||
{ agentId: "researcher", userId: "bob" },
|
||||
"agent:beta:uuid",
|
||||
),
|
||||
).toBe("alice:agent:researcher");
|
||||
});
|
||||
|
||||
it("uses explicit userId when agentId is absent", () => {
|
||||
expect(
|
||||
resolveUserId(base, { userId: "bob" }, "agent:beta:uuid"),
|
||||
).toBe("bob");
|
||||
});
|
||||
|
||||
it("derives from session key when both agentId and userId are absent", () => {
|
||||
expect(
|
||||
resolveUserId(base, {}, "agent:gamma:uuid"),
|
||||
).toBe("alice:agent:gamma");
|
||||
});
|
||||
|
||||
it("falls back to base userId when nothing else is provided", () => {
|
||||
expect(resolveUserId(base, {})).toBe("alice");
|
||||
expect(resolveUserId(base, {}, undefined)).toBe("alice");
|
||||
});
|
||||
|
||||
it("ignores empty-string agentId (falsy)", () => {
|
||||
expect(resolveUserId(base, { agentId: "" })).toBe("alice");
|
||||
});
|
||||
|
||||
it("ignores empty-string userId (falsy)", () => {
|
||||
expect(resolveUserId(base, { userId: "" })).toBe("alice");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Cross-agent isolation sanity checks
|
||||
// ---------------------------------------------------------------------------
|
||||
describe("multi-agent isolation", () => {
|
||||
const base = "user-42";
|
||||
|
||||
it("different agents get different namespaces", () => {
|
||||
const alphaId = effectiveUserId(base, "agent:alpha:uuid-a");
|
||||
const betaId = effectiveUserId(base, "agent:beta:uuid-b");
|
||||
expect(alphaId).not.toBe(betaId);
|
||||
expect(alphaId).toBe("user-42:agent:alpha");
|
||||
expect(betaId).toBe("user-42:agent:beta");
|
||||
});
|
||||
|
||||
it("same agent across sessions yields the same namespace", () => {
|
||||
const s1 = effectiveUserId(base, "agent:alpha:session-1");
|
||||
const s2 = effectiveUserId(base, "agent:alpha:session-2");
|
||||
expect(s1).toBe(s2);
|
||||
});
|
||||
|
||||
it("main session shares the base namespace (no isolation)", () => {
|
||||
const mainId = effectiveUserId(base, "agent:main:uuid-m");
|
||||
expect(mainId).toBe(base);
|
||||
});
|
||||
});
|
||||
+116
-26
@@ -10,6 +10,8 @@
|
||||
* - Short-term (session-scoped) and long-term (user-scoped) memory
|
||||
* - Auto-recall: injects relevant memories (both scopes) before each agent turn
|
||||
* - Auto-capture: stores key facts scoped to the current session after each agent turn
|
||||
* - Per-agent isolation: multi-agent setups write/read from separate userId namespaces
|
||||
* automatically via sessionKey routing (zero breaking changes for single-agent setups)
|
||||
* - CLI: openclaw mem0 search, openclaw mem0 stats
|
||||
* - Dual mode: platform or open-source (self-hosted)
|
||||
*/
|
||||
@@ -605,6 +607,50 @@ function categoriesToArray(
|
||||
return Object.entries(cats).map(([key, value]) => ({ [key]: value }));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Per-agent isolation helpers (exported for testability)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Parse an agent ID from a session key following the pattern `agent:<agentId>:<uuid>`.
|
||||
* Returns undefined for non-agent sessions, the "main" sentinel, or malformed keys.
|
||||
*/
|
||||
export function extractAgentId(sessionKey: string | undefined): string | undefined {
|
||||
if (!sessionKey) return undefined;
|
||||
const match = sessionKey.match(/^agent:([^:]+):/);
|
||||
const agentId = match?.[1];
|
||||
// "main" is the primary session — fall back to configured userId
|
||||
if (!agentId || agentId === "main") return undefined;
|
||||
return agentId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive the effective user_id from a session key, namespacing per-agent.
|
||||
* Falls back to baseUserId when the session is not agent-scoped.
|
||||
*/
|
||||
export function effectiveUserId(baseUserId: string, sessionKey?: string): string {
|
||||
const agentId = extractAgentId(sessionKey);
|
||||
return agentId ? `${baseUserId}:agent:${agentId}` : baseUserId;
|
||||
}
|
||||
|
||||
/** Build a user_id for an explicit agentId (e.g. from tool params). */
|
||||
export function agentUserId(baseUserId: string, agentId: string): string {
|
||||
return `${baseUserId}:agent:${agentId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve user_id with priority: explicit agentId > explicit userId > session-derived > configured.
|
||||
*/
|
||||
export function resolveUserId(
|
||||
baseUserId: string,
|
||||
opts: { agentId?: string; userId?: string },
|
||||
currentSessionId?: string,
|
||||
): string {
|
||||
if (opts.agentId) return agentUserId(baseUserId, opts.agentId);
|
||||
if (opts.userId) return opts.userId;
|
||||
return effectiveUserId(baseUserId, currentSessionId);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Plugin Definition
|
||||
// ============================================================================
|
||||
@@ -624,14 +670,23 @@ const memoryPlugin = {
|
||||
// Track current session ID for tool-level session scoping
|
||||
let currentSessionId: string | undefined;
|
||||
|
||||
// ========================================================================
|
||||
// Per-agent isolation helpers (thin wrappers around exported functions)
|
||||
// ========================================================================
|
||||
const _effectiveUserId = (sessionKey?: string) =>
|
||||
effectiveUserId(cfg.userId, sessionKey);
|
||||
const _agentUserId = (id: string) => agentUserId(cfg.userId, id);
|
||||
const _resolveUserId = (opts: { agentId?: string; userId?: string }) =>
|
||||
resolveUserId(cfg.userId, opts, currentSessionId);
|
||||
|
||||
api.logger.info(
|
||||
`openclaw-mem0: registered (mode: ${cfg.mode}, user: ${cfg.userId}, graph: ${cfg.enableGraph}, autoRecall: ${cfg.autoRecall}, autoCapture: ${cfg.autoCapture})`,
|
||||
);
|
||||
|
||||
// Helper: build add options
|
||||
function buildAddOptions(userIdOverride?: string, runId?: string): AddOptions {
|
||||
function buildAddOptions(userIdOverride?: string, runId?: string, sessionKey?: string): AddOptions {
|
||||
const opts: AddOptions = {
|
||||
user_id: userIdOverride || cfg.userId,
|
||||
user_id: userIdOverride || _effectiveUserId(sessionKey),
|
||||
source: "OPENCLAW",
|
||||
};
|
||||
if (runId) opts.run_id = runId;
|
||||
@@ -649,9 +704,10 @@ const memoryPlugin = {
|
||||
userIdOverride?: string,
|
||||
limit?: number,
|
||||
runId?: string,
|
||||
sessionKey?: string,
|
||||
): SearchOptions {
|
||||
const opts: SearchOptions = {
|
||||
user_id: userIdOverride || cfg.userId,
|
||||
user_id: userIdOverride || _effectiveUserId(sessionKey),
|
||||
top_k: limit ?? cfg.topK,
|
||||
limit: limit ?? cfg.topK,
|
||||
threshold: cfg.searchThreshold,
|
||||
@@ -686,6 +742,12 @@ const memoryPlugin = {
|
||||
"User ID to scope search (default: configured userId)",
|
||||
}),
|
||||
),
|
||||
agentId: Type.Optional(
|
||||
Type.String({
|
||||
description:
|
||||
"Agent ID to search memories for a specific agent (e.g. \"researcher\"). Overrides userId.",
|
||||
}),
|
||||
),
|
||||
scope: Type.Optional(
|
||||
Type.Union([
|
||||
Type.Literal("session"),
|
||||
@@ -698,39 +760,41 @@ const memoryPlugin = {
|
||||
),
|
||||
}),
|
||||
async execute(_toolCallId, params) {
|
||||
const { query, limit, userId, scope = "all" } = params as {
|
||||
const { query, limit, userId, agentId, scope = "all" } = params as {
|
||||
query: string;
|
||||
limit?: number;
|
||||
userId?: string;
|
||||
agentId?: string;
|
||||
scope?: "session" | "long-term" | "all";
|
||||
};
|
||||
|
||||
try {
|
||||
let results: MemoryItem[] = [];
|
||||
const uid = _resolveUserId({ agentId, userId });
|
||||
|
||||
if (scope === "session") {
|
||||
if (currentSessionId) {
|
||||
results = await provider.search(
|
||||
query,
|
||||
buildSearchOptions(userId, limit, currentSessionId),
|
||||
buildSearchOptions(uid, limit, currentSessionId),
|
||||
);
|
||||
}
|
||||
} else if (scope === "long-term") {
|
||||
results = await provider.search(
|
||||
query,
|
||||
buildSearchOptions(userId, limit),
|
||||
buildSearchOptions(uid, limit),
|
||||
);
|
||||
} else {
|
||||
// "all" — search both scopes and combine
|
||||
const longTermResults = await provider.search(
|
||||
query,
|
||||
buildSearchOptions(userId, limit),
|
||||
buildSearchOptions(uid, limit),
|
||||
);
|
||||
let sessionResults: MemoryItem[] = [];
|
||||
if (currentSessionId) {
|
||||
sessionResults = await provider.search(
|
||||
query,
|
||||
buildSearchOptions(userId, limit, currentSessionId),
|
||||
buildSearchOptions(uid, limit, currentSessionId),
|
||||
);
|
||||
}
|
||||
// Deduplicate by ID, preferring long-term
|
||||
@@ -803,6 +867,12 @@ const memoryPlugin = {
|
||||
description: "User ID to scope this memory",
|
||||
}),
|
||||
),
|
||||
agentId: Type.Optional(
|
||||
Type.String({
|
||||
description:
|
||||
"Agent ID to store memory under a specific agent's namespace (e.g. \"researcher\"). Overrides userId.",
|
||||
}),
|
||||
),
|
||||
metadata: Type.Optional(
|
||||
Type.Record(Type.String(), Type.Unknown(), {
|
||||
description: "Optional metadata to attach to this memory",
|
||||
@@ -816,18 +886,20 @@ const memoryPlugin = {
|
||||
),
|
||||
}),
|
||||
async execute(_toolCallId, params) {
|
||||
const { text, userId, longTerm = true } = params as {
|
||||
const { text, userId, agentId, longTerm = true } = params as {
|
||||
text: string;
|
||||
userId?: string;
|
||||
agentId?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
longTerm?: boolean;
|
||||
};
|
||||
|
||||
try {
|
||||
const uid = _resolveUserId({ agentId, userId });
|
||||
const runId = !longTerm && currentSessionId ? currentSessionId : undefined;
|
||||
const result = await provider.add(
|
||||
[{ role: "user", content: text }],
|
||||
buildAddOptions(userId, runId),
|
||||
buildAddOptions(uid, runId, currentSessionId),
|
||||
);
|
||||
|
||||
const added =
|
||||
@@ -919,7 +991,7 @@ const memoryPlugin = {
|
||||
name: "memory_list",
|
||||
label: "Memory List",
|
||||
description:
|
||||
"List all stored memories for a user. Use this when you want to see everything that's been remembered, rather than searching for something specific.",
|
||||
"List all stored memories for a user or agent. Use this when you want to see everything that's been remembered, rather than searching for something specific.",
|
||||
parameters: Type.Object({
|
||||
userId: Type.Optional(
|
||||
Type.String({
|
||||
@@ -927,6 +999,12 @@ const memoryPlugin = {
|
||||
"User ID to list memories for (default: configured userId)",
|
||||
}),
|
||||
),
|
||||
agentId: Type.Optional(
|
||||
Type.String({
|
||||
description:
|
||||
"Agent ID to list memories for a specific agent (e.g. \"researcher\"). Overrides userId.",
|
||||
}),
|
||||
),
|
||||
scope: Type.Optional(
|
||||
Type.Union([
|
||||
Type.Literal("session"),
|
||||
@@ -939,11 +1017,11 @@ const memoryPlugin = {
|
||||
),
|
||||
}),
|
||||
async execute(_toolCallId, params) {
|
||||
const { userId, scope = "all" } = params as { userId?: string; scope?: "session" | "long-term" | "all" };
|
||||
const { userId, agentId, scope = "all" } = params as { userId?: string; agentId?: string; scope?: "session" | "long-term" | "all" };
|
||||
|
||||
try {
|
||||
let memories: MemoryItem[] = [];
|
||||
const uid = userId || cfg.userId;
|
||||
const uid = _resolveUserId({ agentId, userId });
|
||||
|
||||
if (scope === "session") {
|
||||
if (currentSessionId) {
|
||||
@@ -1026,7 +1104,7 @@ const memoryPlugin = {
|
||||
name: "memory_forget",
|
||||
label: "Memory Forget",
|
||||
description:
|
||||
"Delete memories from Mem0. Provide a specific memoryId to delete directly, or a query to search and delete matching memories. GDPR-compliant.",
|
||||
"Delete memories from Mem0. Provide a specific memoryId to delete directly, or a query to search and delete matching memories. Supports agent-scoped deletion. GDPR-compliant.",
|
||||
parameters: Type.Object({
|
||||
query: Type.Optional(
|
||||
Type.String({
|
||||
@@ -1036,11 +1114,18 @@ const memoryPlugin = {
|
||||
memoryId: Type.Optional(
|
||||
Type.String({ description: "Specific memory ID to delete" }),
|
||||
),
|
||||
agentId: Type.Optional(
|
||||
Type.String({
|
||||
description:
|
||||
"Agent ID to scope deletion to a specific agent's memories (e.g. \"researcher\").",
|
||||
}),
|
||||
),
|
||||
}),
|
||||
async execute(_toolCallId, params) {
|
||||
const { query, memoryId } = params as {
|
||||
const { query, memoryId, agentId } = params as {
|
||||
query?: string;
|
||||
memoryId?: string;
|
||||
agentId?: string;
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -1055,9 +1140,10 @@ const memoryPlugin = {
|
||||
}
|
||||
|
||||
if (query) {
|
||||
const uid = _resolveUserId({ agentId });
|
||||
const results = await provider.search(
|
||||
query,
|
||||
buildSearchOptions(undefined, 5),
|
||||
buildSearchOptions(uid, 5),
|
||||
);
|
||||
|
||||
if (!results || results.length === 0) {
|
||||
@@ -1148,10 +1234,12 @@ const memoryPlugin = {
|
||||
.argument("<query>", "Search query")
|
||||
.option("--limit <n>", "Max results", String(cfg.topK))
|
||||
.option("--scope <scope>", 'Memory scope: "session", "long-term", or "all"', "all")
|
||||
.action(async (query: string, opts: { limit: string; scope: string }) => {
|
||||
.option("--agent <agentId>", "Search a specific agent's memory namespace")
|
||||
.action(async (query: string, opts: { limit: string; scope: string; agent?: string }) => {
|
||||
try {
|
||||
const limit = parseInt(opts.limit, 10);
|
||||
const scope = opts.scope as "session" | "long-term" | "all";
|
||||
const uid = opts.agent ? _agentUserId(opts.agent) : _effectiveUserId(currentSessionId);
|
||||
|
||||
let allResults: MemoryItem[] = [];
|
||||
|
||||
@@ -1159,7 +1247,7 @@ const memoryPlugin = {
|
||||
if (currentSessionId) {
|
||||
const sessionResults = await provider.search(
|
||||
query,
|
||||
buildSearchOptions(undefined, limit, currentSessionId),
|
||||
buildSearchOptions(uid, limit, currentSessionId),
|
||||
);
|
||||
if (sessionResults?.length) {
|
||||
allResults.push(...sessionResults.map((r) => ({ ...r, _scope: "session" as const })));
|
||||
@@ -1173,7 +1261,7 @@ const memoryPlugin = {
|
||||
if (scope === "long-term" || scope === "all") {
|
||||
const longTermResults = await provider.search(
|
||||
query,
|
||||
buildSearchOptions(undefined, limit),
|
||||
buildSearchOptions(uid, limit),
|
||||
);
|
||||
if (longTermResults?.length) {
|
||||
allResults.push(...longTermResults.map((r) => ({ ...r, _scope: "long-term" as const })));
|
||||
@@ -1212,14 +1300,16 @@ const memoryPlugin = {
|
||||
mem0
|
||||
.command("stats")
|
||||
.description("Show memory statistics from Mem0")
|
||||
.action(async () => {
|
||||
.option("--agent <agentId>", "Show stats for a specific agent")
|
||||
.action(async (opts: { agent?: string }) => {
|
||||
try {
|
||||
const uid = opts.agent ? _agentUserId(opts.agent) : cfg.userId;
|
||||
const memories = await provider.getAll({
|
||||
user_id: cfg.userId,
|
||||
user_id: uid,
|
||||
source: "OPENCLAW",
|
||||
});
|
||||
console.log(`Mode: ${cfg.mode}`);
|
||||
console.log(`User: ${cfg.userId}`);
|
||||
console.log(`User: ${uid}${opts.agent ? ` (agent: ${opts.agent})` : ""}`);
|
||||
console.log(
|
||||
`Total memories: ${Array.isArray(memories) ? memories.length : "unknown"}`,
|
||||
);
|
||||
@@ -1249,10 +1339,10 @@ const memoryPlugin = {
|
||||
if (sessionId) currentSessionId = sessionId;
|
||||
|
||||
try {
|
||||
// Search long-term memories (user-scoped)
|
||||
// Search long-term memories (user-scoped, isolated per agent)
|
||||
const longTermResults = await provider.search(
|
||||
event.prompt,
|
||||
buildSearchOptions(),
|
||||
buildSearchOptions(undefined, undefined, undefined, sessionId),
|
||||
);
|
||||
|
||||
// Search session memories (session-scoped) if we have a session ID
|
||||
@@ -1260,7 +1350,7 @@ const memoryPlugin = {
|
||||
if (currentSessionId) {
|
||||
sessionResults = await provider.search(
|
||||
event.prompt,
|
||||
buildSearchOptions(undefined, undefined, currentSessionId),
|
||||
buildSearchOptions(undefined, undefined, currentSessionId, sessionId),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1365,7 +1455,7 @@ const memoryPlugin = {
|
||||
|
||||
if (formattedMessages.length === 0) return;
|
||||
|
||||
const addOpts = buildAddOptions(undefined, currentSessionId);
|
||||
const addOpts = buildAddOptions(undefined, currentSessionId, sessionId);
|
||||
const result = await provider.add(
|
||||
formattedMessages,
|
||||
addOpts,
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
"mem0",
|
||||
"long-term-memory"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sinclair/typebox": "0.34.47",
|
||||
"mem0ai": "^2.2.1"
|
||||
@@ -19,5 +22,8 @@
|
||||
"extensions": [
|
||||
"./index.ts"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitest": "^4.0.18"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user