Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ Official plugin maintained by MemTensor.
A minimal OpenClaw lifecycle plugin that **recalls** memories from MemOS Cloud before each run and **adds** new messages to MemOS Cloud after each run.

## Features
- **Recall**: `before_agent_start` → `/search/memory`
- **Add**: `agent_end` → `/add/message`
- **Recall**: `before_agent_start` → `/product/search`
- **Add**: `agent_end` → `/product/add`
- Uses **Token** auth (`Authorization: Token <MEMOS_API_KEY>`)

## Install
Expand Down Expand Up @@ -92,7 +92,7 @@ MEMOS_API_KEY=YOUR_TOKEN
- `MEMOS_API_KEY` (required; Token auth) — get it at https://memos-dashboard.openmem.net/cn/apikeys/
- `MEMOS_USER_ID` (optional; default: `openclaw-user`)
- `MEMOS_CONVERSATION_ID` (optional override)
- `MEMOS_RECALL_GLOBAL` (default: `true`; when true, search does **not** pass conversation_id)
- `MEMOS_RECALL_GLOBAL` (default: `true`; when true, search does **not** pass `session_id`)
- `MEMOS_MULTI_AGENT_MODE` (default: `false`; enable multi-agent data isolation)
- `MEMOS_CONVERSATION_PREFIX` / `MEMOS_CONVERSATION_SUFFIX` (optional)
- `MEMOS_CONVERSATION_SUFFIX_MODE` (`none` | `counter`, default: `none`)
Expand Down Expand Up @@ -158,24 +158,24 @@ In `plugins.entries.memos-cloud-openclaw-plugin.config`:

## How it Works
- **Recall** (`before_agent_start`)
- Builds a `/search/memory` request using `user_id`, `query` (= prompt + optional prefix), and optional filters.
- Default **global recall**: when `recallGlobal=true`, it does **not** pass `conversation_id`.
- Builds a `/product/search` request using `user_id`, `query` (= prompt + optional prefix), and optional filters.
- Default **global recall**: when `recallGlobal=true`, it does **not** pass `session_id`.
- Optional second-pass filtering: if `recallFilterEnabled=true`, candidates are sent to your configured model and only returned `keep` items are injected.
- Injects a stable MemOS recall protocol via `appendSystemContext`, while the retrieved `<memories>` block remains in `prependContext`.

- **Add** (`agent_end`)
- Builds a `/add/message` request with the **last turn** by default (user + assistant).
- Sends `messages` with `user_id`, `conversation_id`, and optional `tags/info/agent_id/app_id`.
- Builds a `/product/add` request with the **last turn** by default (user + assistant).
- Sends `messages` with `user_id`, `session_id`, and optional `tags/info/agent_id/app_id`.

## Multi-Agent Support
The plugin provides native support for multi-agent architectures (via the `agent_id` parameter):
- **Enable Mode**: Set `"multiAgentMode": true` in config or `MEMOS_MULTI_AGENT_MODE=true` in env variables (default is `false`).
- **Dynamic Context**: When enabled, it automatically captures `ctx.agentId` during OpenClaw lifecycle hooks. (Note: the default OpenClaw agent `"main"` is ignored to preserve backwards compatibility for single-agent users).
- **Data Isolation**: The `agent_id` is automatically injected into both `/search/memory` and `/add/message` requests. This ensures completely isolated memory and message histories for different agents, even under the same user or session.
- **Data Isolation**: The `agent_id` is automatically injected into both `/product/search` and `/product/add` requests. This ensures completely isolated memory and message histories for different agents, even under the same user or session.
- **Static Override**: You can also force a specific agent ID by setting `"agentId": "your_agent_id"` in the plugin's `config`.

## Notes
- `conversation_id` defaults to OpenClaw `sessionKey` (unless `conversationId` is provided). **TODO**: consider binding to OpenClaw `sessionId` directly.
- `session_id` defaults to OpenClaw `sessionKey` (unless `conversationId` is provided). **TODO**: consider binding to OpenClaw `sessionId` directly.
- Optional **prefix/suffix** via env or config; `conversationSuffixMode=counter` increments on `/new` (requires `hooks.internal.enabled`).

