@memtensor/memos-cloud-openclaw-plugin 0.1.10 → 0.1.11-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -55,7 +55,8 @@ Make sure it’s enabled in `~/.openclaw/openclaw.json`:
55
55
  Restart the gateway after config changes.
56
56
 
57
57
  ## Environment Variables
58
- The plugin tries env files in order (**openclaw → moltbot → clawdbot**). For each key, the first file with a value wins.
58
+ The plugin resolves runtime config in this order: **plugin config → env files → process environment**.
59
+ Among env files, it tries them in order (**openclaw → moltbot → clawdbot**). For each key, the first file with a value wins.
59
60
  If none of these files exist (or the key is missing), it falls back to the process environment.
60
61
 
61
62
  **Where to configure**
@@ -91,9 +92,14 @@ MEMOS_API_KEY=YOUR_TOKEN
91
92
  - `MEMOS_BASE_URL` (default: `https://memos.memtensor.cn/api/openmem/v1`)
92
93
  - `MEMOS_API_KEY` (required; Token auth) — get it at https://memos-dashboard.openmem.net/cn/apikeys/
93
94
  - `MEMOS_USER_ID` (optional; default: `openclaw-user`)
95
+ - `MEMOS_USE_DIRECT_SESSION_USER_ID` (default: `false`; when enabled, direct session keys like `agent:main:<provider>:direct:<peer-id>` use `<peer-id>` as MemOS `user_id`)
94
96
  - `MEMOS_CONVERSATION_ID` (optional override)
97
+ - `MEMOS_KNOWLEDGEBASE_IDS` (optional; comma-separated global knowledge base IDs for `/search/memory`, e.g., `"kb-123, kb-456"`)
98
+ - `MEMOS_ALLOW_KNOWLEDGEBASE_IDS` (optional; comma-separated knowledge base IDs for `/add/message`, e.g., `"kb-123"`)
99
+ - `MEMOS_TAGS` (optional; comma-separated tags for `/add/message`, default: `"openclaw"`, e.g., `"openclaw, dev"`)
95
100
  - `MEMOS_RECALL_GLOBAL` (default: `true`; when true, search does **not** pass conversation_id)
96
101
  - `MEMOS_MULTI_AGENT_MODE` (default: `false`; enable multi-agent data isolation)
102
+ - `MEMOS_ALLOWED_AGENTS` (optional; comma-separated allowlist for multi-agent mode, e.g. `"agent1,agent2"`; empty means all agents enabled)
97
103
  - `MEMOS_CONVERSATION_PREFIX` / `MEMOS_CONVERSATION_SUFFIX` (optional)
98
104
  - `MEMOS_CONVERSATION_SUFFIX_MODE` (`none` | `counter`, default: `none`)
99
105
  - `MEMOS_CONVERSATION_RESET_ON_NEW` (default: `true`, requires hooks.internal.enabled)
@@ -119,6 +125,7 @@ In `plugins.entries.memos-cloud-openclaw-plugin.config`:
119
125
  "baseUrl": "https://memos.memtensor.cn/api/openmem/v1",
120
126
  "apiKey": "YOUR_API_KEY",
121
127
  "userId": "memos_user_123",
128
+ "useDirectSessionUserId": false,
122
129
  "conversationId": "openclaw-main",
123
130
  "queryPrefix": "important user context preferences decisions ",
124
131
  "recallEnabled": true,
@@ -141,6 +148,7 @@ In `plugins.entries.memos-cloud-openclaw-plugin.config`:
141
148
  "tags": ["openclaw"],
142
149
  "agentId": "",
143
150
  "multiAgentMode": false,
151
+ "allowedAgents": [],
144
152
  "asyncMode": true,
145
153
  "recallFilterEnabled": false,
146
154
  "recallFilterBaseUrl": "http://127.0.0.1:11434/v1",
