@humanlikememory/human-like-mem 0.3.12 → 0.3.14

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
@@ -1,70 +1,70 @@
1
- # Human-Like Memory Plugin for OpenClaw
2
-
3
- [![npm version](https://img.shields.io/npm/v/@humanlikememory/human-like-mem.svg)](https://www.npmjs.com/package/@humanlikememory/human-like-mem)
4
-
5
- Long-term memory plugin for OpenClaw with automatic memory recall and memory storage.
6
-
7
- ## Install
8
-
9
- ```bash
10
- npm install @humanlikememory/human-like-mem
11
- ```
12
-
13
- ## Enable In OpenClaw
14
-
15
- Edit `~/.openclaw/openclaw.json`:
16
-
17
- ```json
18
- {
19
- "plugins": {
20
- "entries": {
21
- "human-like-mem": {
22
- "enabled": true
23
- }
24
- }
25
- }
26
- }
27
- ```
28
-
29
- ## Required Environment Variables
30
-
31
- ```bash
32
- export HUMAN_LIKE_MEM_API_KEY="mp_xxxxxx"
33
- ```
34
-
35
- ## Optional Environment Variables
36
-
37
- ```bash
38
- export HUMAN_LIKE_MEM_BASE_URL="https://human-like.me"
39
- export HUMAN_LIKE_MEM_USER_ID="your-user-id"
40
- export HUMAN_LIKE_MEM_AGENT_ID="main"
41
- export HUMAN_LIKE_MEM_LIMIT_NUMBER="6"
42
- export HUMAN_LIKE_MEM_MIN_SCORE="0.1"
43
- export HUMAN_LIKE_MEM_MIN_TURNS="5"
44
- export HUMAN_LIKE_MEM_SESSION_TIMEOUT="300000"
45
- ```
46
-
47
- ## How It Works
48
-
49
- - Before response: the plugin retrieves relevant memories and injects them into context.
50
- - After response: the plugin caches the conversation and flushes memory by turn threshold or timeout.
51
-
52
- Default storage behavior:
53
-
54
- - `minTurnsToStore = 5`
55
- - `maxTurnsToStore = minTurnsToStore * 2`
56
- - `sessionTimeoutMs = 300000` (5 minutes)
57
-
58
- ## Troubleshooting
59
-
60
- - If you see `HUMAN_LIKE_MEM_API_KEY not configured`, make sure the key is set in your runtime environment.
61
- - If you see request timeout logs, increase plugin `timeoutMs` (for example `30000`).
62
- - Check logs with:
63
-
64
- ```bash
65
- openclaw logs --plain --limit 200
66
- ```
67
-
68
- ## License
69
-
70
- Apache-2.0
1
+ # Human-Like Memory Plugin for OpenClaw
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@humanlikememory/human-like-mem.svg)](https://www.npmjs.com/package/@humanlikememory/human-like-mem)
4
+
5
+ Long-term memory plugin for OpenClaw with automatic memory recall and memory storage.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @humanlikememory/human-like-mem
11
+ ```
12
+
13
+ ## Enable In OpenClaw
14
+
15
+ Edit `~/.openclaw/openclaw.json`:
16
+
17
+ ```json
18
+ {
19
+ "plugins": {
20
+ "entries": {
21
+ "human-like-mem": {
22
+ "enabled": true
23
+ }
24
+ }
25
+ }
26
+ }
27
+ ```
28
+
29
+ ## Required Environment Variables
30
+
31
+ ```bash
32
+ export HUMAN_LIKE_MEM_API_KEY="mp_xxxxxx"
33
+ ```
34
+
35
+ ## Optional Environment Variables
36
+
37
+ ```bash
38
+ export HUMAN_LIKE_MEM_BASE_URL="https://plugin.human-like.me"
39
+ export HUMAN_LIKE_MEM_USER_ID="your-user-id"
40
+ export HUMAN_LIKE_MEM_AGENT_ID="main"
41
+ export HUMAN_LIKE_MEM_LIMIT_NUMBER="6"
42
+ export HUMAN_LIKE_MEM_MIN_SCORE="0.1"
43
+ export HUMAN_LIKE_MEM_MIN_TURNS="5"
44
+ export HUMAN_LIKE_MEM_SESSION_TIMEOUT="300000"
45
+ ```
46
+
47
+ ## How It Works
48
+
49
+ - Before response: the plugin retrieves relevant memories and injects them into context.
50
+ - After response: the plugin caches the conversation and flushes memory by turn threshold or timeout.
51
+
52
+ Default storage behavior:
53
+
54
+ - `minTurnsToStore = 5`
55
+ - `maxTurnsToStore = minTurnsToStore * 2`
56
+ - `sessionTimeoutMs = 300000` (5 minutes)
57
+
58
+ ## Troubleshooting
59
+
60
+ - If you see `HUMAN_LIKE_MEM_API_KEY not configured`, make sure the key is set in your runtime environment.
61
+ - If you see request timeout logs, increase plugin `timeoutMs` (for example `30000`).
62
+ - Check logs with:
63
+
64
+ ```bash
65
+ openclaw logs --plain --limit 200
66
+ ```
67
+
68
+ ## License
69
+
70
+ Apache-2.0
package/README_ZH.md CHANGED
@@ -1,70 +1,70 @@
1
- # Human-Like Memory Plugin for OpenClaw
2
-
3
- [![npm version](https://img.shields.io/npm/v/@humanlikememory/human-like-mem.svg)](https://www.npmjs.com/package/@humanlikememory/human-like-mem)
4
-
5
- OpenClaw 长期记忆插件,支持自动召回和自动存储对话记忆。
6
-
7
- ## 安装
8
-
9
- ```bash
10
- npm install @humanlikememory/human-like-mem
11
- ```
12
-
13
- ## 在 OpenClaw 中启用
14
-
15
- 编辑 `~/.openclaw/openclaw.json`:
16
-
17
- ```json
18
- {
19
- "plugins": {
20
- "entries": {
21
- "human-like-mem": {
22
- "enabled": true
23
- }
24
- }
25
- }
26
- }
27
- ```
28
-
29
- ## 必填环境变量
30
-
31
- ```bash
32
- export HUMAN_LIKE_MEM_API_KEY="mp_xxxxxx"
33
- ```
34
-
35
- ## 可选环境变量
36
-
37
- ```bash
38
- export HUMAN_LIKE_MEM_BASE_URL="https://human-like.me"
39
- export HUMAN_LIKE_MEM_USER_ID="your-user-id"
40
- export HUMAN_LIKE_MEM_AGENT_ID="main"
41
- export HUMAN_LIKE_MEM_LIMIT_NUMBER="6"
42
- export HUMAN_LIKE_MEM_MIN_SCORE="0.1"
43
- export HUMAN_LIKE_MEM_MIN_TURNS="5"
44
- export HUMAN_LIKE_MEM_SESSION_TIMEOUT="300000"
45
- ```
46
-
47
- ## 工作机制
48
-
49
- - 回答前:插件检索相关记忆并注入上下文。
50
- - 回答后:插件缓存会话,在满足轮次阈值或超时后写入记忆。
51
-
52
- 默认存储策略:
53
-
54
- - `minTurnsToStore = 5`
55
- - `maxTurnsToStore = minTurnsToStore * 2`
56
- - `sessionTimeoutMs = 300000`(5 分钟)
57
-
58
- ## 常见问题
59
-
60
- - 如果日志出现 `HUMAN_LIKE_MEM_API_KEY not configured`,请确认运行时环境变量已生效。
61
- - 如果日志出现请求超时,请把插件 `timeoutMs` 调大(例如 `30000`)。
62
- - 查看日志:
63
-
64
- ```bash
65
- openclaw logs --plain --limit 200
66
- ```
67
-
68
- ## 许可证
69
-
70
- Apache-2.0
1
+ # Human-Like Memory Plugin for OpenClaw
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@humanlikememory/human-like-mem.svg)](https://www.npmjs.com/package/@humanlikememory/human-like-mem)
4
+
5
+ OpenClaw 长期记忆插件,支持自动召回和自动存储对话记忆。
6
+
7
+ ## 安装
8
+
9
+ ```bash
10
+ npm install @humanlikememory/human-like-mem
11
+ ```
12
+
13
+ ## 在 OpenClaw 中启用
14
+
15
+ 编辑 `~/.openclaw/openclaw.json`:
16
+
17
+ ```json
18
+ {
19
+ "plugins": {
20
+ "entries": {
21
+ "human-like-mem": {
22
+ "enabled": true
23
+ }
24
+ }
25
+ }
26
+ }
27
+ ```
28
+
29
+ ## 必填环境变量
30
+
31
+ ```bash
32
+ export HUMAN_LIKE_MEM_API_KEY="mp_xxxxxx"
33
+ ```
34
+
35
+ ## 可选环境变量
36
+
37
+ ```bash
38
+ export HUMAN_LIKE_MEM_BASE_URL="https://plugin.human-like.me"
39
+ export HUMAN_LIKE_MEM_USER_ID="your-user-id"
40
+ export HUMAN_LIKE_MEM_AGENT_ID="main"
41
+ export HUMAN_LIKE_MEM_LIMIT_NUMBER="6"
42
+ export HUMAN_LIKE_MEM_MIN_SCORE="0.1"
43
+ export HUMAN_LIKE_MEM_MIN_TURNS="5"
44
+ export HUMAN_LIKE_MEM_SESSION_TIMEOUT="300000"
45
+ ```
46
+
47
+ ## 工作机制
48
+
49
+ - 回答前:插件检索相关记忆并注入上下文。
50
+ - 回答后:插件缓存会话,在满足轮次阈值或超时后写入记忆。
51
+
52
+ 默认存储策略:
53
+
54
+ - `minTurnsToStore = 5`
55
+ - `maxTurnsToStore = minTurnsToStore * 2`
56
+ - `sessionTimeoutMs = 300000`(5 分钟)
57
+
58
+ ## 常见问题
59
+
60
+ - 如果日志出现 `HUMAN_LIKE_MEM_API_KEY not configured`,请确认运行时环境变量已生效。
61
+ - 如果日志出现请求超时,请把插件 `timeoutMs` 调大(例如 `30000`)。
62
+ - 查看日志:
63
+
64
+ ```bash
65
+ openclaw logs --plain --limit 200
66
+ ```
67
+
68
+ ## 许可证
69
+
70
+ Apache-2.0
package/index.js CHANGED
@@ -6,9 +6,8 @@
6
6
  * @license Apache-2.0
7
7
  */
8
8
 
9
- const PLUGIN_VERSION = "0.3.4";
10
- const USER_QUERY_MARKER = "--- User Query ---";
11
- const CACHE_DEDUP_WINDOW_MS = 4000;
9
+ const PLUGIN_VERSION = "0.3.4";
10
+ const USER_QUERY_MARKER = "--- User Query ---";
12
11
 
13
12
  /**
14
13
  * Session cache for tracking conversation history
@@ -36,7 +35,7 @@ function warnMissingApiKey(log) {
36
35
  Get your API key from: https://human-like.me
37
36
  Then set:
38
37
  export HUMAN_LIKE_MEM_API_KEY="mp_xxxxxx"
39
- export HUMAN_LIKE_MEM_BASE_URL="https://human-like.me"
38
+ export HUMAN_LIKE_MEM_BASE_URL="https://plugin.human-like.me"
40
39
  `;
41
40
  if (log?.warn) {
42
41
  log.warn(msg);
@@ -115,101 +114,31 @@ function extractFeishuTailBlock(text) {
115
114
  /**
116
115
  * Whether text is only transport metadata rather than a real utterance.
117
116
  */
118
- function isMetadataOnlyText(text) {
119
- if (!text) return true;
120
- const value = String(text).trim();
121
- if (!value) return true;
122
- if (/^\[message_id:\s*[^\]]+\]$/i.test(value)) return true;
123
- if (/^\[\[reply_to[^\]]*\]\]$/i.test(value)) return true;
124
- return false;
125
- }
126
-
127
- /**
128
- * Remove injected relay metadata blocks/lines from user content.
129
- */
130
- function stripInjectedMetadata(text) {
131
- if (!text || typeof text !== "string") return "";
132
-
133
- let result = text.replace(/\r\n/g, "\n");
134
-
135
- // Remove labeled untrusted metadata JSON blocks.
136
- // Examples:
137
- // Conversation info (untrusted metadata): ```json ... ```
138
- // Sender (untrusted metadata): ```json ... ```
139
- result = result.replace(
140
- /(^|\n)[^\n]*\(untrusted metadata(?:,\s*for context)?\)\s*:\s*```json[\s\S]*?```(?=\n|$)/gi,
141
- "\n"
142
- );
143
-
144
- // Remove transport/system hint lines that are not user utterances.
145
- result = result.replace(/^\[System:[^\n]*\]\s*$/gim, "");
146
- result = result.replace(/^\[message_id:\s*[^\]]+\]\s*$/gim, "");
147
-
148
- // Collapse extra blank lines.
149
- result = result.replace(/\n{3,}/g, "\n\n").trim();
150
- return result;
151
- }
152
-
153
- /**
154
- * Keep the latest meaningful line from cleaned metadata-injected payload.
155
- */
156
- function extractLatestUtteranceFromCleanText(text) {
157
- if (!text || typeof text !== "string") return "";
158
- const lines = text
159
- .split("\n")
160
- .map((line) => line.trim())
161
- .filter(Boolean)
162
- .filter((line) => !isMetadataOnlyText(line));
163
- if (lines.length === 0) return "";
164
- return lines[lines.length - 1];
165
- }
166
-
167
- function escapeRegex(text) {
168
- return String(text).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
169
- }
170
-
171
- /**
172
- * Prefix utterance with parsed sender name when available.
173
- * This preserves who said it in multi-user group contexts.
174
- */
175
- function attachSenderPrefix(utterance, senderName) {
176
- const content = String(utterance || "").trim();
177
- if (!content) return "";
178
- const sender = String(senderName || "").trim();
179
- if (!sender) return content;
180
-
181
- const senderRegex = new RegExp(`^${escapeRegex(sender)}\\s*[::]`);
182
- if (senderRegex.test(content)) return content;
183
- return `${sender}: ${content}`;
184
- }
117
+ function isMetadataOnlyText(text) {
118
+ if (!text) return true;
119
+ const value = String(text).trim();
120
+ if (!value) return true;
121
+ if (/^\[message_id:\s*[^\]]+\]$/i.test(value)) return true;
122
+ if (/^\[\[reply_to[^\]]*\]\]$/i.test(value)) return true;
123
+ return false;
124
+ }
185
125
 