## Acknowledgements
Expand Down
18 changes: 9 additions & 9 deletions README_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
- **添加记忆**:在每轮对话结束后把消息写回 MemOS Cloud

## 功能
- **Recall**:`before_agent_start` → `/search/memory`
- **Add**:`agent_end` → `/add/message`
- **Recall**:`before_agent_start` → `/product/search`
- **Add**:`agent_end` → `/product/add`
- 使用 **Token** 认证(`Authorization: Token <MEMOS_API_KEY>`)

## 安装
Expand Down Expand Up @@ -94,7 +94,7 @@ MEMOS_API_KEY=YOUR_TOKEN
- `MEMOS_API_KEY`(必填,Token 认证)—— 获取地址:https://memos-dashboard.openmem.net/cn/apikeys/
- `MEMOS_USER_ID`(可选,默认 `openclaw-user`)
- `MEMOS_CONVERSATION_ID`(可选覆盖)
- `MEMOS_RECALL_GLOBAL`(默认 `true`;为 true 时检索不传 conversation_id
- `MEMOS_RECALL_GLOBAL`(默认 `true`;为 true 时检索不传 `session_id`
- `MEMOS_MULTI_AGENT_MODE`(默认 `false`;是否开启多 Agent 数据隔离模式)
- `MEMOS_CONVERSATION_PREFIX` / `MEMOS_CONVERSATION_SUFFIX`(可选)
- `MEMOS_CONVERSATION_SUFFIX_MODE`(`none` | `counter`,默认 `none`)
Expand Down Expand Up @@ -158,29 +158,29 @@ MEMOS_API_KEY=YOUR_TOKEN

## 工作原理
### 1) 召回(before_agent_start)
- 组装 `/search/memory` 请求
- 组装 `/product/search` 请求
- `user_id`、`query`(= prompt + 可选前缀)
- 默认**全局召回**:`recallGlobal=true` 时不传 `conversation_id`
- 默认**全局召回**:`recallGlobal=true` 时不传 `session_id`
- 可选 `filter` / `knowledgebase_ids`
- (可选)若开启 `recallFilterEnabled`,会先把 `memory/preference/tool_memory` 候选发给你配置的模型做二次筛选,只保留 `keep` 的条目
- 将稳定的 MemOS 召回协议通过 `appendSystemContext` 注入,而检索到的 `<memories>` 数据块继续通过 `prependContext` 注入

### 2) 添加(agent_end)
- 默认只写**最后一轮**(user + assistant)
- 构造 `/add/message` 请求:
- `user_id`、`conversation_id`
- 构造 `/product/add` 请求:
- `user_id`、`session_id`
- `messages` 列表
- 可选 `tags / info / agent_id / app_id`

## 多Agent支持(Multi-Agent)
插件内置对多Agent模式的支持(`agent_id` 参数):
- **开启模式**:需要在配置中设置 `"multiAgentMode": true` 或在环境变量中设置 `MEMOS_MULTI_AGENT_MODE=true`(默认为 `false`)。
- **动态获取**:开启后,执行生命周期钩子时会自动读取上下文中的 `ctx.agentId`。(注:OpenClaw 的默认 Agent `"main"` 会被自动忽略,以保证老用户的单 Agent 数据兼容性)。
- **数据隔离**:在调用 `/search/memory`(检索记忆)和 `/add/message`(添加记录)时会自动附带该 `agent_id`,从而保证即使是同一用户下的不同 Agent 之间,记忆和反馈数据也是完全隔离的。
- **数据隔离**:在调用 `/product/search`(检索记忆)和 `/product/add`(添加记录)时会自动附带该 `agent_id`,从而保证即使是同一用户下的不同 Agent 之间,记忆和反馈数据也是完全隔离的。
- **静态配置**:如果需要,也可在上述插件的 `config` 中显式指定 `"agentId": "your_agent_id"` 作为固定值。

## 说明
- 未显式指定 `conversation_id` 时,默认使用 OpenClaw `sessionKey`。**TODO**:后续考虑直接绑定 OpenClaw `sessionId`。
- 未显式指定 `session_id` 时,默认使用 OpenClaw `sessionKey`。**TODO**:后续考虑直接绑定 OpenClaw `sessionId`。
- 可配置前后缀;`conversationSuffixMode=counter` 时会在 `/new` 递增(需 `hooks.internal.enabled`)。

## 致谢
Expand Down
6 changes: 3 additions & 3 deletions clawdbot.plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@
},
"conversationId": {
"type": "string",
"description": "Override conversation_id"
"description": "Override session_id"
},
"conversationIdPrefix": {
"type": "string",
"description": "conversation_id prefix"
"description": "session_id prefix"
},
"conversationIdSuffix": {
"type": "string",
"description": "conversation_id suffix"
"description": "session_id suffix"
},
"conversationSuffixMode": {
"type": "string",
Expand Down
43 changes: 21 additions & 22 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ function buildSearchPayload(cfg, prompt, ctx) {

if (!cfg.recallGlobal) {
const conversationId = resolveConversationId(cfg, ctx);
if (conversationId) payload.conversation_id = conversationId;
if (conversationId) payload.session_id = conversationId;
}

let filterObj = cfg.filter ? JSON.parse(JSON.stringify(cfg.filter)) : null;
Expand All @@ -101,42 +101,41 @@ function buildSearchPayload(cfg, prompt, ctx) {

if (filterObj) payload.filter = filterObj;

if (cfg.knowledgebaseIds?.length) payload.knowledgebase_ids = cfg.knowledgebaseIds;
if (cfg.knowledgebaseIds?.length) payload.readable_cube_ids = cfg.knowledgebaseIds;

payload.memory_limit_number = cfg.memoryLimitNumber;
payload.top_k = cfg.memoryLimitNumber;
payload.include_preference = cfg.includePreference;
payload.preference_limit_number = cfg.preferenceLimitNumber;
payload.include_tool_memory = cfg.includeToolMemory;
payload.tool_memory_limit_number = cfg.toolMemoryLimitNumber;
payload.pref_top_k = cfg.preferenceLimitNumber;
payload.search_tool_memory = cfg.includeToolMemory;
payload.tool_mem_top_k = cfg.toolMemoryLimitNumber;
payload.relativity = cfg.relativity;

return payload;
}

function buildAddMessagePayload(cfg, messages, ctx) {
const payload = {
user_id: cfg.userId,
conversation_id: resolveConversationId(cfg, ctx),
messages,
source: MEMOS_SOURCE,
};

const agentId = getEffectiveAgentId(cfg, ctx);
if (agentId) payload.agent_id = agentId;
if (cfg.appId) payload.app_id = cfg.appId;
if (cfg.tags?.length) payload.tags = cfg.tags;

const info = {
source: "openclaw",
sessionKey: ctx?.sessionKey,
agentId: ctx?.agentId,
...(cfg.info || {}),
};
if (Object.keys(info).length > 0) payload.info = info;
const agentId = getEffectiveAgentId(cfg, ctx);
if (agentId) info.agent_id = agentId;
if (cfg.appId) info.app_id = cfg.appId;
if (cfg.allowPublic !== undefined) info.allow_public = cfg.allowPublic;

const payload = {
user_id: cfg.userId,
session_id: resolveConversationId(cfg, ctx),
messages,
source: MEMOS_SOURCE,
info,
async_mode: cfg.asyncMode === "sync" || cfg.asyncMode === false ? "sync" : "async",
};

payload.allow_public = cfg.allowPublic;
if (cfg.allowKnowledgebaseIds?.length) payload.allow_knowledgebase_ids = cfg.allowKnowledgebaseIds;
payload.async_mode = cfg.asyncMode;
if (cfg.tags?.length) payload.custom_tags = cfg.tags;
if (cfg.allowKnowledgebaseIds?.length) payload.writable_cube_ids = cfg.allowKnowledgebaseIds;

return payload;
}
Expand Down
55 changes: 52 additions & 3 deletions lib/memos-cloud-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,56 @@ function stripQuotes(value) {

export function extractResultData(result) {
if (!result || typeof result !== "object") return null;
return result.data ?? result.data?.data ?? result.data?.result ?? null;
const data = result.data ?? result.data?.data ?? result.data?.result ?? null;
return normalizeSearchData(data);
}

function flattenSearchBucket(buckets, valueMapper) {
if (!Array.isArray(buckets)) return [];
const list = [];
for (const bucket of buckets) {
const memories = Array.isArray(bucket?.memories) ? bucket.memories : [];
for (const item of memories) {
const value = valueMapper(item);
if (!value) continue;
list.push({
...item,
...value,
relativity: item?.metadata?.relativity ?? item?.metadata?.score ?? 1,
create_time: item?.metadata?.create_time ?? item?.metadata?.created_at ?? item?.created_at,
});
}
}
return list;
}

function normalizeSearchData(data) {
if (!data || typeof data !== "object") return data;
const hasLegacyShape =
Array.isArray(data.memory_detail_list) ||
Array.isArray(data.preference_detail_list) ||
Array.isArray(data.tool_memory_detail_list);
if (hasLegacyShape) return data;

const hasServerShape =
Array.isArray(data.text_mem) || Array.isArray(data.pref_mem) || Array.isArray(data.tool_mem);
if (!hasServerShape) return data;

return {
...data,
memory_detail_list: flattenSearchBucket(data.text_mem, (item) => ({
memory_value: item?.memory || item?.metadata?.memory || "",
memory_key: item?.memory || item?.metadata?.memory || "",
})),
preference_detail_list: flattenSearchBucket(data.pref_mem, (item) => ({
preference: item?.memory || item?.metadata?.memory || "",
preference_type: item?.metadata?.preference_type || "",
})),
tool_memory_detail_list: flattenSearchBucket(data.tool_mem, (item) => ({
tool_value: item?.memory || item?.metadata?.memory || "",
})),
preference_note: data.pref_note ?? data.preference_note ?? "",
};
}

function pad2(value) {
Expand Down Expand Up @@ -318,7 +367,7 @@ function sanitizeAddMessageEntry(entry) {
}

export async function searchMemory(cfg, payload) {
return callApi(cfg, "/search/memory", sanitizeSearchPayload(payload));
return callApi(cfg, "/product/search", payload);
}

export async function addMessage(cfg, payload) {
Expand All @@ -329,7 +378,7 @@ export async function addMessage(cfg, payload) {
// Fail open: if sanitization throws unexpectedly, send original payload.
finalPayload = payload;
}
return callApi(cfg, "/add/message", finalPayload);
return callApi(cfg, "/product/add", finalPayload);
}

function isInboundMetaSentinelLine(line) {
Expand Down
6 changes: 3 additions & 3 deletions moltbot.plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@
},
"conversationId": {
"type": "string",
"description": "Override conversation_id"
"description": "Override session_id"
},
"conversationIdPrefix": {
"type": "string",
"description": "conversation_id prefix"
"description": "session_id prefix"
},
"conversationIdSuffix": {
"type": "string",
"description": "conversation_id suffix"
"description": "session_id suffix"
},
"conversationSuffixMode": {
"type": "string",
Expand Down
6 changes: 3 additions & 3 deletions openclaw.plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@
},
"conversationId": {
"type": "string",
"description": "Override conversation_id"
"description": "Override session_id"
},
"conversationIdPrefix": {
"type": "string",
"description": "conversation_id prefix"
"description": "session_id prefix"
},
"conversationIdSuffix": {
"type": "string",
"description": "conversation_id suffix"
"description": "session_id suffix"
},
"conversationSuffixMode": {
"type": "string",
Expand Down