fix: adding login and fixing the plugin to follow the openclaw plugin standards (#4686)
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Tool aggregator — single entry-point for registering all Mem0 tools.
|
||||
*
|
||||
* Re-exports the canonical `ToolContext` interface and provides
|
||||
* `registerAllTools(ctx)` which wires up every tool in one call.
|
||||
*/
|
||||
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||
import type { Backend } from "../backend/base.ts";
|
||||
import type {
|
||||
Mem0Config,
|
||||
Mem0Provider,
|
||||
AddOptions,
|
||||
SearchOptions,
|
||||
} from "../types.ts";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Canonical ToolContext definition
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface ToolContext {
|
||||
api: OpenClawPluginApi;
|
||||
backend: Backend;
|
||||
cfg: Mem0Config;
|
||||
provider?: Mem0Provider;
|
||||
resolveUserId: (opts: { agentId?: string; userId?: string }) => string;
|
||||
effectiveUserId: (sessionKey?: string) => string;
|
||||
agentUserId: (id: string) => string;
|
||||
getCurrentSessionId: () => string | undefined;
|
||||
skillsActive: boolean;
|
||||
buildAddOptions: (
|
||||
userIdOverride?: string,
|
||||
runId?: string,
|
||||
sessionKey?: string,
|
||||
) => AddOptions;
|
||||
buildSearchOptions: (
|
||||
userIdOverride?: string,
|
||||
limit?: number,
|
||||
runId?: string,
|
||||
sessionKey?: string,
|
||||
) => SearchOptions;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tool factory imports
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
import { createMemorySearchTool } from "./memory-search.ts";
|
||||
import { createMemoryStoreTool } from "./memory-store.ts";
|
||||
import { createMemoryGetTool } from "./memory-get.ts";
|
||||
import { createMemoryListTool } from "./memory-list.ts";
|
||||
import { createMemoryUpdateTool } from "./memory-update.ts";
|
||||
import { createMemoryDeleteTool } from "./memory-delete.ts";
|
||||
import { createMemoryHistoryTool } from "./memory-history.ts";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Aggregator
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Registers all Mem0 tools with the plugin API.
|
||||
*
|
||||
* 7 core tools for memory management:
|
||||
* search, store, get, list, update, delete, history
|
||||
*/
|
||||
export function registerAllTools(ctx: ToolContext): void {
|
||||
const { api } = ctx;
|
||||
|
||||
api.registerTool(createMemorySearchTool(ctx), { name: "memory_search" });
|
||||
api.registerTool(createMemoryStoreTool(ctx), { name: "memory_store" });
|
||||
api.registerTool(createMemoryGetTool(ctx), { name: "memory_get" });
|
||||
api.registerTool(createMemoryListTool(ctx), { name: "memory_list" });
|
||||
api.registerTool(createMemoryUpdateTool(ctx), { name: "memory_update" });
|
||||
api.registerTool(createMemoryDeleteTool(ctx), { name: "memory_delete" });
|
||||
api.registerTool(createMemoryHistoryTool(ctx), { name: "memory_history" });
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
/**
|
||||
* memory_delete tool — unified delete tool replacing memory_forget and memory_delete_all.
|
||||
*
|
||||
* Supports four modes:
|
||||
* 1. By memory_id — direct deletion of a specific memory
|
||||
* 2. By query — search-and-delete (auto-deletes high-confidence match, otherwise lists candidates)
|
||||
* 3. all:true — bulk-delete all memories for a user (requires confirm:true)
|
||||
* 4. entity:true — cascade-delete an entity and all its memories (requires confirm:true, platform only)
|
||||
*/
|
||||
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { isSubagentSession } from "../isolation.ts";
|
||||
|
||||
import type { ToolContext } from "./index.ts";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tool factory
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Creates the `memory_delete` tool config object suitable for
|
||||
* `api.registerTool(config, { name })`.
|
||||
*
|
||||
* This replaces both `memory_forget` and `memory_delete_all` from the
|
||||
* original index.ts implementation.
|
||||
*/
|
||||
export function createMemoryDeleteTool(ctx: ToolContext) {
|
||||
const {
|
||||
api,
|
||||
provider,
|
||||
resolveUserId,
|
||||
getCurrentSessionId,
|
||||
buildSearchOptions,
|
||||
backend,
|
||||
} = ctx;
|
||||
|
||||
return {
|
||||
name: "memory_delete",
|
||||
label: "Memory Delete",
|
||||
description:
|
||||
"Delete memories from Mem0. Provide a specific memoryId for direct deletion, a query to search and delete, all:true for bulk deletion, or entity:true to cascade-delete an entity. Bulk operations require confirm:true. GDPR-compliant.",
|
||||
parameters: Type.Object({
|
||||
memory_id: Type.Optional(
|
||||
Type.String({ description: "Specific memory ID to delete" }),
|
||||
),
|
||||
query: Type.Optional(
|
||||
Type.String({
|
||||
description:
|
||||
"Search query to find memory to delete (searches and deletes best match)",
|
||||
}),
|
||||
),
|
||||
all: Type.Optional(
|
||||
Type.Boolean({
|
||||
description:
|
||||
"Delete ALL memories matching scope filters. Requires confirm: true",
|
||||
}),
|
||||
),
|
||||
entity: Type.Optional(
|
||||
Type.Boolean({
|
||||
description:
|
||||
"Delete entity and all its memories (cascade). Requires confirm: true",
|
||||
}),
|
||||
),
|
||||
confirm: Type.Optional(
|
||||
Type.Boolean({
|
||||
description:
|
||||
"Must be true for bulk operations (all/entity). Safety gate.",
|
||||
}),
|
||||
),
|
||||
user_id: Type.Optional(Type.String({ description: "Scope to user" })),
|
||||
agent_id: Type.Optional(Type.String({ description: "Scope to agent" })),
|
||||
}),
|
||||
|
||||
async execute(_toolCallId: string, params: Record<string, unknown>) {
|
||||
const { memory_id, query, all, entity, confirm, user_id, agent_id } =
|
||||
params as {
|
||||
memory_id?: string;
|
||||
query?: string;
|
||||
all?: boolean;
|
||||
entity?: boolean;
|
||||
confirm?: boolean;
|
||||
user_id?: string;
|
||||
agent_id?: string;
|
||||
};
|
||||
|
||||
try {
|
||||
// Block subagent deletes at the tool level.
|
||||
const currentSessionId = getCurrentSessionId();
|
||||
if (isSubagentSession(currentSessionId)) {
|
||||
api.logger.warn(
|
||||
"openclaw-mem0: blocked memory_delete from subagent session",
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Memory deletion is not available in subagent sessions. The main agent handles memory.",
|
||||
},
|
||||
],
|
||||
details: { error: "subagent_blocked" },
|
||||
};
|
||||
}
|
||||
|
||||
// Mode 1: Delete by specific memory ID
|
||||
if (memory_id) {
|
||||
await provider!.delete(memory_id);
|
||||
return {
|
||||
content: [{ type: "text", text: `Memory ${memory_id} deleted.` }],
|
||||
details: { action: "deleted", id: memory_id },
|
||||
};
|
||||
}
|
||||
|
||||
// Mode 2: Search-and-delete by query
|
||||
if (query) {
|
||||
const uid = resolveUserId({ agentId: agent_id, userId: user_id });
|
||||
const results = await provider!.search(
|
||||
query,
|
||||
buildSearchOptions(uid, 5),
|
||||
);
|
||||
|
||||
if (!results || results.length === 0) {
|
||||
return {
|
||||
content: [{ type: "text", text: "No matching memories found." }],
|
||||
details: { found: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
// If single high-confidence match, delete directly
|
||||
if (results.length === 1 || (results[0].score ?? 0) > 0.9) {
|
||||
await provider!.delete(results[0].id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Deleted: "${results[0].memory}"`,
|
||||
},
|
||||
],
|
||||
details: { action: "deleted", id: results[0].id },
|
||||
};
|
||||
}
|
||||
|
||||
// Multiple ambiguous results — return candidates for user to pick
|
||||
const list = results
|
||||
.map(
|
||||
(r) =>
|
||||
`- [${r.id}] ${r.memory.slice(0, 80)}${r.memory.length > 80 ? "..." : ""} (score: ${((r.score ?? 0) * 100).toFixed(0)}%)`,
|
||||
)
|
||||
.join("\n");
|
||||
|
||||
const candidates = results.map((r) => ({
|
||||
id: r.id,
|
||||
memory: r.memory,
|
||||
score: r.score,
|
||||
}));
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Found ${results.length} candidates. Specify memory_id to delete:\n${list}`,
|
||||
},
|
||||
],
|
||||
details: { action: "candidates", candidates },
|
||||
};
|
||||
}
|
||||
|
||||
// Mode 3: Bulk-delete all memories for a user
|
||||
if (all) {
|
||||
if (!confirm) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Bulk deletion requires confirm: true. Ask the user to confirm before proceeding.",
|
||||
},
|
||||
],
|
||||
details: { error: "confirmation_required" },
|
||||
};
|
||||
}
|
||||
|
||||
const uid = resolveUserId({ agentId: agent_id, userId: user_id });
|
||||
await provider!.deleteAll(uid);
|
||||
api.logger.info(
|
||||
`openclaw-mem0: deleted all memories for user ${uid}`,
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `All memories deleted for user "${uid}".`,
|
||||
},
|
||||
],
|
||||
details: { action: "deleted_all", user_id: uid },
|
||||
};
|
||||
}
|
||||
|
||||
// Mode 4: Cascade-delete entity (platform only)
|
||||
if (entity) {
|
||||
if (!confirm) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Entity deletion requires confirm: true. Ask the user to confirm before proceeding.",
|
||||
},
|
||||
],
|
||||
details: { error: "confirmation_required" },
|
||||
};
|
||||
}
|
||||
|
||||
const entityOpts: { userId?: string; agentId?: string } = {};
|
||||
if (user_id) entityOpts.userId = user_id;
|
||||
if (agent_id) entityOpts.agentId = agent_id;
|
||||
|
||||
await backend.deleteEntities(entityOpts);
|
||||
api.logger.info(
|
||||
`openclaw-mem0: deleted entity (userId=${user_id ?? "default"}, agentId=${agent_id ?? "none"})`,
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Entity and all associated memories deleted (userId=${user_id ?? "default"}, agentId=${agent_id ?? "none"}).`,
|
||||
},
|
||||
],
|
||||
details: {
|
||||
action: "entity_deleted",
|
||||
user_id: user_id ?? "default",
|
||||
agent_id: agent_id,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// No valid mode specified
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Provide memory_id, query, all, or entity to specify what to delete.",
|
||||
},
|
||||
],
|
||||
details: { error: "missing_param" },
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Memory delete failed: ${String(err)}`,
|
||||
},
|
||||
],
|
||||
details: { error: String(err) },
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* memory_get tool — extracted from index.ts registerTools().
|
||||
*
|
||||
* Retrieves a specific memory by its ID from Mem0.
|
||||
*/
|
||||
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import type { ToolContext } from "./index.ts";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tool factory
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Creates the `memory_get` tool config object suitable for
|
||||
* `api.registerTool(config, { name })`.
|
||||
*/
|
||||
export function createMemoryGetTool(ctx: ToolContext) {
|
||||
const { provider } = ctx;
|
||||
|
||||
return {
|
||||
name: "memory_get",
|
||||
label: "Memory Get",
|
||||
description: "Retrieve a specific memory by its ID from Mem0.",
|
||||
parameters: Type.Object({
|
||||
memoryId: Type.String({ description: "The memory ID to retrieve" }),
|
||||
}),
|
||||
|
||||
async execute(_toolCallId: string, params: Record<string, unknown>) {
|
||||
const { memoryId } = params as { memoryId: string };
|
||||
|
||||
try {
|
||||
const memory = await provider!.get(memoryId);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Memory ${memory.id}:\n${memory.memory}\n\nCreated: ${memory.created_at ?? "unknown"}\nUpdated: ${memory.updated_at ?? "unknown"}`,
|
||||
},
|
||||
],
|
||||
details: { memory },
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Memory get failed: ${String(err)}`,
|
||||
},
|
||||
],
|
||||
details: { error: String(err) },
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* memory_history tool — extracted from index.ts registerTools().
|
||||
*
|
||||
* Views the edit history of a specific memory, showing all changes
|
||||
* over time including previous values, new values, and timestamps.
|
||||
*/
|
||||
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import type { ToolContext } from "./index.ts";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tool factory
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Creates the `memory_history` tool config object suitable for
|
||||
* `api.registerTool(config, { name })`.
|
||||
*/
|
||||
export function createMemoryHistoryTool(ctx: ToolContext) {
|
||||
const { provider } = ctx;
|
||||
|
||||
return {
|
||||
name: "memory_history",
|
||||
label: "Memory History",
|
||||
description:
|
||||
"View the edit history of a specific memory. Shows all changes over time including previous values, new values, and timestamps. Useful for understanding how a memory evolved.",
|
||||
parameters: Type.Object({
|
||||
memoryId: Type.String({
|
||||
description: "The memory ID to view history for",
|
||||
}),
|
||||
}),
|
||||
|
||||
async execute(_toolCallId: string, params: Record<string, unknown>) {
|
||||
const { memoryId } = params as { memoryId: string };
|
||||
|
||||
try {
|
||||
const history = await provider!.history(memoryId);
|
||||
|
||||
if (!history || history.length === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `No history found for memory ${memoryId}.`,
|
||||
},
|
||||
],
|
||||
details: { count: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
const text = history
|
||||
.map(
|
||||
(h, i) =>
|
||||
`${i + 1}. [${h.event}] ${h.created_at}\n Old: ${h.old_memory || "(none)"}\n New: ${h.new_memory || "(none)"}`,
|
||||
)
|
||||
.join("\n\n");
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `History for memory ${memoryId} (${history.length} entries):\n\n${text}`,
|
||||
},
|
||||
],
|
||||
details: { count: history.length, history },
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
content: [
|
||||
{ type: "text", text: `Memory history failed: ${String(err)}` },
|
||||
],
|
||||
details: { error: String(err) },
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* memory_list tool — extracted from index.ts registerTools().
|
||||
*
|
||||
* Lists all stored memories for a user or agent. Supports scope filtering
|
||||
* (session/long-term/all), deduplication, and CLI-parity parameters for
|
||||
* pagination, category filtering, date ranges, and graph support.
|
||||
*/
|
||||
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import type { Mem0Config, Mem0Provider, MemoryItem } from "../types.ts";
|
||||
|
||||
import type { ListOptions } from "../types.ts";
|
||||
import type { ToolContext } from "./index.ts";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tool factory
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Creates the `memory_list` tool config object suitable for
|
||||
* `api.registerTool(config, { name })`.
|
||||
*/
|
||||
export function createMemoryListTool(ctx: ToolContext) {
|
||||
const { provider, resolveUserId, getCurrentSessionId } = ctx;
|
||||
|
||||
return {
|
||||
name: "memory_list",
|
||||
label: "Memory List",
|
||||
description:
|
||||
"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({
|
||||
description:
|
||||
"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"),
|
||||
Type.Literal("long-term"),
|
||||
Type.Literal("all"),
|
||||
],
|
||||
{
|
||||
description:
|
||||
'Memory scope: "session" (current session only), "long-term" (user-scoped only), or "all" (both). Default: "all"',
|
||||
},
|
||||
),
|
||||
),
|
||||
// --- NEW CLI-parity parameters ---
|
||||
page: Type.Optional(
|
||||
Type.Number({ description: "Page number (default: 1)" }),
|
||||
),
|
||||
pageSize: Type.Optional(
|
||||
Type.Number({ description: "Results per page (default: 100)" }),
|
||||
),
|
||||
category: Type.Optional(
|
||||
Type.String({ description: "Filter by category" }),
|
||||
),
|
||||
after: Type.Optional(
|
||||
Type.String({ description: "Created after date (YYYY-MM-DD)" }),
|
||||
),
|
||||
before: Type.Optional(
|
||||
Type.String({ description: "Created before date (YYYY-MM-DD)" }),
|
||||
),
|
||||
enableGraph: Type.Optional(
|
||||
Type.Boolean({ description: "Enable graph in listing" }),
|
||||
),
|
||||
}),
|
||||
|
||||
async execute(_toolCallId: string, params: Record<string, unknown>) {
|
||||
const {
|
||||
userId,
|
||||
agentId,
|
||||
scope = "all",
|
||||
// New CLI-parity params
|
||||
page,
|
||||
pageSize,
|
||||
category,
|
||||
after,
|
||||
before,
|
||||
enableGraph,
|
||||
} = params as {
|
||||
userId?: string;
|
||||
agentId?: string;
|
||||
scope?: "session" | "long-term" | "all";
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
category?: string;
|
||||
after?: string;
|
||||
before?: string;
|
||||
enableGraph?: boolean;
|
||||
};
|
||||
|
||||
try {
|
||||
let memories: MemoryItem[] = [];
|
||||
const uid = resolveUserId({ agentId, userId });
|
||||
const currentSessionId = getCurrentSessionId();
|
||||
|
||||
// Build base options for getAll, incorporating new CLI-parity params
|
||||
const buildGetAllOpts = (extra?: { run_id?: string }): ListOptions => {
|
||||
const opts: Record<string, unknown> = {
|
||||
user_id: uid,
|
||||
source: "OPENCLAW",
|
||||
};
|
||||
if (extra?.run_id) opts.run_id = extra.run_id;
|
||||
if (pageSize !== undefined) opts.page_size = pageSize;
|
||||
if (page !== undefined) opts.page = page;
|
||||
if (category !== undefined) opts.category = category;
|
||||
if (after !== undefined) opts.after = after;
|
||||
if (before !== undefined) opts.before = before;
|
||||
if (enableGraph !== undefined) opts.enable_graph = enableGraph;
|
||||
return opts as unknown as ListOptions;
|
||||
};
|
||||
|
||||
if (scope === "session") {
|
||||
if (currentSessionId) {
|
||||
memories = await provider!.getAll(
|
||||
buildGetAllOpts({ run_id: currentSessionId }),
|
||||
);
|
||||
}
|
||||
} else if (scope === "long-term") {
|
||||
memories = await provider!.getAll(buildGetAllOpts());
|
||||
} else {
|
||||
// "all" — combine both scopes
|
||||
const longTerm = await provider!.getAll(buildGetAllOpts());
|
||||
let session: MemoryItem[] = [];
|
||||
if (currentSessionId) {
|
||||
session = await provider!.getAll(
|
||||
buildGetAllOpts({ run_id: currentSessionId }),
|
||||
);
|
||||
}
|
||||
const seen = new Set(longTerm.map((r) => r.id));
|
||||
memories = [...longTerm, ...session.filter((r) => !seen.has(r.id))];
|
||||
}
|
||||
|
||||
if (!memories || memories.length === 0) {
|
||||
return {
|
||||
content: [{ type: "text", text: "No memories stored yet." }],
|
||||
details: { count: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
const text = memories
|
||||
.map((r, i) => `${i + 1}. ${r.memory} (id: ${r.id})`)
|
||||
.join("\n");
|
||||
|
||||
const sanitized = memories.map((r) => ({
|
||||
id: r.id,
|
||||
memory: r.memory,
|
||||
categories: r.categories,
|
||||
created_at: r.created_at,
|
||||
}));
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `${memories.length} memories:\n\n${text}`,
|
||||
},
|
||||
],
|
||||
details: { count: memories.length, memories: sanitized },
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Memory list failed: ${String(err)}`,
|
||||
},
|
||||
],
|
||||
details: { error: String(err) },
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
/**
|
||||
* memory_search tool — extracted from index.ts registerTools().
|
||||
*
|
||||
* Searches long-term and session-scoped memories stored in Mem0.
|
||||
* Supports scope filtering (session/long-term/all), categories,
|
||||
* advanced filters, and deduplication.
|
||||
*/
|
||||
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import type {
|
||||
Mem0Config,
|
||||
Mem0Provider,
|
||||
MemoryItem,
|
||||
SearchOptions,
|
||||
} from "../types.ts";
|
||||
|
||||
import type { ToolContext } from "./index.ts";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tool factory
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Creates the `memory_search` tool config object suitable for
|
||||
* `api.registerTool(config, { name })`.
|
||||
*/
|
||||
export function createMemorySearchTool(ctx: ToolContext) {
|
||||
const {
|
||||
cfg,
|
||||
provider,
|
||||
resolveUserId,
|
||||
getCurrentSessionId,
|
||||
buildSearchOptions,
|
||||
} = ctx;
|
||||
|
||||
return {
|
||||
name: "memory_search",
|
||||
label: "Memory Search",
|
||||
description:
|
||||
"Search through long-term memories stored in Mem0. Use when you need context about user preferences, past decisions, or previously discussed topics.",
|
||||
parameters: Type.Object({
|
||||
query: Type.String({ description: "Search query" }),
|
||||
limit: Type.Optional(
|
||||
Type.Number({
|
||||
description: `Max results (default: ${cfg.topK})`,
|
||||
}),
|
||||
),
|
||||
userId: Type.Optional(
|
||||
Type.String({
|
||||
description: "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"),
|
||||
Type.Literal("long-term"),
|
||||
Type.Literal("all"),
|
||||
],
|
||||
{
|
||||
description:
|
||||
'Memory scope: "session" (current session only), "long-term" (user-scoped only), or "all" (both). Default: "all"',
|
||||
},
|
||||
),
|
||||
),
|
||||
categories: Type.Optional(
|
||||
Type.Array(Type.String(), {
|
||||
description:
|
||||
'Filter results by category (e.g. ["identity", "preference"]). Only returns memories tagged with these categories.',
|
||||
}),
|
||||
),
|
||||
filters: Type.Optional(
|
||||
Type.Record(Type.String(), Type.Unknown(), {
|
||||
description:
|
||||
'Advanced filters object. Supports date ranges and metadata filtering. Examples: {"created_at": {"gte": "2026-03-01"}} for recent memories, {"AND": [{"categories": {"contains": "decision"}}, {"created_at": {"gte": "2026-01-01"}}]} for decisions this year. Operators: eq, ne, gt, gte, lt, lte, in, contains, icontains. Logical: AND, OR, NOT.',
|
||||
}),
|
||||
),
|
||||
// --- NEW CLI-parity parameters ---
|
||||
rerank: Type.Optional(
|
||||
Type.Boolean({ description: "Enable reranking (platform only)" }),
|
||||
),
|
||||
keyword: Type.Optional(
|
||||
Type.Boolean({ description: "Use keyword search instead of semantic" }),
|
||||
),
|
||||
threshold: Type.Optional(
|
||||
Type.Number({ description: "Minimum similarity score (0-1)" }),
|
||||
),
|
||||
topK: Type.Optional(
|
||||
Type.Number({ description: "Maximum number of results" }),
|
||||
),
|
||||
fields: Type.Optional(
|
||||
Type.Array(Type.String(), {
|
||||
description: "Specific fields to return",
|
||||
}),
|
||||
),
|
||||
enableGraph: Type.Optional(
|
||||
Type.Boolean({ description: "Enable graph memory in search" }),
|
||||
),
|
||||
}),
|
||||
|
||||
async execute(_toolCallId: string, params: Record<string, unknown>) {
|
||||
const {
|
||||
query,
|
||||
limit,
|
||||
userId,
|
||||
agentId,
|
||||
scope = "all",
|
||||
categories: filterCategories,
|
||||
filters: agentFilters,
|
||||
// New CLI-parity params
|
||||
rerank,
|
||||
keyword,
|
||||
threshold,
|
||||
topK,
|
||||
fields,
|
||||
enableGraph,
|
||||
} = params as {
|
||||
query: string;
|
||||
limit?: number;
|
||||
userId?: string;
|
||||
agentId?: string;
|
||||
scope?: "session" | "long-term" | "all";
|
||||
categories?: string[];
|
||||
filters?: Record<string, unknown>;
|
||||
rerank?: boolean;
|
||||
keyword?: boolean;
|
||||
threshold?: number;
|
||||
topK?: number;
|
||||
fields?: string[];
|
||||
enableGraph?: boolean;
|
||||
};
|
||||
|
||||
try {
|
||||
let results: MemoryItem[] = [];
|
||||
const uid = resolveUserId({ agentId, userId });
|
||||
const currentSessionId = getCurrentSessionId();
|
||||
|
||||
// Apply agent-provided filters and new CLI-parity params to search options
|
||||
const applyFilters = (opts: SearchOptions): SearchOptions => {
|
||||
if (filterCategories?.length) opts.categories = filterCategories;
|
||||
if (agentFilters) opts.filters = agentFilters;
|
||||
// Pass through new CLI-parity parameters
|
||||
if (rerank !== undefined) opts.reranking = rerank;
|
||||
if (keyword !== undefined) opts.keyword_search = keyword;
|
||||
if (threshold !== undefined) opts.threshold = threshold;
|
||||
if (topK !== undefined) {
|
||||
opts.top_k = topK;
|
||||
opts.limit = topK;
|
||||
}
|
||||
// Note: 'fields' and 'enableGraph' are not in the legacy SearchOptions
|
||||
// type from types.ts. They are passed through for forward compatibility
|
||||
// when the backend supports them.
|
||||
if (fields !== undefined)
|
||||
(opts as unknown as Record<string, unknown>).fields = fields;
|
||||
if (enableGraph !== undefined)
|
||||
(opts as unknown as Record<string, unknown>).enable_graph =
|
||||
enableGraph;
|
||||
return opts;
|
||||
};
|
||||
|
||||
if (scope === "session") {
|
||||
if (currentSessionId) {
|
||||
results = await provider!.search(
|
||||
query,
|
||||
applyFilters(buildSearchOptions(uid, limit, currentSessionId)),
|
||||
);
|
||||
}
|
||||
} else if (scope === "long-term") {
|
||||
results = await provider!.search(
|
||||
query,
|
||||
applyFilters(buildSearchOptions(uid, limit)),
|
||||
);
|
||||
} else {
|
||||
// "all" -- search both scopes and combine
|
||||
const longTermResults = await provider!.search(
|
||||
query,
|
||||
applyFilters(buildSearchOptions(uid, limit)),
|
||||
);
|
||||
let sessionResults: MemoryItem[] = [];
|
||||
if (currentSessionId) {
|
||||
sessionResults = await provider!.search(
|
||||
query,
|
||||
applyFilters(buildSearchOptions(uid, limit, currentSessionId)),
|
||||
);
|
||||
}
|
||||
// Deduplicate by ID, preferring long-term
|
||||
const seen = new Set(longTermResults.map((r) => r.id));
|
||||
results = [
|
||||
...longTermResults,
|
||||
...sessionResults.filter((r) => !seen.has(r.id)),
|
||||
];
|
||||
}
|
||||
|
||||
if (!results || results.length === 0) {
|
||||
return {
|
||||
content: [{ type: "text", text: "No relevant memories found." }],
|
||||
details: { count: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
const text = results
|
||||
.map(
|
||||
(r, i) =>
|
||||
`${i + 1}. ${r.memory} (score: ${((r.score ?? 0) * 100).toFixed(0)}%, id: ${r.id})`,
|
||||
)
|
||||
.join("\n");
|
||||
|
||||
const sanitized = results.map((r) => ({
|
||||
id: r.id,
|
||||
memory: r.memory,
|
||||
score: r.score,
|
||||
categories: r.categories,
|
||||
created_at: r.created_at,
|
||||
}));
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Found ${results.length} memories:\n\n${text}`,
|
||||
},
|
||||
],
|
||||
details: { count: results.length, memories: sanitized },
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Memory search failed: ${String(err)}`,
|
||||
},
|
||||
],
|
||||
details: { error: String(err) },
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
/**
|
||||
* memory_store tool — extracted from index.ts registerTools().
|
||||
*
|
||||
* Saves important information in long-term memory via Mem0.
|
||||
* Supports skills mode (direct storage with infer=false) and
|
||||
* legacy mode (mem0 extraction LLM handles deduplication).
|
||||
*/
|
||||
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import type {
|
||||
Mem0Config,
|
||||
Mem0Provider,
|
||||
AddOptions,
|
||||
SearchOptions,
|
||||
} from "../types.ts";
|
||||
import { isSubagentSession } from "../isolation.ts";
|
||||
import { resolveCategories, ttlToExpirationDate } from "../skill-loader.ts";
|
||||
|
||||
import type { ToolContext } from "./index.ts";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tool factory
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Creates the `memory_store` tool config object suitable for
|
||||
* `api.registerTool(config, { name })`.
|
||||
*/
|
||||
export function createMemoryStoreTool(ctx: ToolContext) {
|
||||
const {
|
||||
api,
|
||||
cfg,
|
||||
provider,
|
||||
resolveUserId,
|
||||
getCurrentSessionId,
|
||||
buildAddOptions,
|
||||
buildSearchOptions,
|
||||
skillsActive,
|
||||
} = ctx;
|
||||
|
||||
return {
|
||||
name: "memory_store",
|
||||
label: "Memory Store",
|
||||
description:
|
||||
"Save important information in long-term memory via Mem0. Use for preferences, facts, decisions, and anything worth remembering.",
|
||||
parameters: Type.Object({
|
||||
text: Type.Optional(
|
||||
Type.String({
|
||||
description:
|
||||
"Single fact to remember. Use 'facts' array instead when storing multiple facts from one conversation turn.",
|
||||
}),
|
||||
),
|
||||
facts: Type.Optional(
|
||||
Type.Array(Type.String(), {
|
||||
description:
|
||||
"Array of facts to store in one call. ALL facts MUST share the same category. If a turn has facts in different categories, make one call per category. Category determines retention policy (TTL, immutability).",
|
||||
}),
|
||||
),
|
||||
category: Type.Optional(
|
||||
Type.String({
|
||||
description:
|
||||
'Memory category. Determines retention policy (TTL, immutability). All facts in this call inherit this category. Options: "identity", "preference", "decision", "rule", "project", "configuration", "technical", "relationship"',
|
||||
}),
|
||||
),
|
||||
importance: Type.Optional(
|
||||
Type.Number({
|
||||
description:
|
||||
"Importance override (0.0-1.0). Omit to use category default. Applies to all facts in this call. Defaults: identity/config 0.95, rules 0.90, preferences 0.85, decisions 0.80, projects 0.75, operational 0.60",
|
||||
}),
|
||||
),
|
||||
userId: Type.Optional(
|
||||
Type.String({
|
||||
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: "Additional metadata to attach to this memory",
|
||||
}),
|
||||
),
|
||||
longTerm: Type.Optional(
|
||||
Type.Boolean({
|
||||
description:
|
||||
"Store as long-term (user-scoped) memory. Default: true. Set to false for session-scoped memory.",
|
||||
}),
|
||||
),
|
||||
// --- NEW CLI-parity parameters ---
|
||||
immutable: Type.Optional(
|
||||
Type.Boolean({
|
||||
description: "Prevent future updates to this memory",
|
||||
}),
|
||||
),
|
||||
infer: Type.Optional(
|
||||
Type.Boolean({
|
||||
description: "Set to false to skip inference and store raw",
|
||||
}),
|
||||
),
|
||||
expires: Type.Optional(
|
||||
Type.String({
|
||||
description: "Expiration date (YYYY-MM-DD)",
|
||||
}),
|
||||
),
|
||||
enableGraph: Type.Optional(
|
||||
Type.Boolean({
|
||||
description: "Enable graph memory extraction",
|
||||
}),
|
||||
),
|
||||
categories: Type.Optional(
|
||||
Type.Array(Type.String(), {
|
||||
description: "Categories for this memory",
|
||||
}),
|
||||
),
|
||||
}),
|
||||
|
||||
async execute(_toolCallId: string, params: Record<string, unknown>) {
|
||||
const p = params as {
|
||||
text?: string;
|
||||
facts?: string[];
|
||||
category?: string;
|
||||
importance?: number;
|
||||
userId?: string;
|
||||
agentId?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
longTerm?: boolean;
|
||||
// New CLI-parity params
|
||||
immutable?: boolean;
|
||||
infer?: boolean;
|
||||
expires?: string;
|
||||
enableGraph?: boolean;
|
||||
categories?: string[];
|
||||
};
|
||||
const { userId, agentId, longTerm = true } = p;
|
||||
|
||||
// Resolve facts: prefer 'facts' array, fall back to single 'text'
|
||||
const allFacts: string[] = p.facts?.length
|
||||
? p.facts
|
||||
: p.text
|
||||
? [p.text]
|
||||
: [];
|
||||
if (allFacts.length === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "No facts provided. Pass 'text' or 'facts' array.",
|
||||
},
|
||||
],
|
||||
details: { error: "missing_facts" },
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const currentSessionId = getCurrentSessionId();
|
||||
|
||||
// Block subagent writes at the tool level. The system prompt
|
||||
// instructs subagents not to store, but a disobedient tool call
|
||||
// would write to a transient namespace that is never read again.
|
||||
if (isSubagentSession(currentSessionId)) {
|
||||
api.logger.warn(
|
||||
"openclaw-mem0: blocked memory_store from subagent session",
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Memory storage is not available in subagent sessions. The main agent handles memory.",
|
||||
},
|
||||
],
|
||||
details: { error: "subagent_blocked" },
|
||||
};
|
||||
}
|
||||
|
||||
const uid = resolveUserId({ agentId, userId });
|
||||
const runId =
|
||||
!longTerm && currentSessionId ? currentSessionId : undefined;
|
||||
|
||||
// Skills mode: bypass extraction LLM, store directly via infer=false
|
||||
if (skillsActive) {
|
||||
// Enforce batch homogeneity: if no category provided for a multi-fact
|
||||
// batch, warn. The prompt teaches batch-by-category but this is the
|
||||
// runtime safety net.
|
||||
if (allFacts.length > 1 && !p.category) {
|
||||
api.logger.warn(
|
||||
`openclaw-mem0: multi-fact batch (${allFacts.length} facts) without category. Retention policy defaults to uncategorized. Prompt instructs batch-by-category.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Resolve metadata: prefer explicit params, fall back to metadata record
|
||||
const rawMetadata = p.metadata;
|
||||
const category =
|
||||
p.category ?? (rawMetadata?.category as string | undefined);
|
||||
const importance =
|
||||
p.importance ?? (rawMetadata?.importance as number | undefined);
|
||||
const parsedMetadata: Record<string, unknown> = {
|
||||
...(rawMetadata ?? {}),
|
||||
...(category && { category }),
|
||||
...(importance !== undefined && { importance }),
|
||||
};
|
||||
const categories = resolveCategories(cfg.skills);
|
||||
const catConfig = category ? categories[category] : undefined;
|
||||
const expirationDate = catConfig
|
||||
? ttlToExpirationDate(catConfig.ttl)
|
||||
: undefined;
|
||||
const isImmutable = catConfig?.immutable ?? false;
|
||||
|
||||
// Single API call: all facts go as deduced_memories array
|
||||
const addOpts: AddOptions = {
|
||||
user_id: uid,
|
||||
source: "OPENCLAW",
|
||||
infer: false,
|
||||
deduced_memories: allFacts,
|
||||
metadata: parsedMetadata ?? {},
|
||||
...(expirationDate && { expiration_date: expirationDate }),
|
||||
...(isImmutable && { immutable: true }),
|
||||
};
|
||||
if (runId) addOpts.run_id = runId;
|
||||
if (cfg.mode === "platform") {
|
||||
addOpts.output_format = "v1.1";
|
||||
if (cfg.enableGraph || cfg.skills?.triage?.enableGraph) {
|
||||
addOpts.enable_graph = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply new CLI-parity params (override category-derived values)
|
||||
if (p.immutable !== undefined) addOpts.immutable = p.immutable;
|
||||
if (p.infer !== undefined) addOpts.infer = p.infer;
|
||||
if (p.expires !== undefined) addOpts.expiration_date = p.expires;
|
||||
if (p.enableGraph !== undefined) addOpts.enable_graph = p.enableGraph;
|
||||
if (p.categories !== undefined)
|
||||
(addOpts as unknown as Record<string, unknown>).categories =
|
||||
p.categories;
|
||||
|
||||
const result = await provider!.add(
|
||||
[{ role: "user", content: allFacts.join("\n") }],
|
||||
addOpts,
|
||||
);
|
||||
|
||||
const count = result.results?.length ?? 0;
|
||||
api.logger.info(
|
||||
`openclaw-mem0: skills-mode stored ${count} memor${count === 1 ? "y" : "ies"} from ${allFacts.length} fact(s) in 1 API call (infer=false, category=${category ?? "none"})`,
|
||||
);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Stored ${allFacts.length} fact(s) [${category ?? "uncategorized"}]: ${allFacts.map((f) => `"${f.slice(0, 60)}${f.length > 60 ? "..." : ""}"`).join(", ")}`,
|
||||
},
|
||||
],
|
||||
details: {
|
||||
action: "stored",
|
||||
mode: "skills",
|
||||
infer: false,
|
||||
category,
|
||||
factCount: allFacts.length,
|
||||
results: result.results,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Legacy mode: let mem0 extraction LLM handle it
|
||||
const combinedText = allFacts.join("\n");
|
||||
|
||||
// Pre-check for near-duplicates so the extraction model has
|
||||
// context about existing memories and can UPDATE rather than ADD
|
||||
const preview = combinedText.slice(0, 200);
|
||||
const dedupOpts = buildSearchOptions(uid, 3);
|
||||
dedupOpts.threshold = 0.85;
|
||||
const existing = await provider!.search(preview, dedupOpts);
|
||||
if (existing.length > 0) {
|
||||
api.logger.info(
|
||||
`openclaw-mem0: found ${existing.length} similar existing memories — mem0 may update instead of add`,
|
||||
);
|
||||
}
|
||||
|
||||
const legacyAddOpts = buildAddOptions(uid, runId, currentSessionId);
|
||||
|
||||
// Apply new CLI-parity params to legacy mode as well
|
||||
if (p.immutable !== undefined) legacyAddOpts.immutable = p.immutable;
|
||||
if (p.infer !== undefined) legacyAddOpts.infer = p.infer;
|
||||
if (p.expires !== undefined) legacyAddOpts.expiration_date = p.expires;
|
||||
if (p.enableGraph !== undefined)
|
||||
legacyAddOpts.enable_graph = p.enableGraph;
|
||||
if (p.categories !== undefined)
|
||||
(legacyAddOpts as unknown as Record<string, unknown>).categories =
|
||||
p.categories;
|
||||
|
||||
const result = await provider!.add(
|
||||
[{ role: "user", content: combinedText }],
|
||||
legacyAddOpts,
|
||||
);
|
||||
|
||||
const added = result.results?.filter((r) => r.event === "ADD") ?? [];
|
||||
const updated =
|
||||
result.results?.filter((r) => r.event === "UPDATE") ?? [];
|
||||
|
||||
const summary = [];
|
||||
if (added.length > 0)
|
||||
summary.push(
|
||||
`${added.length} new memor${added.length === 1 ? "y" : "ies"} added`,
|
||||
);
|
||||
if (updated.length > 0)
|
||||
summary.push(
|
||||
`${updated.length} memor${updated.length === 1 ? "y" : "ies"} updated`,
|
||||
);
|
||||
if (summary.length === 0) summary.push("No new memories extracted");
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Stored: ${summary.join(", ")}. ${result.results?.map((r) => `[${r.event}] ${r.memory}`).join("; ") ?? ""}`,
|
||||
},
|
||||
],
|
||||
details: {
|
||||
action: "stored",
|
||||
results: result.results,
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Memory store failed: ${String(err)}`,
|
||||
},
|
||||
],
|
||||
details: { error: String(err) },
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* memory_update tool — extracted from index.ts registerTools().
|
||||
*
|
||||
* Updates an existing memory's text in place. Preserves the memory's
|
||||
* history and supports subagent blocking. Supports optional metadata
|
||||
* updates for CLI parity.
|
||||
*/
|
||||
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { isSubagentSession } from "../isolation.ts";
|
||||
import type { ToolContext } from "./index.ts";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tool factory
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Creates the `memory_update` tool config object suitable for
|
||||
* `api.registerTool(config, { name })`.
|
||||
*/
|
||||
export function createMemoryUpdateTool(ctx: ToolContext) {
|
||||
const { api, provider, getCurrentSessionId } = ctx;
|
||||
|
||||
return {
|
||||
name: "memory_update",
|
||||
label: "Memory Update",
|
||||
description:
|
||||
"Update an existing memory's text in place. Use when a fact has changed and you have the memory ID. This is atomic and preserves the memory's history. Preferred over delete-then-store for corrections.",
|
||||
parameters: Type.Object({
|
||||
memoryId: Type.String({ description: "The memory ID to update" }),
|
||||
text: Type.String({
|
||||
description: "The new text for this memory (replaces the old text)",
|
||||
}),
|
||||
// --- NEW CLI-parity parameter ---
|
||||
metadata: Type.Optional(
|
||||
Type.Record(Type.String(), Type.Unknown(), {
|
||||
description: "Metadata to update (JSON object)",
|
||||
}),
|
||||
),
|
||||
}),
|
||||
|
||||
async execute(_toolCallId: string, params: Record<string, unknown>) {
|
||||
const { memoryId, text, metadata } = params as {
|
||||
memoryId: string;
|
||||
text: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
try {
|
||||
const currentSessionId = getCurrentSessionId();
|
||||
if (isSubagentSession(currentSessionId)) {
|
||||
api.logger.warn(
|
||||
"openclaw-mem0: blocked memory_update from subagent session",
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Memory update is not available in subagent sessions.",
|
||||
},
|
||||
],
|
||||
details: { error: "subagent_blocked" },
|
||||
};
|
||||
}
|
||||
|
||||
await provider!.update(memoryId, text);
|
||||
|
||||
// If metadata was provided, note it in the response. The provider's
|
||||
// update() currently only accepts (id, text). Metadata-only updates
|
||||
// may need to go through the backend for full support.
|
||||
let metadataNote = "";
|
||||
if (metadata && Object.keys(metadata).length > 0) {
|
||||
metadataNote = `\nNote: metadata was provided but the current provider only supports text updates. Metadata-only updates may require backend support.`;
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Updated memory ${memoryId}: "${text.slice(0, 80)}${text.length > 80 ? "..." : ""}"${metadataNote}`,
|
||||
},
|
||||
],
|
||||
details: { action: "updated", id: memoryId, metadata: metadata },
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
content: [
|
||||
{ type: "text", text: `Memory update failed: ${String(err)}` },
|
||||
],
|
||||
details: { error: String(err) },
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user