@@ -174,6 +182,133 @@ The plugin provides native support for multi-agent architectures (via the `agent
174
182
  - **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.
175
183
  - **Static Override**: You can also force a specific agent ID by setting `"agentId": "your_agent_id"` in the plugin's `config`.
176
184
 
185
+ ### Per-Agent Memory Toggle
186
+
187
+ In multi-agent mode, you can use `MEMOS_ALLOWED_AGENTS` to control exactly which agents have memory enabled. Agents not in the allowlist will skip both memory recall and memory capture entirely.
188
+
189
+ **Environment variable** (in `~/.openclaw/.env`):
190
+ ```env
191
+ MEMOS_MULTI_AGENT_MODE=true
192
+ MEMOS_ALLOWED_AGENTS="agent1,agent2"
193
+ ```
194
+
195
+ Separate multiple agent IDs with commas.
196
+
197
+ **Plugin config** (in `openclaw.json`):
198
+ ```json
199
+ {
200
+ "plugins": {
201
+ "entries": {
202
+ "memos-cloud-openclaw-plugin": {
203
+ "enabled": true,
204
+ "config": {
205
+ "multiAgentMode": true,
206
+ "allowedAgents": ["agent1", "agent2"]
207
+ }
208
+ }
209
+ }
210
+ }
211
+ }
212
+ ```
213
+
214
+ **Behavior**:
215
+ | Config | Effect |
216
+ |--------|--------|
217
+ | `MEMOS_ALLOWED_AGENTS` unset or empty | All agents have memory enabled |
218
+ | `MEMOS_ALLOWED_AGENTS="agent1,agent2"` | Only `agent1` and `agent2` are enabled; others are skipped |
219
+ | `MEMOS_ALLOWED_AGENTS="agent1"` | Only `agent1` is enabled; all other agents are skipped |
220
+ | `MEMOS_MULTI_AGENT_MODE=false` | Allowlist has no effect; all requests use single-agent mode |
221
+
222
+ > **Note**: The allowlist only takes effect when `multiAgentMode=true`. When multi-agent mode is off, memory works for all agents and the allowlist is ignored.
223
+
224
+ ### Per-Agent Configuration (agentOverrides)
225
+
226
+ Beyond simple on/off toggles, you can configure **different memory parameters for each agent** using `agentOverrides`. Each agent can have its own knowledge base, recall limits, relativity threshold, and more.
227
+
228
+ **Plugin config** (in `openclaw.json`):
229
+ ```json
230
+ {
231
+ "plugins": {
232
+ "entries": {
233
+ "memos-cloud-openclaw-plugin": {
234
+ "enabled": true,
235
+ "config": {
236
+ "multiAgentMode": true,
237
+ "allowedAgents": ["default", "research-agent", "coding-agent"],
238
+ "knowledgebaseIds": [],
239
+ "memoryLimitNumber": 6,
240
+ "relativity": 0.45,
241
+
242
+ "agentOverrides": {
243
+ "research-agent": {
244
+ "knowledgebaseIds": ["kb-research-papers", "kb-academic"],
245
+ "memoryLimitNumber": 12,
246
+ "relativity": 0.3,
247
+ "includeToolMemory": true,
248
+ "captureStrategy": "full_session",
249
+ "queryPrefix": "research context: "
250
+ },
251
+ "coding-agent": {
252
+ "knowledgebaseIds": ["kb-codebase", "kb-api-docs"],
253
+ "memoryLimitNumber": 9,
254
+ "relativity": 0.5,
255
+ "addEnabled": false
256
+ }
257
+ }
258
+ }
259
+ }
260
+ }
261
+ }
262
+ }
263
+ ```
264
+
265
+ **Environment variable** (in `~/.openclaw/.env`):
266
+ You can use `MEMOS_AGENT_OVERRIDES` to configure a JSON string to override global parameters. Note: `.env` configuration has a lower priority than `agentOverrides` in `openclaw.json`.
267
+ ```env
268
+ MEMOS_AGENT_OVERRIDES='{"research-agent": {"memoryLimitNumber": 12, "relativity": 0.3}, "coding-agent": {"memoryLimitNumber": 9}}'
269
+ ```
270
+
271
+ **How it works**:
272
+ - Fields in `agentOverrides.<agentId>` override the global defaults for that specific agent.
273
+ - Only the fields you specify are overridden; all other parameters inherit from the global config.
274
+ - If no override exists for an agent, it uses the global config as-is.
275
+
276
+ **Overridable fields**:
277
+
278
+ | Field | Description |
279
+ |-------|-------------|
280
+ | `knowledgebaseIds` | Knowledge base IDs for `/search/memory` |
281
+ | `memoryLimitNumber` | Max memory items to recall |
282
+ | `preferenceLimitNumber` | Max preference items to recall |
283
+ | `includePreference` | Enable preference recall |
284
+ | `includeToolMemory` | Enable tool memory recall |
285
+ | `toolMemoryLimitNumber` | Max tool memory items |
286
+ | `relativity` | Relevance threshold (0-1) |
287
+ | `recallEnabled` | Enable/disable recall for this agent |
288
+ | `addEnabled` | Enable/disable memory capture for this agent |
289
+ | `captureStrategy` | `last_turn` or `full_session` |
290
+ | `queryPrefix` | Prefix for search queries |
291
+ | `maxItemChars` | Max chars per memory item in prompt |
292
+ | `maxMessageChars` | Max chars per message when adding |
293
+ | `includeAssistant` | Include assistant messages in capture |
294
+ | `recallGlobal` | Global recall (skip conversation_id) |
295
+ | `recallFilterEnabled` | Enable model-based recall filtering |
296
+ | `recallFilterModel` | Model for recall filtering |
297
+ | `recallFilterBaseUrl` | Base URL for recall filter model |
298
+ | `recallFilterApiKey` | API key for recall filter |
299
+ | `allowKnowledgebaseIds` | Knowledge bases for `/add/message` |
300
+ | `tags` | Tags for `/add/message` |
301
+ | `throttleMs` | Throttle interval |
302
+
303
+ ## Direct Session User ID
304
+ - **Default behavior**: the plugin still uses the configured `userId` (or `MEMOS_USER_ID`) and stays fully backward compatible.
305
+ - **Enable mode**: set `"useDirectSessionUserId": true` in plugin config or `MEMOS_USE_DIRECT_SESSION_USER_ID=true` in env.
306
+ - **What it does**: when enabled, session keys like `agent:main:<provider>:direct:<peer-id>` reuse `<peer-id>` as MemOS `user_id`.
307
+ - **What it does not do**: non-direct session keys such as `agent:main:<provider>:channel:<channel-id>` keep using the configured fallback `userId`.
308
+ - **Request paths affected**: the same resolver is used by both `buildSearchPayload()` and `buildAddMessagePayload()`, so recall and add stay consistent.
309
+ - **Config precedence**: runtime config still follows the same rule as the rest of the plugin - plugin config first, then `.env` files (`~/.openclaw/.env` -> `~/.moltbot/.env` -> `~/.clawdbot/.env`), then process env.
310
+
311
+
177
312
  ## Notes
178
313
  - `conversation_id` defaults to OpenClaw `sessionKey` (unless `conversationId` is provided). **TODO**: consider binding to OpenClaw `sessionId` directly.
179
314
  - Optional **prefix/suffix** via env or config; `conversationSuffixMode=counter` increments on `/new` (requires `hooks.internal.enabled`).
package/README_ZH.md CHANGED
@@ -57,7 +57,8 @@ openclaw gateway restart
57
57
  修改配置后需要重启 gateway。
58
58
 
59
59
  ## 环境变量
60
- 插件按顺序读取 env 文件(**openclaw → moltbot → clawdbot**),每个键优先使用最先匹配到的值。
60
+ 插件运行时配置的优先级是:**插件 config → env 文件 → 进程环境变量**。
61
+ 在 env 文件层,按顺序读取(**openclaw → moltbot → clawdbot**),每个键优先使用最先匹配到的值。
61
62
  若三个文件都不存在(或该键未找到),才会回退到进程环境变量。
62
63
 
63
64
  **配置位置**
@@ -93,9 +94,14 @@ MEMOS_API_KEY=YOUR_TOKEN
93
94
  - `MEMOS_BASE_URL`(默认 `https://memos.memtensor.cn/api/openmem/v1`)
94
95
  - `MEMOS_API_KEY`(必填,Token 认证)—— 获取地址:https://memos-dashboard.openmem.net/cn/apikeys/
95
96
  - `MEMOS_USER_ID`(可选,默认 `openclaw-user`)
97
+ - `MEMOS_USE_DIRECT_SESSION_USER_ID`(默认 `false`;开启后,对 `agent:main:<provider>:direct:<peer-id>` 这类私聊 sessionKey,会把 `<peer-id>` 作为 MemOS `user_id`)
96
98
  - `MEMOS_CONVERSATION_ID`(可选覆盖)
99
+ - `MEMOS_KNOWLEDGEBASE_IDS`(可选;逗号分隔的全局知识库 ID 列表,用于 `/search/memory`,例如:`"kb-123, kb-456"`)
100
+ - `MEMOS_ALLOW_KNOWLEDGEBASE_IDS`(可选;逗号分隔的知识库 ID 列表,用于 `/add/message`,例如:`"kb-123"`)
101
+ - `MEMOS_TAGS`(可选;逗号分隔的标签列表,用于 `/add/message`,默认:`"openclaw"`,例如:`"openclaw, dev"`)
97
102
  - `MEMOS_RECALL_GLOBAL`(默认 `true`;为 true 时检索不传 conversation_id)
98
103
  - `MEMOS_MULTI_AGENT_MODE`(默认 `false`;是否开启多 Agent 数据隔离模式)
104
+ - `MEMOS_ALLOWED_AGENTS`(可选;多 Agent 模式下的白名单,逗号分隔,例如 `"agent1,agent2"`;为空则所有 Agent 均启用)
99
105
  - `MEMOS_CONVERSATION_PREFIX` / `MEMOS_CONVERSATION_SUFFIX`(可选)
100
106
  - `MEMOS_CONVERSATION_SUFFIX_MODE`(`none` | `counter`,默认 `none`)
101
107
  - `MEMOS_CONVERSATION_RESET_ON_NEW`(默认 `true`,需 hooks.internal.enabled)
@@ -121,6 +127,7 @@ MEMOS_API_KEY=YOUR_TOKEN
121
127
  "baseUrl": "https://memos.memtensor.cn/api/openmem/v1",
122
128
  "apiKey": "YOUR_API_KEY",
123
129
  "userId": "memos_user_123",
130
+ "useDirectSessionUserId": false,
124
131
  "conversationId": "openclaw-main",
125
132
  "queryPrefix": "important user context preferences decisions ",
126
133
  "recallEnabled": true,
@@ -141,6 +148,7 @@ MEMOS_API_KEY=YOUR_TOKEN
141
148
  "tags": ["openclaw"],
142
149
  "agentId": "",
143
150
  "multiAgentMode": false,
151
+ "allowedAgents": [],
144
152
  "asyncMode": true,
145
153
  "recallFilterEnabled": false,
146
154
  "recallFilterBaseUrl": "http://127.0.0.1:11434/v1",
@@ -179,6 +187,133 @@ MEMOS_API_KEY=YOUR_TOKEN
179
187
  - **数据隔离**:在调用 `/search/memory`(检索记忆)和 `/add/message`(添加记录)时会自动附带该 `agent_id`,从而保证即使是同一用户下的不同 Agent 之间,记忆和反馈数据也是完全隔离的。
180
188
  - **静态配置**:如果需要,也可在上述插件的 `config` 中显式指定 `"agentId": "your_agent_id"` 作为固定值。
181
189
 
190
+ ### 按 Agent 开关记忆插件
191
+
192
+ 在多 Agent 模式下,可以通过 `MEMOS_ALLOWED_AGENTS` 精确控制哪些 Agent 启用记忆功能。未在白名单中的 Agent 将完全跳过记忆召回和记忆添加。
193
+
194
+ **环境变量配置**(在 `~/.openclaw/.env` 中设置):
195
+ ```env
196
+ MEMOS_MULTI_AGENT_MODE=true
197
+ MEMOS_ALLOWED_AGENTS="agent1,agent2"
198
+ ```
199
+
200
+ 多个 Agent ID 之间用英文逗号分隔。
201
+
202
+ **插件配置**(在 `openclaw.json` 中设置):
203
+ ```json
204
+ {
205
+ "plugins": {
206
+ "entries": {
207
+ "memos-cloud-openclaw-plugin": {
208
+ "enabled": true,
209
+ "config": {
210
+ "multiAgentMode": true,
211
+ "allowedAgents": ["agent1", "agent2"]
212
+ }
213
+ }
214
+ }
215
+ }
216
+ }
217
+ ```
218
+
219
+ **行为规则**:
220
+ | 配置 | 效果 |
221
+ |------|------|
222
+ | `MEMOS_ALLOWED_AGENTS` 未设置或为空 | 所有 Agent 均启用记忆 |
223
+ | `MEMOS_ALLOWED_AGENTS="agent1,agent2"` | 仅 `agent1` 和 `agent2` 启用,其余跳过 |
224
+ | `MEMOS_ALLOWED_AGENTS="agent1"` | 仅 `agent1` 启用,其他 Agent 均跳过 |
225
+ | `MEMOS_MULTI_AGENT_MODE=false` | 白名单不生效,所有请求按单 Agent 模式处理 |
226
+
227
+ > **注意**:白名单仅在 `multiAgentMode=true` 时生效。关闭多 Agent 模式时,所有 Agent 的记忆功能均正常工作,白名单配置被忽略。
228
+
229
+ ### 按 Agent 独立配置参数(agentOverrides)
230
+
231
+ 除了按 Agent 开关记忆功能外,你还可以通过 `agentOverrides` 为**每个 Agent 配置不同的记忆参数**,包括知识库、召回条数、相关性阈值等。
232
+
233
+ **插件配置**(在 `openclaw.json` 中设置):
234
+ ```json
235
+ {
236
+ "plugins": {
237
+ "entries": {
238
+ "memos-cloud-openclaw-plugin": {
239
+ "enabled": true,
240
+ "config": {
241
+ "multiAgentMode": true,
242
+ "allowedAgents": ["default", "research-agent", "coding-agent"],
243
+ "knowledgebaseIds": [],
244
+ "memoryLimitNumber": 6,
245
+ "relativity": 0.45,
246
+
247
+ "agentOverrides": {
248
+ "research-agent": {
249
+ "knowledgebaseIds": ["kb-research-papers", "kb-academic"],
250
+ "memoryLimitNumber": 12,
251
+ "relativity": 0.3,
252
+ "includeToolMemory": true,
253
+ "captureStrategy": "full_session",
254
+ "queryPrefix": "research context: "
255
+ },
256
+ "coding-agent": {
257
+ "knowledgebaseIds": ["kb-codebase", "kb-api-docs"],
258
+ "memoryLimitNumber": 9,
259
+ "relativity": 0.5,
260
+ "addEnabled": false
261
+ }
262
+ }
263
+ }
264
+ }
265
+ }
266
+ }
267
+ }
268
+ ```
269
+
270
+ **环境变量配置**(在 `~/.openclaw/.env` 中设置):
271
+ 你可以使用 `MEMOS_AGENT_OVERRIDES` 来配置一个 JSON 字符串,覆盖全局参数。注意:`.env` 中的配置优先级低于 `openclaw.json` 中的 `agentOverrides` 配置。
272
+ ```env
273
+ MEMOS_AGENT_OVERRIDES='{"research-agent": {"memoryLimitNumber": 12, "relativity": 0.3}, "coding-agent": {"memoryLimitNumber": 9}}'
274
+ ```
275
+
276
+ **工作原理**:
277
+ - `agentOverrides.<agentId>` 中的字段会覆盖该 Agent 对应的全局默认值
278
+ - 只需写需要覆盖的字段,其余参数从全局配置继承
279
+ - 若某个 Agent 没有对应的 override 条目,则完全使用全局配置
280
+
281
+ **可覆盖字段**:
282
+
283
+ | 字段 | 说明 |
284
+ |------|------|
285
+ | `knowledgebaseIds` | `/search/memory` 使用的知识库 ID 列表 |
286
+ | `memoryLimitNumber` | 召回的事实记忆最大条数 |
287
+ | `preferenceLimitNumber` | 召回的偏好记忆最大条数 |
288
+ | `includePreference` | 是否启用偏好记忆召回 |
289
+ | `includeToolMemory` | 是否启用工具记忆召回 |
290
+ | `toolMemoryLimitNumber` | 工具记忆最大条数 |
291
+ | `relativity` | 相关性阈值(0-1) |
292
+ | `recallEnabled` | 该 Agent 是否启用记忆检索 |
293
+ | `addEnabled` | 该 Agent 是否启用记忆写入 |
294
+ | `captureStrategy` | `last_turn` 或 `full_session` |
295
+ | `queryPrefix` | 搜索查询前缀 |
296
+ | `maxItemChars` | 注入 prompt 时每条记忆的最大字符数 |
297
+ | `maxMessageChars` | 写入记忆时每条消息的最大字符数 |
298
+ | `includeAssistant` | 写入记忆时是否包含助手回复 |
299
+ | `recallGlobal` | 全局召回(不传 conversation_id) |
300
+ | `recallFilterEnabled` | 是否启用模型二次过滤 |
301
+ | `recallFilterModel` | 过滤模型名 |
302
+ | `recallFilterBaseUrl` | 过滤模型接口地址 |
303
+ | `recallFilterApiKey` | 过滤模型鉴权密钥 |
304
+ | `allowKnowledgebaseIds` | `/add/message` 允许写入的知识库 |
305
+ | `tags` | `/add/message` 标签 |
306
+ | `throttleMs` | 请求节流间隔 |
307
+
308
+ ## 私聊 Session User ID(Direct Session User ID)
309
+ - **默认行为**:仍然使用配置里的 `userId`(或 `MEMOS_USER_ID`),完全兼容旧行为。
310
+ - **开启方式**:在插件 config 中设置 `"useDirectSessionUserId": true`,或在环境变量中设置 `MEMOS_USE_DIRECT_SESSION_USER_ID=true`。
311
+ - **行为说明**:开启后,像 `agent:main:<provider>:direct:<peer-id>` 这样的私聊 sessionKey,会把 `<peer-id>` 当作 MemOS `user_id`。
312
+ - **不会影响的场景**:像 `agent:main:<provider>:channel:<channel-id>` 这类非私聊 sessionKey,仍继续使用配置好的 fallback `userId`。
313
+ - **作用范围**:同一套解析逻辑同时作用于 `buildSearchPayload()` 和 `buildAddMessagePayload()`,保证 recall 与 add 一致。
314
+ - **配置优先级**:仍遵循插件现有规则——插件 config 优先,其次是 `.env` 文件(`~/.openclaw/.env` -> `~/.moltbot/.env` -> `~/.clawdbot/.env`),最后才回退到进程环境变量。
315
+
316
+
182
317
  ## 说明
183
318
  - 未显式指定 `conversation_id` 时,默认使用 OpenClaw `sessionKey`。**TODO**:后续考虑直接绑定 OpenClaw `sessionId`。
184
319
  - 可配置前后缀;`conversationSuffixMode=counter` 时会在 `/new` 递增(需 `hooks.internal.enabled`)。
@@ -2,7 +2,7 @@
2
2
  "id": "memos-cloud-openclaw-plugin",
3
3
  "name": "MemOS Cloud OpenClaw Plugin",
4
4
  "description": "MemOS Cloud recall + add memory via lifecycle hooks",
5
- "version": "0.1.10",
5
+ "version": "0.1.11-beta.0",
6
6
  "kind": "lifecycle",
7
7
  "main": "./index.js",
8
8
  "configSchema": {
@@ -41,6 +41,11 @@
41
41
  ],
42
42
  "default": "none"
43
43
  },