186
126
  /**
187
127
  * Normalize user message text before caching/storing.
188
128
  * For channel-formatted payloads (e.g. Feishu), keep only the actual user utterance
189
129
  * and drop prepended system transcript lines.
190
130
  */
191
- function normalizeUserMessageContent(content) {
192
- const text = stripPrependedPrompt(content);
193
- if (!text) return "";
194
-
195
- const normalized = String(text).replace(/\r\n/g, "\n").trim();
196
- if (!normalized) return "";
197
-
198
- const parsedIdentity = parseIdentityFromUntrustedMetadata(normalized);
199
-
200
- // New relay payload format: strip injected metadata blocks first.
201
- const strippedMetadata = stripInjectedMetadata(normalized);
202
- if (strippedMetadata && strippedMetadata !== normalized) {
203
- const latestUtterance = extractLatestUtteranceFromCleanText(strippedMetadata);
204
- if (latestUtterance && !isMetadataOnlyText(latestUtterance)) {
205
- return attachSenderPrefix(latestUtterance, parsedIdentity?.userName);
206
- }
207
- }
208
-
209
- // Feishu: preserve the final traceable tail block (contains platform user id/message_id).
210
- const feishuTailBlock = extractFeishuTailBlock(normalized);
211
- if (feishuTailBlock && !isMetadataOnlyText(feishuTailBlock)) {
212
- return feishuTailBlock;
131
+ function normalizeUserMessageContent(content) {
132
+ const text = stripPrependedPrompt(content);
133
+ if (!text) return "";
134
+
135
+ const normalized = String(text).replace(/\r\n/g, "\n").trim();
136
+ if (!normalized) return "";
137
+
138
+ // Feishu: preserve the final traceable tail block (contains platform user id/message_id).
139
+ const feishuTailBlock = extractFeishuTailBlock(normalized);
140
+ if (feishuTailBlock && !isMetadataOnlyText(feishuTailBlock)) {
141
+ return feishuTailBlock;
213
142
  }
214
143
 
215
144
  // Generic channel relays: fallback to latest "System: ...: <message>" line.
@@ -231,18 +160,16 @@ function normalizeUserMessageContent(content) {
231
160
 
232
161
  // Discord-style channel-formatted payload:
233
162
  // [from: username (id)] actual message
234
- const discordTail = normalized.match(
235
- /\[from:\s*[^\(\]\n]+?\s*\(\d{6,}\)\]\s*([\s\S]*?)$/i
236
- );
237
- if (discordTail && discordTail[1]) {
238
- const candidate = discordTail[1].trim();
239
- if (!isMetadataOnlyText(candidate)) return candidate;
240
- }
241
-
242
- return isMetadataOnlyText(normalized)
243
- ? ""
244
- : attachSenderPrefix(normalized, parsedIdentity?.userName);
245
- }
163
+ const discordTail = normalized.match(
164
+ /\[from:\s*[^\(\]\n]+?\s*\(\d{6,}\)\]\s*([\s\S]*?)$/i
165
+ );
166
+ if (discordTail && discordTail[1]) {
167
+ const candidate = discordTail[1].trim();
168
+ if (!isMetadataOnlyText(candidate)) return candidate;
169
+ }
170
+
171
+ return isMetadataOnlyText(normalized) ? "" : normalized;
172
+ }
246
173
 
247
174
  /**
248
175
  * Normalize assistant message text before caching/storing.
@@ -256,139 +183,12 @@ function normalizeAssistantMessageContent(content) {
256
183
  return normalized;
257
184
  }
258
185
 
259
- /**
260
- * Parse JSON objects from markdown code fences after a specific label.
261
- * Example:
262
- * Sender (untrusted metadata):
263
- * ```json
264
- * { "id": "123" }
265
- * ```
266
- */
267
- function parseJsonFencesAfterLabel(text, labelPattern) {
268
- if (!text || typeof text !== "string") return [];
269
-
270
- const results = [];
271
- const regex = new RegExp(`${labelPattern}\\s*:\\s*\`\`\`json\\s*([\\s\\S]*?)\\s*\`\`\``, "gi");
272
- let match;
273
- while ((match = regex.exec(text)) !== null) {
274
- if (!match[1]) continue;
275
- try {
276
- const parsed = JSON.parse(match[1]);
277
- if (parsed && typeof parsed === "object") {
278
- results.push(parsed);
279
- }
280
- } catch (_) {
281
- // Ignore malformed JSON blocks and continue matching others.
282
- }
283
- }
284
- return results;
285
- }
286
-
287
- /**
288
- * Normalize potential user id values into non-empty strings.
289
- */
290
- function normalizeParsedUserId(value) {
291
- if (value === undefined || value === null) return "";
292
- const normalized = String(value).trim();
293
- return normalized || "";
294
- }
295
-
296
- /**
297
- * Extract user id from a display label like:
298
- * "用户250389 (ou_xxx)" / "name (123456789)".
299
- */
300
- function extractUserIdFromLabel(label) {
301
- const text = normalizeParsedUserId(label);
302
- if (!text) return "";
303
- const match = text.match(/\(\s*((?:ou|on|u)_[A-Za-z0-9]+|\d{6,})\s*\)$/i);
304
- return match?.[1] ? match[1] : "";
305
- }
306
-
307
- /**
308
- * Extract transport message id from current relay payload.
309
- */
310
- function extractRelayMessageId(text) {
311
- if (!text || typeof text !== "string") return "";
312
-
313
- const conversationMetaList = parseJsonFencesAfterLabel(
314
- text,
315
- "Conversation info \\(untrusted metadata\\)"
316
- );
317
- for (let i = conversationMetaList.length - 1; i >= 0; i--) {
318
- const conversationMeta = conversationMetaList[i];
319
- const messageId =
320
- normalizeParsedUserId(conversationMeta?.message_id) ||
321
- normalizeParsedUserId(conversationMeta?.messageId);
322
- if (messageId) return messageId;
323
- }
324
-
325
- const messageIdMatch = text.match(/\[message_id:\s*([^\]\n]+)\]/i);
326
- if (messageIdMatch?.[1]) return messageIdMatch[1].trim();
327
-
328
- return "";
329
- }
330
-
331
- /**
332
- * Parse identity from new "untrusted metadata" JSON blocks.
333
- * Priority:
334
- * 1) Sender (untrusted metadata).id
335
- * 2) Conversation info (untrusted metadata).sender_id
336
- */
337
- function parseIdentityFromUntrustedMetadata(text) {
338
- if (!text || typeof text !== "string") return null;
339
-
340
- const senderMetaList = parseJsonFencesAfterLabel(text, "Sender \\(untrusted metadata\\)");
341
- for (let i = senderMetaList.length - 1; i >= 0; i--) {
342
- const senderMeta = senderMetaList[i];
343
- const userId =
344
- normalizeParsedUserId(senderMeta?.id) ||
345
- extractUserIdFromLabel(senderMeta?.label);
346
- if (userId) {
347
- return {
348
- platform: null,
349
- userId,
350
- userName: senderMeta?.name || senderMeta?.username || senderMeta?.label || null,
351
- source: "sender-untrusted-metadata-json",
352
- };
353
- }
354
- }
355
-
356
- const conversationMetaList = parseJsonFencesAfterLabel(
357
- text,
358
- "Conversation info \\(untrusted metadata\\)"
359
- );
360
- for (let i = conversationMetaList.length - 1; i >= 0; i--) {
361
- const conversationMeta = conversationMetaList[i];
362
- const userId =
363
- normalizeParsedUserId(conversationMeta?.sender_id) ||
364
- normalizeParsedUserId(conversationMeta?.senderId);
365
- if (userId) {
366
- return {
367
- platform: null,
368
- userId,
369
- userName: conversationMeta?.sender || null,
370
- source: "conversation-untrusted-metadata-json",
371
- };
372
- }
373
- }
374
-
375
- return null;
376
- }
377
-
378
- /**
379
- * Parse platform identity hints from channel-formatted message text.
380
- * Returns null when no platform-specific id can be extracted.
381
- */
382
- function parsePlatformIdentity(text) {
383
- if (!text || typeof text !== "string") return null;
384
-
385
- // New Discord format first:
386
- // Sender (untrusted metadata): ```json { "id": "116..." } ```
387
- // Conversation info (untrusted metadata): ```json { "sender_id": "116..." } ```
388
- const metadataIdentity = parseIdentityFromUntrustedMetadata(text);
389
- if (metadataIdentity?.userId) {
390
- return metadataIdentity;
391
- }
186
+ /**
187
+ * Parse platform identity hints from channel-formatted message text.
188
+ * Returns null when no platform-specific id can be extracted.
189
+ */
190
+ function parsePlatformIdentity(text) {
191
+ if (!text || typeof text !== "string") return null;
392
192
 
393
193
  // Discord example:
394
194
  // [from: huang yongqing (1470374017541079042)]
@@ -420,39 +220,18 @@ function parsePlatformIdentity(text) {
420
220
  /**
421
221
  * Parse all platform user ids from a text blob.
422
222
  */
423
- function parseAllPlatformUserIds(text) {
424
- if (!text || typeof text !== "string") return [];
425
-
426
- const ids = [];
427
- let match;
428
-
429
- // New format: "Sender (untrusted metadata)" JSON blocks.
430
- const senderMetaList = parseJsonFencesAfterLabel(text, "Sender \\(untrusted metadata\\)");
431
- for (const senderMeta of senderMetaList) {
432
- const parsedId =
433
- normalizeParsedUserId(senderMeta?.id) ||
434
- extractUserIdFromLabel(senderMeta?.label);
435
- if (parsedId) ids.push(parsedId);
436
- }
437
-
438
- // New format: "Conversation info (untrusted metadata)" JSON blocks.
439
- const conversationMetaList = parseJsonFencesAfterLabel(
440
- text,
441
- "Conversation info \\(untrusted metadata\\)"
442
- );
443
- for (const conversationMeta of conversationMetaList) {
444
- const parsedId =
445
- normalizeParsedUserId(conversationMeta?.sender_id) ||
446
- normalizeParsedUserId(conversationMeta?.senderId);
447
- if (parsedId) ids.push(parsedId);
448
- }
449
-
450
- // Discord example:
451
- // [from: huang yongqing (1470374017541079042)]
452
- const discordRegex = /\[from:\s*[^\(\]\n]+?\s*\((\d{6,})\)\]/gi;
453
- while ((match = discordRegex.exec(text)) !== null) {
454
- if (match[1]) ids.push(match[1]);
455
- }
223
+ function parseAllPlatformUserIds(text) {
224
+ if (!text || typeof text !== "string") return [];
225
+
226
+ const ids = [];
227
+
228
+ // Discord example:
229
+ // [from: huang yongqing (1470374017541079042)]
230
+ const discordRegex = /\[from:\s*[^\(\]\n]+?\s*\((\d{6,})\)\]/gi;
231
+ let match;
232
+ while ((match = discordRegex.exec(text)) !== null) {
233
+ if (match[1]) ids.push(match[1]);
234
+ }
456
235
 
457
236
  // Feishu example:
458
237
  // [Feishu ...:ou_17b624... Wed ...] username: message
@@ -467,8 +246,8 @@ function parseAllPlatformUserIds(text) {
467
246
  /**
468
247
  * Collect distinct user ids parsed from all messages.
469
248
  */
470
- function collectUniqueUserIdsFromMessages(messages, fallbackUserId) {
471
- const unique = new Set();
249
+ function collectUniqueUserIdsFromMessages(messages, fallbackUserId) {
250
+ const unique = new Set();
472
251
 
473
252
  if (Array.isArray(messages)) {
474
253
  for (const msg of messages) {
@@ -484,96 +263,12 @@ function collectUniqueUserIdsFromMessages(messages, fallbackUserId) {
484
263
  unique.add(fallbackUserId);
485
264
  }
486
265
 
487
- return Array.from(unique);
488
- }
489
-
490
- function extractUserNameFromLabel(label) {
491
- const text = normalizeParsedUserId(label);
492
- if (!text) return "";
493
- const match = text.match(
494
- /^(.*?)\s*\(\s*(?:(?:ou|on|u)_[A-Za-z0-9]+|\d{6,})\s*\)\s*$/i
495
- );
496
- return match?.[1] ? match[1].trim() : text;
497
- }
498
-
499
- /**
500
- * Parse name-id pairs from supported relay payload formats.
501
- */
502
- function parseNameIdPairsFromText(text) {
503
- if (!text || typeof text !== "string") return [];
504
- const pairs = [];
505
-
506
- // New format: Sender (untrusted metadata)
507
- const senderMetaList = parseJsonFencesAfterLabel(text, "Sender \\(untrusted metadata\\)");
508
- for (const senderMeta of senderMetaList) {
509
- const id =
510
- normalizeParsedUserId(senderMeta?.id) ||
511
- extractUserIdFromLabel(senderMeta?.label);
512
- const name =
513
- normalizeParsedUserId(senderMeta?.name) ||
514
- normalizeParsedUserId(senderMeta?.username) ||
515
- extractUserNameFromLabel(senderMeta?.label);
516
- if (name && id) pairs.push([name, id]);
517
- }
518
-
519
- // New format: Conversation info (untrusted metadata)
520
- const conversationMetaList = parseJsonFencesAfterLabel(
521
- text,
522
- "Conversation info \\(untrusted metadata\\)"
523
- );
524
- for (const conversationMeta of conversationMetaList) {
525
- const id =
526
- normalizeParsedUserId(conversationMeta?.sender_id) ||
527
- normalizeParsedUserId(conversationMeta?.senderId);
528
- const name = normalizeParsedUserId(conversationMeta?.sender);
529
- if (name && id) pairs.push([name, id]);
530
- }
531
-
532
- // Old Discord: [from: name (id)]
533
- const discordRegex = /\[from:\s*([^\(\]\n]+?)\s*\((\d{6,})\)\]/gi;
534
- let match;
535
- while ((match = discordRegex.exec(text)) !== null) {
536
- const name = normalizeParsedUserId(match[1]);
537
- const id = normalizeParsedUserId(match[2]);
538
- if (name && id) pairs.push([name, id]);
539
- }
540
-
541
- // Old Feishu: [Feishu ...:ou_xxx ...] name:
542
- const feishuRegex = /\[Feishu[^\]]*:((?:ou|on|u)_[A-Za-z0-9]+)[^\]]*\]\s*([^:\n]+):/gi;
543
- while ((match = feishuRegex.exec(text)) !== null) {
544
- const id = normalizeParsedUserId(match[1]);
545
- const name = normalizeParsedUserId(match[2]);
546
- if (name && id) pairs.push([name, id]);
547
- }
548
-
549
- return pairs;
550
- }
551
-
552
- /**
553
- * Collect distinct name->id mapping from all messages.
554
- */
555
- function collectNameIdMapFromMessages(messages) {
556
- const map = {};
557
-
558
- if (!Array.isArray(messages)) return map;
559
-
560
- for (const msg of messages) {
561
- if (!msg) continue;
562
- const sourceText = msg.rawContent !== undefined ? msg.rawContent : msg.content;
563
- const text = typeof sourceText === "string" ? sourceText : extractText(sourceText);
564
- const pairs = parseNameIdPairsFromText(text);
565
- for (const [name, id] of pairs) {
566
- if (!name || !id) continue;
567
- map[name] = id;
568
- }
569
- }
570
-
571
- return map;
572
- }
573
-
574
- /**
575
- * Get latest user message text from cached messages.
576
- */
266
+ return Array.from(unique);
267
+ }
268
+
269
+ /**
270
+ * Get latest user message text from cached messages.
271
+ */
577
272
  function getLatestUserMessageText(messages) {
578
273
  if (!Array.isArray(messages)) return "";
579
274
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -589,9 +284,9 @@ function getLatestUserMessageText(messages) {
589
284
  * Resolve request identity with fallback:
590
285
  * platform user id -> configured user id -> "openclaw-user"
591
286
  */
592
- function resolveRequestIdentity(promptText, cfg) {
593
- const parsed = parsePlatformIdentity(promptText);
594
- if (parsed?.userId) {
287
+ function resolveRequestIdentity(promptText, cfg, ctx) {
288
+ const parsed = parsePlatformIdentity(promptText);
289
+ if (parsed?.userId) {
595
290
  return {
596
291
  userId: parsed.userId,
597
292
  userName: parsed.userName || null,
@@ -600,7 +295,7 @@ function resolveRequestIdentity(promptText, cfg) {
600
295
  };
601
296
  }
602
297
 
603
- if (cfg?.configuredUserId) {
298
+ if (cfg?.configuredUserId) {
604
299
  return {
605
300
  userId: cfg.configuredUserId,
606
301
  userName: null,
@@ -609,8 +304,17 @@ function resolveRequestIdentity(promptText, cfg) {
609
304
  };
610
305
  }
611
306
 
612
- return {
613
- userId: "openclaw-user",
307
+ if (ctx?.userId) {
308
+ return {
309
+ userId: ctx.userId,
310
+ userName: null,
311
+ platform: null,
312
+ source: "ctx-user-id",
313
+ };
314
+ }
315
+
316
+ return {
317
+ userId: "openclaw-user",
614
318
  userName: null,
615
319
  platform: null,
616
320
  source: "default-user-id",
@@ -803,7 +507,7 @@ async function retrieveMemory(prompt, cfg, ctx, log) {
803
507
  if (log?.info) {
804
508
  log.info(`[Memory Plugin] Recall request URL: ${url}`);
805
509
  }
806
- const identity = resolveRequestIdentity(prompt, cfg);
510
+ const identity = resolveRequestIdentity(prompt, cfg, ctx);
807
511
  const userId = sanitizeUserId(identity.userId);
808
512
  const payload = {
809
513
  query: prompt,
@@ -862,25 +566,18 @@ async function addMemory(messages, cfg, ctx, log) {
862
566
  }
863
567
  const sessionId = resolveSessionId(ctx, null) || `session-${Date.now()}`;
864
568
  const latestUserText = getLatestUserMessageText(messages);
865
- const identity = resolveRequestIdentity(latestUserText, cfg);
569
+ const identity = resolveRequestIdentity(latestUserText, cfg, ctx);
866
570
  const userId = sanitizeUserId(identity.userId);
867
- const metadataUserIds = (() => {
868
- const parsed = collectUniqueUserIdsFromMessages(messages, null)
869
- .map((id) => sanitizeUserId(id))
870
- .filter(Boolean);
871
- if (parsed.length > 0) {
872
- return Array.from(new Set(parsed));
873
- }
874
- return [userId];
875
- })();
876
- const nameIdMap = (() => {
877
- const parsed = collectNameIdMapFromMessages(messages);
878
- if (identity?.userName && userId && !parsed[identity.userName]) {
879
- parsed[identity.userName] = userId;
880
- }
881
- return parsed;
882
- })();
883
- const agentId = cfg.agentId || ctx?.agentId || "main";
571
+ const metadataUserIds = (() => {
572
+ const parsed = collectUniqueUserIdsFromMessages(messages, null)
573
+ .map((id) => sanitizeUserId(id))
574
+ .filter(Boolean);
575
+ if (parsed.length > 0) {
576
+ return Array.from(new Set(parsed));
577
+ }
578
+ return [userId];
579
+ })();
580
+ const agentId = cfg.agentId || ctx?.agentId || "main";
884
581
 
885
582
  const payload = {
886
583
  user_id: userId,
@@ -894,22 +591,21 @@ async function addMemory(messages, cfg, ctx, log) {
894
591
  async_mode: true,
895
592
  custom_workflows: {
896
593
  stream_params: {
897
- metadata: JSON.stringify({
898
- user_ids: metadataUserIds,
899
- agent_ids: [agentId],
900
- name_id_map: nameIdMap,
901
- session_id: sessionId,
902
- scenario: cfg.scenario || "openclaw-plugin",
903
- }),
904
- },
905
- },
594
+ metadata: JSON.stringify({
595
+ user_ids: metadataUserIds,
596
+ agent_ids: [agentId],
597
+ session_id: sessionId,
598
+ scenario: cfg.scenario || "openclaw-plugin",
599
+ }),
600
+ },
601
+ },
906
602
  };
907
603
 
908
604
  if (log?.debug) {
909
- log.debug(
910
- `[Memory Plugin] add/message payload: user_id=${userId}, agent_id=${agentId}, conversation_id=${sessionId}, metadata.user_ids=${JSON.stringify(metadataUserIds)}, metadata.agent_ids=${JSON.stringify([agentId])}, metadata.name_id_map=${JSON.stringify(nameIdMap)}`
911
- );
912
- }
605
+ log.debug(
606
+ `[Memory Plugin] add/message payload: user_id=${userId}, agent_id=${agentId}, conversation_id=${sessionId}, metadata.user_ids=${JSON.stringify(metadataUserIds)}, metadata.agent_ids=${JSON.stringify([agentId])}`
607
+ );
608
+ }
913
609
 
914
610
  try {
915
611
  const result = await httpRequest(url, {
@@ -1154,44 +850,18 @@ function getSessionCache(sessionId) {
1154
850
  /**
1155
851
  * Add message to session cache
1156
852
  */
1157
- function addToSessionCache(sessionId, message) {
1158
- const cache = getSessionCache(sessionId);
1159
- const now = Date.now();
1160
- const incoming = {
1161
- ...message,
1162
- cachedAt: now,
1163
- };
1164
-
1165
- const prev = cache.messages.length > 0 ? cache.messages[cache.messages.length - 1] : null;
1166
- if (prev) {
1167
- const sameRole = prev.role === incoming.role;
1168
- const sameMessageId =
1169
- incoming.messageId &&
1170
- prev.messageId &&
1171
- String(incoming.messageId) === String(prev.messageId);
1172
- const sameContent =
1173
- sameRole &&
1174
- String(prev.content || "") === String(incoming.content || "") &&
1175
- String(prev.rawContent || "") === String(incoming.rawContent || "");
1176
- const withinWindow = now - (prev.cachedAt || 0) <= CACHE_DEDUP_WINDOW_MS;
1177
-
1178
- // Dedup duplicate hook triggers for the same relay message.
1179
- if ((sameRole && sameMessageId) || (sameContent && withinWindow)) {
1180
- cache.lastActivity = now;
1181
- return cache;
1182
- }
1183
- }
1184
-
1185
- cache.messages.push(incoming);
1186
- cache.lastActivity = Date.now();
1187
-
1188
- // Count turns (user message after assistant = new turn)
1189
- if (incoming.role === "user" && cache.messages.length > 1) {
1190
- const prevMsg = cache.messages[cache.messages.length - 2];
1191
- if (prevMsg && prevMsg.role === "assistant") {
1192
- cache.turnCount++;
1193
- }
1194
- }
853
+ function addToSessionCache(sessionId, message) {
854
+ const cache = getSessionCache(sessionId);
855
+ cache.messages.push(message);
856
+ cache.lastActivity = Date.now();
857
+
858
+ // Count turns (user message after assistant = new turn)
859
+ if (message.role === "user" && cache.messages.length > 1) {
860
+ const prevMsg = cache.messages[cache.messages.length - 2];
861
+ if (prevMsg && prevMsg.role === "assistant") {
862
+ cache.turnCount++;
863
+ }
864
+ }
1195
865
 
1196
866
  return cache;
1197
867
  }
@@ -1348,18 +1018,12 @@ function registerPlugin(api) {
1348
1018
  return;
1349
1019
  }
1350
1020
 
1351
- const sessionId = resolveSessionId(ctx, event) || `session-${Date.now()}`;
1352
- const userContent = normalizeUserMessageContent(prompt);
1353
- const rawUserContent = stripPrependedPrompt(prompt);
1354
- const messageId = extractRelayMessageId(rawUserContent || prompt);
1355
- if (userContent) {
1356
- addToSessionCache(sessionId, {
1357
- role: "user",
1358
- content: userContent,
1359
- rawContent: rawUserContent,
1360
- messageId: messageId || undefined,
1361
- });
1362
- }
1021
+ const sessionId = resolveSessionId(ctx, event) || `session-${Date.now()}`;
1022
+ const userContent = normalizeUserMessageContent(prompt);
1023
+ const rawUserContent = stripPrependedPrompt(prompt);
1024
+ if (userContent) {
1025
+ addToSessionCache(sessionId, { role: "user", content: userContent, rawContent: rawUserContent });
1026
+ }
1363
1027
 
1364
1028
  try {
1365
1029
  const memories = await retrieveMemory(prompt, cfg, ctx, log);
@@ -1486,18 +1150,12 @@ function createHooksPlugin(config) {
1486
1150
  return;
1487
1151
  }
1488
1152
 
1489
- const sessionId = resolveSessionId(ctx, event) || `session-${Date.now()}`;
1490
- const userContent = normalizeUserMessageContent(prompt);
1491
- const rawUserContent = stripPrependedPrompt(prompt);
1492
- const messageId = extractRelayMessageId(rawUserContent || prompt);
1493
- if (userContent) {
1494
- addToSessionCache(sessionId, {
1495
- role: "user",
1496
- content: userContent,
1497
- rawContent: rawUserContent,
1498
- messageId: messageId || undefined,
1499
- });
1500
- }
1153
+ const sessionId = resolveSessionId(ctx, event) || `session-${Date.now()}`;
1154
+ const userContent = normalizeUserMessageContent(prompt);
1155
+ const rawUserContent = stripPrependedPrompt(prompt);
1156
+ if (userContent) {
1157
+ addToSessionCache(sessionId, { role: "user", content: userContent, rawContent: rawUserContent });
1158
+ }
1501
1159
 
1502
1160
  try {
1503
1161
  const memories = await retrieveMemory(prompt, cfg, ctx, log);
@@ -1565,19 +1223,13 @@ function createHooksPlugin(config) {
1565
1223
  const content = msg.role === "user"
1566
1224
  ? normalizeUserMessageContent(msg.content)
1567
1225
  : normalizeAssistantMessageContent(msg.content);
1568
- const rawSource = msg.role === "user"
1569
- ? stripPrependedPrompt(msg.content)
1570
- : extractText(msg.content);
1571
- const messageId = msg.role === "user" ? extractRelayMessageId(rawSource || "") : "";
1572
- if (content) {
1573
- addToSessionCache(sessionId, {
1574
- role: msg.role,
1575
- content,
1576
- rawContent: rawSource || undefined,
1577
- messageId: messageId || undefined,
1578
- });
1579
- }
1580
- }
1226
+ const rawSource = msg.role === "user"
1227
+ ? stripPrependedPrompt(msg.content)
1228
+ : extractText(msg.content);
1229
+ if (content) {
1230
+ addToSessionCache(sessionId, { role: msg.role, content, rawContent: rawSource || undefined });
1231
+ }
1232
+ }
1581
1233
  } else {
1582
1234
  // Only add last assistant message
1583
1235
  for (let i = event.messages.length - 1; i >= 0; i--) {
@@ -1659,7 +1311,7 @@ function buildConfig(config) {
1659
1311
  const configuredUserId = config?.userId || process.env.HUMAN_LIKE_MEM_USER_ID;
1660
1312
 
1661
1313
  return {
1662
- baseUrl: config?.baseUrl || process.env.HUMAN_LIKE_MEM_BASE_URL || "https://human-like.me",
1314
+ baseUrl: config?.baseUrl || process.env.HUMAN_LIKE_MEM_BASE_URL || "https://plugin.human-like.me",
1663
1315
  apiKey: config?.apiKey || process.env.HUMAN_LIKE_MEM_API_KEY,
1664
1316
  configuredUserId: configuredUserId,
1665
1317
  userId: configuredUserId || "openclaw-user",
@@ -3,7 +3,7 @@
3
3
  "id": "human-like-mem",
4
4
  "name": "Human-Like Memory Plugin",
5
5
  "description": "Long-term memory plugin with automatic recall and storage.",
6
- "version": "0.3.12",
6
+ "version": "0.3.14",
7
7
  "kind": "lifecycle",
8
8
  "main": "./index.js",
9
9
  "author": {
@@ -25,7 +25,7 @@
25
25
  "baseUrl": {
26
26
  "type": "string",
27
27
  "description": "Memory API base URL",
28
- "default": "https://human-like.me"
28
+ "default": "https://plugin.human-like.me"
29
29
  },
30
30
  "userId": {
31
31
  "type": "string",
@@ -90,7 +90,7 @@
90
90
  "baseUrl": {
91
91
  "type": "string",
92
92
  "description": "Memory API base URL",
93
- "default": "https://human-like.me",
93
+ "default": "https://plugin.human-like.me",
94
94
  "env": "HUMAN_LIKE_MEM_BASE_URL"
95
95
  },
96
96
  "userId": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanlikememory/human-like-mem",
3
- "version": "0.3.12",
3
+ "version": "0.3.14",
4
4
  "description": "Long-term memory plugin for OpenClaw - AI Social Memory Integration",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -50,7 +50,7 @@
50
50
  "jest": "^29.7.0",
51
51
  "prettier": "^3.2.0"
52
52
  },
53
- "publishConfig": {
54
- "access": "public"
53
+ "publishConfig": {
54
+ "access": "public"
55
55
  }
56
56
  }