44
+ "useDirectSessionUserId": {
45
+ "type": "boolean",
46
+ "description": "When enabled, direct-session keys like agent:main:<provider>:direct:<id> use the direct id as MemOS user_id instead of the default configured userId.",
47
+ "default": false
48
+ },
44
49
  "resetOnNew": {
45
50
  "type": "boolean",
46
51
  "default": true
@@ -174,6 +179,13 @@
174
179
  "type": "boolean",
175
180
  "default": false
176
181
  },
182
+ "allowedAgents": {
183
+ "type": "array",
184
+ "items": {
185
+ "type": "string"
186
+ },
187
+ "description": "When multiAgentMode is true, only these agent IDs will activate the memory plugin. Comma-separated in env var MEMOS_ALLOWED_AGENTS. Empty list means all agents are allowed."
188
+ },
177
189
  "appId": {
178
190
  "type": "string"
179
191
  },
@@ -202,6 +214,108 @@
202
214
  "throttleMs": {
203
215
  "type": "integer",
204
216
  "default": 0
217
+ },
218
+ "agentOverrides": {
219
+ "type": "object",
220
+ "description": "Per-agent config overrides. Keys are agent IDs, values override global defaults for that agent.",
221
+ "additionalProperties": {
222
+ "type": "object",
223
+ "properties": {
224
+ "knowledgebaseIds": {
225
+ "type": "array",
226
+ "items": {
227
+ "type": "string"
228
+ }
229
+ },
230
+ "memoryLimitNumber": {
231
+ "type": "integer"
232
+ },
233
+ "preferenceLimitNumber": {
234
+ "type": "integer"
235
+ },
236
+ "includePreference": {
237
+ "type": "boolean"
238
+ },
239
+ "includeToolMemory": {
240
+ "type": "boolean"
241
+ },
242
+ "toolMemoryLimitNumber": {
243
+ "type": "integer"
244
+ },
245
+ "includeSkill": {
246
+ "type": "boolean"
247
+ },
248
+ "skillLimitNumber": {
249
+ "type": "integer"
250
+ },
251
+ "relativity": {
252
+ "type": "number"
253
+ },
254
+ "filter": {
255
+ "type": "object",
256
+ "additionalProperties": true
257
+ },
258
+ "recallEnabled": {
259
+ "type": "boolean"
260
+ },
261
+ "addEnabled": {
262
+ "type": "boolean"
263
+ },
264
+ "captureStrategy": {
265
+ "type": "string",
266
+ "enum": [
267
+ "last_turn",
268
+ "full_session"
269
+ ]
270
+ },
271
+ "queryPrefix": {
272
+ "type": "string"
273
+ },
274
+ "maxQueryChars": {
275
+ "type": "integer"
276
+ },
277
+ "maxItemChars": {
278
+ "type": "integer"
279
+ },
280
+ "maxMessageChars": {
281
+ "type": "integer"
282
+ },
283
+ "includeAssistant": {
284
+ "type": "boolean"
285
+ },
286
+ "recallGlobal": {
287
+ "type": "boolean"
288
+ },
289
+ "recallFilterEnabled": {
290
+ "type": "boolean"
291
+ },
292
+ "recallFilterModel": {
293
+ "type": "string"
294
+ },
295
+ "recallFilterBaseUrl": {
296
+ "type": "string"
297
+ },
298
+ "recallFilterApiKey": {
299
+ "type": "string"
300
+ },
301
+ "allowKnowledgebaseIds": {
302
+ "type": "array",
303
+ "items": {
304
+ "type": "string"
305
+ }
306
+ },
307
+ "tags": {
308
+ "type": "array",
309
+ "items": {
310
+ "type": "string"
311
+ }
312
+ },
313
+ "throttleMs": {
314
+ "type": "integer"
315
+ }
316
+ },
317
+ "additionalProperties": false
318
+ }
205
319
  }
206
320
  },
207
321
  "additionalProperties": false
package/index.js CHANGED
@@ -5,6 +5,8 @@ import {
5
5
  extractResultData,
6
6
  extractText,
7
7
  formatRecallHookResult,
8
+ isAgentAllowed,
9
+ resolveAgentConfig,
8
10
  searchMemory,
9
11
  stripOpenClawInjectedPrefix,
10
12
  } from "./lib/memos-cloud-api.js";
@@ -53,6 +55,21 @@ function getEffectiveAgentId(cfg, ctx) {
53
55
  return agentId === "main" ? undefined : agentId;
54
56
  }
55
57
 
58
+ export function extractDirectSessionUserId(sessionKey) {
59
+ if (!sessionKey || typeof sessionKey !== "string") return "";
60
+ const parts = sessionKey.split(":");
61
+ const directIndex = parts.lastIndexOf("direct");
62
+ if (directIndex === -1) return "";
63
+ return parts[directIndex + 1] || "";
64
+ }
65
+
66
+ export function resolveMemosUserId(cfg, ctx) {
67
+ const fallback = cfg?.userId || "openclaw-user";
68
+ if (!cfg?.useDirectSessionUserId) return fallback;
69
+ const directUserId = extractDirectSessionUserId(ctx?.sessionKey);
70
+ return directUserId || fallback;
71
+ }
72
+
56
73
  function resolveConversationId(cfg, ctx) {
57
74
  if (cfg.conversationId) return cfg.conversationId;
58
75
  // TODO: consider binding conversation_id directly to OpenClaw sessionId (prefer ctx.sessionId).
@@ -65,7 +82,7 @@ function resolveConversationId(cfg, ctx) {
65
82
  return `${prefix}openclaw-${Date.now()}${dynamicSuffix}${suffix}`;
66
83
  }
67
84
 
68
- function buildSearchPayload(cfg, prompt, ctx) {
85
+ export function buildSearchPayload(cfg, prompt, ctx) {
69
86
  const cleanPrompt = stripOpenClawInjectedPrefix(prompt);
70
87
  const queryRaw = `${cfg.queryPrefix || ""}${cleanPrompt}`;
71
88
  const query =
@@ -74,7 +91,7 @@ function buildSearchPayload(cfg, prompt, ctx) {
74
91
  : queryRaw;
75
92
 
76
93
  const payload = {
77
- user_id: cfg.userId,
94
+ user_id: resolveMemosUserId(cfg, ctx),
78
95
  query,
79
96
  source: MEMOS_SOURCE,
80
97
  };
@@ -113,9 +130,9 @@ function buildSearchPayload(cfg, prompt, ctx) {
113
130
  return payload;
114
131
  }
115
132
 
116
- function buildAddMessagePayload(cfg, messages, ctx) {
133
+ export function buildAddMessagePayload(cfg, messages, ctx) {
117
134
  const payload = {
118
- user_id: cfg.userId,
135
+ user_id: resolveMemosUserId(cfg, ctx),
119
136
  conversation_id: resolveConversationId(cfg, ctx),
120
137
  messages,
121
138
  source: MEMOS_SOURCE,
@@ -413,6 +430,15 @@ export default {
413
430
  log.warn?.(`[memos-cloud] No .env found in ${searchPaths}; falling back to process env or plugin config.`);
414
431
  }
415
432
 
433
+ if (cfg.multiAgentMode && cfg.allowedAgents?.length > 0) {
434
+ log.info?.(`[memos-cloud] Multi-agent mode enabled. Allowed agents: [${cfg.allowedAgents.join(", ")}]`);
435
+ }
436
+
437
+ const overrideAgentIds = Object.keys(cfg._agentOverrides || {});
438
+ if (overrideAgentIds.length > 0) {
439
+ log.info?.(`[memos-cloud] Per-agent overrides configured for: [${overrideAgentIds.join(", ")}]`);
440
+ }
441
+
416
442
  if (cfg.conversationSuffixMode === "counter" && cfg.resetOnNew) {
417
443
  if (api.config?.hooks?.internal?.enabled !== true) {
418
444
  log.warn?.("[memos-cloud] command:new hook requires hooks.internal.enabled = true");
@@ -432,24 +458,29 @@ export default {
432
458
  }
433
459
 
434
460
  api.on("before_agent_start", async (event, ctx) => {
435
- if (!cfg.recallEnabled) return;
461
+ if (!isAgentAllowed(cfg, ctx)) {
462
+ log.info?.(`[memos-cloud] recall skipped: agent "${ctx?.agentId}" not in allowedAgents [${cfg.allowedAgents?.join(", ")}]`);
463
+ return;
464
+ }
465
+ const agentCfg = resolveAgentConfig(cfg, ctx?.agentId);
466
+ if (!agentCfg.recallEnabled) return;
436
467
  const userPrompt = stripOpenClawInjectedPrefix(event?.prompt || "");
437
468
  if (!userPrompt || userPrompt.length < 3) return;
438
- if (!cfg.apiKey) {
469
+ if (!agentCfg.apiKey) {
439
470
  warnMissingApiKey(log, "recall");
440
471
  return;
441
472
  }
442
473
 
443
474
  try {
444
- const payload = buildSearchPayload(cfg, userPrompt, ctx);
445
- const result = await searchMemory(cfg, payload);
475
+ const payload = buildSearchPayload(agentCfg, userPrompt, ctx);
476
+ const result = await searchMemory(agentCfg, payload);
446
477
  const resultData = extractResultData(result);
447
478
  if (!resultData) return;
448
- const filteredData = await maybeFilterRecallData(cfg, resultData, userPrompt, log);
479
+ const filteredData = await maybeFilterRecallData(agentCfg, resultData, userPrompt, log);
449
480
  const hookResult = formatRecallHookResult({ data: filteredData }, {
450
481
  wrapTagBlocks: true,
451
482
  relativity: payload.relativity,
452
- maxItemChars: cfg.maxItemChars,
483
+ maxItemChars: agentCfg.maxItemChars,
453
484
  });
454
485
  if (!hookResult.appendSystemContext && !hookResult.prependContext) return;
455
486
 
@@ -460,29 +491,34 @@ export default {
460
491
  });
461
492
 
462
493
  api.on("agent_end", async (event, ctx) => {
463
- if (!cfg.addEnabled) return;
494
+ if (!isAgentAllowed(cfg, ctx)) {
495
+ log.info?.(`[memos-cloud] add skipped: agent "${ctx?.agentId}" not in allowedAgents [${cfg.allowedAgents?.join(", ")}]`);
496
+ return;
497
+ }
498
+ const agentCfg = resolveAgentConfig(cfg, ctx?.agentId);
499
+ if (!agentCfg.addEnabled) return;
464
500
  if (!event?.success || !event?.messages?.length) return;
465
- if (!cfg.apiKey) {
501
+ if (!agentCfg.apiKey) {
466
502
  warnMissingApiKey(log, "add");
467
503
  return;
468
504
  }
469
505
 
470
506
  const now = Date.now();
471
- if (cfg.throttleMs && now - lastCaptureTime < cfg.throttleMs) {
507
+ if (agentCfg.throttleMs && now - lastCaptureTime < agentCfg.throttleMs) {
472
508
  return;
473
509
  }
474
510
  lastCaptureTime = now;
475
511
 
476
512
  try {
477
513
  const messages =
478
- cfg.captureStrategy === "full_session"
479
- ? pickFullSessionMessages(event.messages, cfg)
480
- : pickLastTurnMessages(event.messages, cfg);
514
+ agentCfg.captureStrategy === "full_session"
515
+ ? pickFullSessionMessages(event.messages, agentCfg)
516
+ : pickLastTurnMessages(event.messages, agentCfg);
481
517
 
482
518
  if (!messages.length) return;
483
519
 
484
- const payload = buildAddMessagePayload(cfg, messages, ctx);
485
- await addMessage(cfg, payload);
520
+ const payload = buildAddMessagePayload(agentCfg, messages, ctx);
521
+ await addMessage(agentCfg, payload);
486
522
  } catch (err) {
487
523
  log.warn?.(`[memos-cloud] add failed: ${String(err)}`);
488
524
  }
@@ -157,6 +157,28 @@ function parseNumber(value, fallback) {
157
157
  return Number.isFinite(n) ? n : fallback;
158
158
  }
159
159
 
160
+ function parseStringArray(value) {
161
+ if (!value) return [];
162
+ if (Array.isArray(value)) return value.map((v) => String(v).trim()).filter(Boolean);
163
+ return String(value)
164
+ .split(",")
165
+ .map((s) => s.trim().replace(/^["']|["']$/g, ""))
166
+ .filter(Boolean);
167
+ }
168
+
169
+ function parseJsonObject(value) {
170
+ if (!value || typeof value !== "string") return null;
171
+ try {
172
+ const parsed = JSON.parse(value);
173
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
174
+ return parsed;
175
+ }
176
+ } catch {
177
+ // ignore parse error
178
+ }
179
+ return null;
180
+ }
181
+
160
182
  export function buildConfig(pluginConfig = {}) {
161
183
  const cfg = pluginConfig ?? {};
162
184
 
@@ -184,6 +206,10 @@ export function buildConfig(pluginConfig = {}) {
184
206
  parseBool(loadEnvVar("MEMOS_MULTI_AGENT_MODE"), false),
185
207
  );
186
208
 
209
+ const allowedAgents = parseStringArray(
210
+ cfg.allowedAgents ?? loadEnvVar("MEMOS_ALLOWED_AGENTS"),
211
+ );
212
+
187
213
  const recallFilterEnabled = parseBool(
188
214
  cfg.recallFilterEnabled,
189
215
  parseBool(loadEnvVar("MEMOS_RECALL_FILTER_ENABLED"), false),
@@ -200,6 +226,10 @@ export function buildConfig(pluginConfig = {}) {
200
226
  ? parseBool(loadEnvVar("MEMOS_INCLUDE_ASSISTANT"), true)
201
227
  : cfg.includeAssistant !== false;
202
228
  const maxMessageChars = cfg.maxMessageChars ?? parseNumber(loadEnvVar("MEMOS_MAX_MESSAGE_CHARS"), 20000);
229
+ const useDirectSessionUserId = parseBool(
230
+ cfg.useDirectSessionUserId,
231
+ parseBool(loadEnvVar("MEMOS_USE_DIRECT_SESSION_USER_ID"), false),
232
+ );
203
233
 
204
234
  return {
205
235
  baseUrl: baseUrl.replace(/\/+$/, ""),
@@ -209,6 +239,7 @@ export function buildConfig(pluginConfig = {}) {
209
239
  conversationIdPrefix,
210
240
  conversationIdSuffix,
211
241
  conversationSuffixMode,
242
+ useDirectSessionUserId,
212
243
  recallGlobal,
213
244
  resetOnNew,
214
245
  envFileStatus: getEnvFileStatus(),
@@ -230,15 +261,16 @@ export function buildConfig(pluginConfig = {}) {
230
261
  return v ? parseFloat(v) : 0.45;
231
262
  })()),
232
263
  filter: cfg.filter,
233
- knowledgebaseIds: cfg.knowledgebaseIds ?? [],
234
- tags: cfg.tags ?? ["openclaw"],
264
+ knowledgebaseIds: cfg.knowledgebaseIds ?? (loadEnvVar("MEMOS_KNOWLEDGEBASE_IDS") ? parseStringArray(loadEnvVar("MEMOS_KNOWLEDGEBASE_IDS")) : []),
265
+ tags: cfg.tags ?? (loadEnvVar("MEMOS_TAGS") ? parseStringArray(loadEnvVar("MEMOS_TAGS")) : ["openclaw"]),
235
266
  info: cfg.info ?? {},
236
267
  agentId: cfg.agentId,
237
268
  appId: cfg.appId,
238
269
  allowPublic: cfg.allowPublic ?? false,
239
- allowKnowledgebaseIds: cfg.allowKnowledgebaseIds ?? [],
270
+ allowKnowledgebaseIds: cfg.allowKnowledgebaseIds ?? (loadEnvVar("MEMOS_ALLOW_KNOWLEDGEBASE_IDS") ? parseStringArray(loadEnvVar("MEMOS_ALLOW_KNOWLEDGEBASE_IDS")) : []),
240
271
  asyncMode,
241
272
  multiAgentMode,
273
+ allowedAgents,
242
274
  recallFilterEnabled,
243
275
  recallFilterBaseUrl:
244
276
  (cfg.recallFilterBaseUrl ?? loadEnvVar("MEMOS_RECALL_FILTER_BASE_URL") ?? "").replace(/\/+$/, ""),
@@ -257,9 +289,35 @@ export function buildConfig(pluginConfig = {}) {
257
289
  timeoutMs: cfg.timeoutMs ?? 5000,
258
290
  retries: cfg.retries ?? 1,
259
291
  throttleMs,
292
+ _agentOverrides: cfg.agentOverrides ?? parseJsonObject(loadEnvVar("MEMOS_AGENT_OVERRIDES")) ?? {},
260
293
  };
261
294
  }
262
295
 
296
+ const AGENT_OVERRIDABLE_KEYS = [
297
+ "knowledgebaseIds", "memoryLimitNumber", "preferenceLimitNumber",
298
+ "includePreference", "includeToolMemory", "toolMemoryLimitNumber",
299
+ "relativity",
300
+ "recallEnabled", "addEnabled", "captureStrategy", "queryPrefix",
301
+ "maxItemChars", "maxMessageChars", "includeAssistant",
302
+ "recallGlobal", "recallFilterEnabled", "recallFilterModel",
303
+ "recallFilterBaseUrl", "recallFilterApiKey",
304
+ "allowKnowledgebaseIds", "tags", "throttleMs",
305
+ ];
306
+
307
+ export function resolveAgentConfig(baseCfg, agentId) {
308
+ if (!agentId || !baseCfg._agentOverrides) return baseCfg;
309
+ const overrides = baseCfg._agentOverrides[agentId];
310
+ if (!overrides || typeof overrides !== "object") return baseCfg;
311
+
312
+ const merged = { ...baseCfg };
313
+ for (const key of AGENT_OVERRIDABLE_KEYS) {
314
+ if (key in overrides) {
315
+ merged[key] = overrides[key];
316
+ }
317
+ }
318
+ return merged;
319
+ }
320
+
263
321
  export async function callApi({ baseUrl, apiKey, timeoutMs = 5000, retries = 1 }, path, body) {
264
322
  if (!apiKey) {
265
323
  throw new Error("Missing MEMOS API key (Token auth)");
@@ -317,6 +375,13 @@ function sanitizeAddMessageEntry(entry) {
317
375
  return { ...entry, content };
318
376
  }
319
377
 
378
+ export function isAgentAllowed(cfg, ctx) {
379
+ if (!cfg.multiAgentMode) return true;
380
+ if (!cfg.allowedAgents || cfg.allowedAgents.length === 0) return true;
381
+ const agentId = ctx?.agentId || cfg.agentId || "main";
382
+ return cfg.allowedAgents.includes(agentId);
383
+ }
384
+
320
385
  export async function searchMemory(cfg, payload) {
321
386
  return callApi(cfg, "/search/memory", sanitizeSearchPayload(payload));
322
387
  }
@@ -2,7 +2,7 @@
2
2
  "id": "memos-cloud-openclaw-plugin",
3
3
  "name": "MemOS Cloud OpenClaw Plugin",
4
4
  "description": "MemOS Cloud recall + add memory via lifecycle hooks",
5
- "version": "0.1.10",
5
+ "version": "0.1.11-beta.0",
6
6
  "kind": "lifecycle",
7
7
  "main": "./index.js",
8
8
  "configSchema": {
@@ -41,6 +41,11 @@
41
41
  ],
42
42
  "default": "none"
43
43
  },
44
+ "useDirectSessionUserId": {
45
+ "type": "boolean",
46
+ "description": "When enabled, direct-session keys like agent:main:<provider>:direct:<id> use the direct id as MemOS user_id instead of the default configured userId.",
47
+ "default": false
48
+ },
44
49
  "resetOnNew": {
45
50
  "type": "boolean",
46
51
  "default": true
@@ -174,6 +179,13 @@
174
179
  "type": "boolean",
175
180
  "default": false
176
181
  },
182
+ "allowedAgents": {
183
+ "type": "array",
184
+ "items": {
185
+ "type": "string"
186
+ },
187
+ "description": "When multiAgentMode is true, only these agent IDs will activate the memory plugin. Comma-separated in env var MEMOS_ALLOWED_AGENTS. Empty list means all agents are allowed."
188
+ },
177
189
  "appId": {
178
190
  "type": "string"
179
191
  },
@@ -202,6 +214,108 @@
202
214
  "throttleMs": {
203
215
  "type": "integer",
204
216
  "default": 0
217
+ },
218
+ "agentOverrides": {
219
+ "type": "object",
220
+ "description": "Per-agent config overrides. Keys are agent IDs, values override global defaults for that agent.",
221
+ "additionalProperties": {
222
+ "type": "object",
223
+ "properties": {
224
+ "knowledgebaseIds": {
225
+ "type": "array",
226
+ "items": {
227
+ "type": "string"
228
+ }
229
+ },
230
+ "memoryLimitNumber": {
231
+ "type": "integer"
232
+ },
233
+ "preferenceLimitNumber": {
234
+ "type": "integer"
235
+ },
236
+ "includePreference": {
237
+ "type": "boolean"
238
+ },
239
+ "includeToolMemory": {
240
+ "type": "boolean"
241
+ },
242
+ "toolMemoryLimitNumber": {
243
+ "type": "integer"
244
+ },
245
+ "includeSkill": {
246
+ "type": "boolean"
247
+ },
248
+ "skillLimitNumber": {
249
+ "type": "integer"
250
+ },
251
+ "relativity": {
252
+ "type": "number"
253
+ },
254
+ "filter": {
255
+ "type": "object",
256
+ "additionalProperties": true
257
+ },
258
+ "recallEnabled": {
259
+ "type": "boolean"
260
+ },
261
+ "addEnabled": {
262
+ "type": "boolean"
263
+ },
264
+ "captureStrategy": {
265
+ "type": "string",
266
+ "enum": [
267
+ "last_turn",
268
+ "full_session"
269
+ ]
270
+ },
271
+ "queryPrefix": {
272
+ "type": "string"
273
+ },
274
+ "maxQueryChars": {
275
+ "type": "integer"
276
+ },
277
+ "maxItemChars": {
278
+ "type": "integer"
279
+ },
280
+ "maxMessageChars": {
281
+ "type": "integer"
282
+ },
283
+ "includeAssistant": {
284
+ "type": "boolean"
285
+ },
286
+ "recallGlobal": {
287
+ "type": "boolean"
288
+ },
289
+ "recallFilterEnabled": {
290
+ "type": "boolean"
291
+ },
292
+ "recallFilterModel": {
293
+ "type": "string"
294
+ },
295
+ "recallFilterBaseUrl": {
296
+ "type": "string"
297
+ },
298
+ "recallFilterApiKey": {
299
+ "type": "string"
300
+ },
301
+ "allowKnowledgebaseIds": {
302
+ "type": "array",
303
+ "items": {
304
+ "type": "string"
305
+ }
306
+ },
307
+ "tags": {
308
+ "type": "array",
309
+ "items": {
310
+ "type": "string"
311
+ }
312
+ },
313
+ "throttleMs": {
314
+ "type": "integer"
315
+ }
316
+ },
317
+ "additionalProperties": false
318
+ }
205
319
  }
206
320
  },
207
321
  "additionalProperties": false
@@ -2,7 +2,7 @@
2
2
  "id": "memos-cloud-openclaw-plugin",
3
3
  "name": "MemOS Cloud OpenClaw Plugin",
4
4
  "description": "MemOS Cloud recall + add memory via lifecycle hooks",
5
- "version": "0.1.10",
5
+ "version": "0.1.11-beta.0",
6
6
  "kind": "lifecycle",
7
7
  "main": "./index.js",
8
8
  "configSchema": {
@@ -41,6 +41,11 @@
41
41
  ],
42
42
  "default": "none"
43
43
  },
44
+ "useDirectSessionUserId": {
45
+ "type": "boolean",
46
+ "description": "When enabled, direct-session keys like agent:main:<provider>:direct:<id> use the direct id as MemOS user_id instead of the default configured userId.",
47
+ "default": false
48
+ },
44
49
  "resetOnNew": {
45
50
  "type": "boolean",
46
51
  "default": true
@@ -174,6 +179,13 @@
174
179
  "type": "boolean",
175
180
  "default": false
176
181
  },
182
+ "allowedAgents": {
183
+ "type": "array",
184
+ "items": {
185
+ "type": "string"
186
+ },
187
+ "description": "When multiAgentMode is true, only these agent IDs will activate the memory plugin. Comma-separated in env var MEMOS_ALLOWED_AGENTS. Empty list means all agents are allowed."
188
+ },
177
189
  "appId": {
178
190
  "type": "string"
179
191
  },
@@ -202,6 +214,108 @@
202
214
  "throttleMs": {
203
215
  "type": "integer",
204
216
  "default": 0
217
+ },
218
+ "agentOverrides": {
219
+ "type": "object",
220
+ "description": "Per-agent config overrides. Keys are agent IDs, values override global defaults for that agent.",
221
+ "additionalProperties": {
222
+ "type": "object",
223
+ "properties": {
224
+ "knowledgebaseIds": {
225
+ "type": "array",
226
+ "items": {
227
+ "type": "string"
228
+ }
229
+ },
230
+ "memoryLimitNumber": {
231
+ "type": "integer"
232
+ },
233
+ "preferenceLimitNumber": {
234
+ "type": "integer"
235
+ },
236
+ "includePreference": {
237
+ "type": "boolean"
238
+ },
239
+ "includeToolMemory": {
240
+ "type": "boolean"
241
+ },
242
+ "toolMemoryLimitNumber": {
243
+ "type": "integer"
244
+ },
245
+ "includeSkill": {
246
+ "type": "boolean"
247
+ },
248
+ "skillLimitNumber": {
249
+ "type": "integer"
250
+ },
251
+ "relativity": {
252
+ "type": "number"
253
+ },
254
+ "filter": {
255
+ "type": "object",
256
+ "additionalProperties": true
257
+ },
258
+ "recallEnabled": {
259
+ "type": "boolean"
260
+ },
261
+ "addEnabled": {
262
+ "type": "boolean"
263
+ },
264
+ "captureStrategy": {
265
+ "type": "string",
266
+ "enum": [
267
+ "last_turn",
268
+ "full_session"
269
+ ]
270
+ },
271
+ "queryPrefix": {
272
+ "type": "string"
273
+ },
274
+ "maxQueryChars": {
275
+ "type": "integer"
276
+ },
277
+ "maxItemChars": {
278
+ "type": "integer"
279
+ },
280
+ "maxMessageChars": {
281
+ "type": "integer"
282
+ },
283
+ "includeAssistant": {
284
+ "type": "boolean"
285
+ },
286
+ "recallGlobal": {
287
+ "type": "boolean"
288
+ },
289
+ "recallFilterEnabled": {
290
+ "type": "boolean"
291
+ },
292
+ "recallFilterModel": {
293
+ "type": "string"
294
+ },
295
+ "recallFilterBaseUrl": {
296
+ "type": "string"
297
+ },
298
+ "recallFilterApiKey": {
299
+ "type": "string"
300
+ },
301
+ "allowKnowledgebaseIds": {
302
+ "type": "array",
303
+ "items": {
304
+ "type": "string"
305
+ }
306
+ },
307
+ "tags": {
308
+ "type": "array",
309
+ "items": {
310
+ "type": "string"
311
+ }
312
+ },
313
+ "throttleMs": {
314
+ "type": "integer"
315
+ }
316
+ },
317
+ "additionalProperties": false
318
+ }
205
319
  }
206
320
  },
207
321
  "additionalProperties": false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memtensor/memos-cloud-openclaw-plugin",
3
- "version": "0.1.10",
3
+ "version": "0.1.11-beta.0",
4
4
  "description": "OpenClaw lifecycle plugin for MemOS Cloud (add + recall memory)",
5
5
  "scripts": {
6
6
  "sync-version": "node scripts/sync-version.js",
@@ -0,0 +1,94 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ import { buildConfig } from "../lib/memos-cloud-api.js";
5
+ import {
6
+ buildAddMessagePayload,
7
+ buildSearchPayload,
8
+ extractDirectSessionUserId,
9
+ resolveMemosUserId,
10
+ } from "../index.js";
11
+
12
+ test("buildConfig keeps useDirectSessionUserId disabled by default", () => {
13
+ const previous = process.env.MEMOS_USE_DIRECT_SESSION_USER_ID;
14
+ delete process.env.MEMOS_USE_DIRECT_SESSION_USER_ID;
15
+ try {
16
+ const cfg = buildConfig({});
17
+ assert.equal(cfg.useDirectSessionUserId, false);
18
+ } finally {
19
+ if (previous === undefined) {
20
+ delete process.env.MEMOS_USE_DIRECT_SESSION_USER_ID;
21
+ } else {
22
+ process.env.MEMOS_USE_DIRECT_SESSION_USER_ID = previous;
23
+ }
24
+ }
25
+ });
26
+
27
+ test("extractDirectSessionUserId returns the id for direct session keys", () => {
28
+ assert.equal(
29
+ extractDirectSessionUserId("agent:main:discord:direct:1160853368999247882"),
30
+ "1160853368999247882",
31
+ );
32
+ assert.equal(extractDirectSessionUserId("agent:main:telegram:direct:8361983702"), "8361983702");
33
+ });
34
+
35
+ test("extractDirectSessionUserId ignores non-direct session keys", () => {
36
+ assert.equal(extractDirectSessionUserId("agent:main:discord:channel:1482035270651220051"), "");
37
+ assert.equal(extractDirectSessionUserId(""), "");
38
+ });
39
+
40
+ test("resolveMemosUserId falls back to configured userId when switch is off", () => {
41
+ const cfg = { userId: "openclaw-user", useDirectSessionUserId: false };
42
+ const ctx = { sessionKey: "agent:main:discord:direct:1160853368999247882" };
43
+ assert.equal(resolveMemosUserId(cfg, ctx), "openclaw-user");
44
+ });
45
+
46
+ test("resolveMemosUserId uses direct id when switch is on", () => {
47
+ const cfg = { userId: "openclaw-user", useDirectSessionUserId: true };
48
+ const ctx = { sessionKey: "agent:main:discord:direct:1160853368999247882" };
49
+ assert.equal(resolveMemosUserId(cfg, ctx), "1160853368999247882");
50
+ });
51
+
52
+ test("buildSearchPayload uses direct session id as user_id for private chats", () => {
53
+ const cfg = {
54
+ userId: "openclaw-user",
55
+ useDirectSessionUserId: true,
56
+ queryPrefix: "",
57
+ maxQueryChars: 0,
58
+ recallGlobal: true,
59
+ knowledgebaseIds: [],
60
+ memoryLimitNumber: 6,
61
+ includePreference: true,
62
+ preferenceLimitNumber: 6,
63
+ includeToolMemory: false,
64
+ toolMemoryLimitNumber: 0,
65
+ relativity: 0.45,
66
+ multiAgentMode: false,
67
+ };
68
+ const ctx = { sessionKey: "agent:main:discord:direct:1160853368999247882" };
69
+
70
+ const payload = buildSearchPayload(cfg, "你好", ctx);
71
+ assert.equal(payload.user_id, "1160853368999247882");
72
+ });
73
+
74
+ test("buildAddMessagePayload keeps configured userId for non-direct chats", () => {
75
+ const cfg = {
76
+ userId: "openclaw-user",
77
+ useDirectSessionUserId: true,
78
+ multiAgentMode: false,
79
+ appId: "",
80
+ tags: [],
81
+ info: {},
82
+ allowPublic: false,
83
+ allowKnowledgebaseIds: [],
84
+ asyncMode: true,
85
+ conversationId: "",
86
+ conversationIdPrefix: "",
87
+ conversationIdSuffix: "",
88
+ conversationSuffixMode: "none",
89
+ };
90
+ const ctx = { sessionKey: "agent:main:discord:channel:1482035270651220051" };
91
+
92
+ const payload = buildAddMessagePayload(cfg, [{ role: "user", content: "hi" }], ctx);
93
+ assert.equal(payload.user_id, "openclaw-user");
94
+ });