@humanlikememory/human-like-mem 0.3.13 → 0.3.15

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
@@ -4,13 +4,50 @@
4
4
 
5
5
  Long-term memory plugin for OpenClaw with automatic memory recall and memory storage.
6
6
 
7
- ## Install
8
-
9
- ```bash
10
- npm install @humanlikememory/human-like-mem
11
- ```
12
-
13
- ## Enable In OpenClaw
7
+ ## Install
8
+
9
+ Recommended: Method 1.
10
+
11
+ ### Method 1: Install With OpenClaw CLI
12
+
13
+ ```bash
14
+ openclaw plugins install @humanlikememory/human-like-mem
15
+ ```
16
+
17
+ Then edit `~/.openclaw/openclaw.json`:
18
+
19
+ ```bash
20
+ vim ~/.openclaw/openclaw.json
21
+ ```
22
+
23
+ ```json
24
+ {
25
+ "plugins": {
26
+ "entries": {
27
+ "human-like-mem": {
28
+ "enabled": true,
29
+ "config": {
30
+ "minTurnsToStore": 5,
31
+ "apiKey": "mp_......",
32
+ "baseUrl": "https://plugin.human-like.me"
33
+ }
34
+ }
35
+ }
36
+ }
37
+ }
38
+ ```
39
+
40
+ ### Method 2: Install With npm
41
+
42
+ You can also install with npm first, then enable it in OpenClaw.
43
+
44
+ ```bash
45
+ npm install @humanlikememory/human-like-mem
46
+ ```
47
+
48
+ OpenClaw can also help install the package directly, so Method 1 is recommended.
49
+
50
+ ## Enable In OpenClaw
14
51
 
15
52
  Edit `~/.openclaw/openclaw.json`:
16
53
 
@@ -35,7 +72,7 @@ export HUMAN_LIKE_MEM_API_KEY="mp_xxxxxx"
35
72
  ## Optional Environment Variables
36
73
 
37
74
  ```bash
38
- export HUMAN_LIKE_MEM_BASE_URL="https://plugin.human-like.me"
75
+ export HUMAN_LIKE_MEM_BASE_URL="https://plugin.human-like.me"
39
76
  export HUMAN_LIKE_MEM_USER_ID="your-user-id"
40
77
  export HUMAN_LIKE_MEM_AGENT_ID="main"
41
78
  export HUMAN_LIKE_MEM_LIMIT_NUMBER="6"
package/README_ZH.md CHANGED
@@ -1,70 +1,107 @@
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
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
+ 推荐:优先使用第一种方式。
10
+
11
+ ### 方法一:通过 OpenClaw CLI 安装
12
+
13
+ ```bash
14
+ openclaw plugins install @humanlikememory/human-like-mem
15
+ ```
16
+
17
+ 然后编辑 `~/.openclaw/openclaw.json`:
18
+
19
+ ```bash
20
+ vim ~/.openclaw/openclaw.json
21
+ ```
22
+
23
+ ```json
24
+ {
25
+ "plugins": {
26
+ "entries": {
27
+ "human-like-mem": {
28
+ "enabled": true,
29
+ "config": {
30
+ "minTurnsToStore": 5,
31
+ "apiKey": "mp_xxxxxx",
32
+ "baseUrl": "https://plugin.human-like.me"
33
+ }
34
+ }
35
+ }
36
+ }
37
+ }
38
+ ```
39
+
40
+ ### 方法二:通过 npm 安装
41
+
42
+ 也可以先用 npm 安装,再在 OpenClaw 中启用。
43
+
44
+ ```bash
45
+ npm install @humanlikememory/human-like-mem
46
+ ```
47
+
48
+ OpenClaw 也可以直接帮你安装,所以一般更推荐第一种方式。
49
+
50
+ ## 在 OpenClaw 中启用
51
+
52
+ 编辑 `~/.openclaw/openclaw.json`:
53
+
54
+ ```json
55
+ {
56
+ "plugins": {
57
+ "entries": {
58
+ "human-like-mem": {
59
+ "enabled": true
60
+ }
61
+ }
62
+ }
63
+ }
64
+ ```
65
+
66
+ ## 必填环境变量
67
+
68
+ ```bash
69
+ export HUMAN_LIKE_MEM_API_KEY="mp_xxxxxx"
70
+ ```
71
+
72
+ ## 可选环境变量
73
+
74
+ ```bash
75
+ export HUMAN_LIKE_MEM_BASE_URL="https://plugin.human-like.me"
76
+ export HUMAN_LIKE_MEM_USER_ID="your-user-id"
77
+ export HUMAN_LIKE_MEM_AGENT_ID="main"
78
+ export HUMAN_LIKE_MEM_LIMIT_NUMBER="6"
79
+ export HUMAN_LIKE_MEM_MIN_SCORE="0.1"
80
+ export HUMAN_LIKE_MEM_MIN_TURNS="5"
81
+ export HUMAN_LIKE_MEM_SESSION_TIMEOUT="300000"
82
+ ```
83
+
84
+ ## 工作机制
85
+
86
+ - 回复前:插件会检索相关记忆并注入上下文。
87
+ - 回复后:插件会缓存会话,并在达到轮次阈值或超时后写入记忆。
88
+
89
+ 默认存储策略:
90
+
91
+ - `minTurnsToStore = 5`
92
+ - `maxTurnsToStore = minTurnsToStore * 2`
93
+ - `sessionTimeoutMs = 300000`(5 分钟)
94
+
95
+ ## 常见问题
96
+
97
+ - 如果日志里出现 `HUMAN_LIKE_MEM_API_KEY not configured`,请确认运行环境变量已经生效。
98
+ - 如果日志里出现请求超时,可以把插件 `timeoutMs` 调大一些,比如 `30000`。
99
+ - 查看日志:
100
+
101
+ ```bash
102
+ openclaw logs --plain --limit 200
103
+ ```
104
+
105
+ ## 许可证
106
+
107
+ Apache-2.0
package/index.js CHANGED
@@ -8,7 +8,6 @@
8
8
 
9
9
  const PLUGIN_VERSION = "0.3.4";
10
10
  const USER_QUERY_MARKER = "--- User Query ---";
11
- const CACHE_DEDUP_WINDOW_MS = 4000;
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);
@@ -60,127 +59,6 @@ function extractText(content) {
60
59
  return "";
61
60
  }
62
61
 
63
- function isToolCallBlock(block) {
64
- if (!block || typeof block !== "object") return false;
65
- const type = String(block.type || "").trim().toLowerCase();
66
- return type === "toolcall" || type === "tool_call";
67
- }
68
-
69
- function normalizeToolCallBlock(block) {
70
- if (!isToolCallBlock(block)) return null;
71
-
72
- const name =
73
- block.function?.name ||
74
- block.toolName ||
75
- block.name ||
76
- "unknown";
77
- const args =
78
- block.function?.arguments ??
79
- block.arguments ??
80
- block.args ??
81
- block.input ??
82
- {};
83
- const callId =
84
- block.id ||
85
- block.callId ||
86
- block.toolCallId ||
87
- block.tool_call_id ||
88
- null;
89
-
90
- return {
91
- id: callId,
92
- name,
93
- arguments: args,
94
- function: {
95
- name,
96
- arguments: args,
97
- },
98
- };
99
- }
100
-
101
- function extractToolCallsFromContent(content) {
102
- if (!Array.isArray(content)) return [];
103
-
104
- return content
105
- .map((block) => normalizeToolCallBlock(block))
106
- .filter(Boolean);
107
- }
108
-
109
- function getMessageToolCalls(msg) {
110
- if (!msg || typeof msg !== "object") return [];
111
- if (Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
112
- return msg.tool_calls;
113
- }
114
- return extractToolCallsFromContent(msg.content);
115
- }
116
-
117
- function getToolResultCallId(msg) {
118
- if (!msg || typeof msg !== "object") return null;
119
- return (
120
- msg.tool_call_id ||
121
- msg.toolCallId ||
122
- msg.call_id ||
123
- msg.callId ||
124
- null
125
- );
126
- }
127
-
128
- function getToolResultName(msg) {
129
- if (!msg || typeof msg !== "object") return undefined;
130
- return msg.name || msg.toolName || msg.tool_name || undefined;
131
- }
132
-
133
- function normalizeCacheRole(role) {
134
- if (role === "toolResult") return "tool";
135
- return role;
136
- }
137
-
138
- function isAssistantLikeRole(role) {
139
- return role === "assistant" || role === "tool" || role === "toolResult";
140
- }
141
-
142
- function buildCacheMessageFromTranscript(msg) {
143
- if (!msg || msg.role === "system") return null;
144
-
145
- const normalizedRole = normalizeCacheRole(msg.role);
146
- let content = "";
147
- let rawSource = msg.content;
148
- let messageId = "";
149
-
150
- if (msg.role === "user") {
151
- content = normalizeUserMessageContent(msg.content);
152
- rawSource = stripPrependedPrompt(msg.content);
153
- messageId = extractRelayMessageId(rawSource || "");
154
- } else if (msg.role === "assistant") {
155
- content = normalizeAssistantMessageContent(msg.content);
156
- rawSource = extractText(msg.content);
157
- } else if (normalizedRole === "tool") {
158
- content = extractText(msg.content);
159
- rawSource = content;
160
- } else {
161
- return null;
162
- }
163
-
164
- const toolCalls = getMessageToolCalls(msg);
165
- const toolCallId = getToolResultCallId(msg);
166
- const toolName = getToolResultName(msg);
167
- const hasToolContext =
168
- (normalizedRole === "assistant" && toolCalls.length > 0) ||
169
- (normalizedRole === "tool" && (!!toolCallId || !!toolName));
170
-
171
- if (!content && !hasToolContext) return null;
172
-
173
- return {
174
- role: normalizedRole,
175
- content: content || "",
176
- rawContent: rawSource || undefined,
177
- messageId: messageId || undefined,
178
- tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
179
- tool_call_id: toolCallId || undefined,
180
- name: toolName,
181
- };
182
- }
183
-
184
62
  /**
185
63
  * Strip prepended prompt markers from message content
186
64
  */
@@ -245,65 +123,6 @@ function isMetadataOnlyText(text) {
245
123
  return false;
246
124
  }
247
125
 
248
- /**
249
- * Remove injected relay metadata blocks/lines from user content.
250
- */
251
- function stripInjectedMetadata(text) {
252
- if (!text || typeof text !== "string") return "";
253
-
254
- let result = text.replace(/\r\n/g, "\n");
255
-
256
- // Remove labeled untrusted metadata JSON blocks.
257
- // Examples:
258
- // Conversation info (untrusted metadata): ```json ... ```
259
- // Sender (untrusted metadata): ```json ... ```
260
- result = result.replace(
261
- /(^|\n)[^\n]*\(untrusted metadata(?:,\s*for context)?\)\s*:\s*```json[\s\S]*?```(?=\n|$)/gi,
262
- "\n"
263
- );
264
-
265
- // Remove transport/system hint lines that are not user utterances.
266
- result = result.replace(/^\[System:[^\n]*\]\s*$/gim, "");
267
- result = result.replace(/^\[message_id:\s*[^\]]+\]\s*$/gim, "");
268
-
269
- // Collapse extra blank lines.
270
- result = result.replace(/\n{3,}/g, "\n\n").trim();
271
- return result;
272
- }
273
-
274
- /**
275
- * Keep the latest meaningful line from cleaned metadata-injected payload.
276
- */
277
- function extractLatestUtteranceFromCleanText(text) {
278
- if (!text || typeof text !== "string") return "";
279
- const lines = text
280
- .split("\n")
281
- .map((line) => line.trim())
282
- .filter(Boolean)
283
- .filter((line) => !isMetadataOnlyText(line));
284
- if (lines.length === 0) return "";
285
- return lines[lines.length - 1];
286
- }
287
-
288
- function escapeRegex(text) {
289
- return String(text).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
290
- }
291
-
292
- /**
293
- * Prefix utterance with parsed sender name when available.
294
- * This preserves who said it in multi-user group contexts.
295
- */
296
- function attachSenderPrefix(utterance, senderName) {
297
- const content = String(utterance || "").trim();
298
- if (!content) return "";
299
- const sender = String(senderName || "").trim();
300
- if (!sender) return content;
301
-
302
- const senderRegex = new RegExp(`^${escapeRegex(sender)}\\s*[::]`);
303
- if (senderRegex.test(content)) return content;
304
- return `${sender}: ${content}`;
305
- }
306
-
307
126
  /**
308
127
  * Normalize user message text before caching/storing.
309
128
  * For channel-formatted payloads (e.g. Feishu), keep only the actual user utterance
@@ -316,17 +135,6 @@ function normalizeUserMessageContent(content) {
316
135
  const normalized = String(text).replace(/\r\n/g, "\n").trim();
317
136
  if (!normalized) return "";
318
137
 
319
- const parsedIdentity = parseIdentityFromUntrustedMetadata(normalized);
320
-
321
- // New relay payload format: strip injected metadata blocks first.
322
- const strippedMetadata = stripInjectedMetadata(normalized);
323
- if (strippedMetadata && strippedMetadata !== normalized) {
324
- const latestUtterance = extractLatestUtteranceFromCleanText(strippedMetadata);
325
- if (latestUtterance && !isMetadataOnlyText(latestUtterance)) {
326
- return attachSenderPrefix(latestUtterance, parsedIdentity?.userName);
327
- }
328
- }
329
-
330
138
  // Feishu: preserve the final traceable tail block (contains platform user id/message_id).
331
139
  const feishuTailBlock = extractFeishuTailBlock(normalized);
332
140
  if (feishuTailBlock && !isMetadataOnlyText(feishuTailBlock)) {
@@ -360,9 +168,7 @@ function normalizeUserMessageContent(content) {
360
168
  if (!isMetadataOnlyText(candidate)) return candidate;
361
169
  }
362
170
 
363
- return isMetadataOnlyText(normalized)
364
- ? ""
365
- : attachSenderPrefix(normalized, parsedIdentity?.userName);
171
+ return isMetadataOnlyText(normalized) ? "" : normalized;
366
172
  }
367
173
 
368
174
  /**
@@ -377,125 +183,6 @@ function normalizeAssistantMessageContent(content) {
377
183
  return normalized;
378
184
  }
379
185
 
380
- /**
381
- * Parse JSON objects from markdown code fences after a specific label.
382
- * Example:
383
- * Sender (untrusted metadata):
384
- * ```json
385
- * { "id": "123" }
386
- * ```
387
- */
388
- function parseJsonFencesAfterLabel(text, labelPattern) {
389
- if (!text || typeof text !== "string") return [];
390
-
391
- const results = [];
392
- const regex = new RegExp(`${labelPattern}\\s*:\\s*\`\`\`json\\s*([\\s\\S]*?)\\s*\`\`\``, "gi");
393
- let match;
394
- while ((match = regex.exec(text)) !== null) {
395
- if (!match[1]) continue;
396
- try {
397
- const parsed = JSON.parse(match[1]);
398
- if (parsed && typeof parsed === "object") {
399
- results.push(parsed);
400
- }
401
- } catch (_) {
402
- // Ignore malformed JSON blocks and continue matching others.
403
- }
404
- }
405
- return results;
406
- }
407
-
408
- /**
409
- * Normalize potential user id values into non-empty strings.
410
- */
411
- function normalizeParsedUserId(value) {
412
- if (value === undefined || value === null) return "";
413
- const normalized = String(value).trim();
414
- return normalized || "";
415
- }
416
-
417
- /**
418
- * Extract user id from a display label like:
419
- * "用户250389 (ou_xxx)" / "name (123456789)".
420
- */
421
- function extractUserIdFromLabel(label) {
422
- const text = normalizeParsedUserId(label);
423
- if (!text) return "";
424
- const match = text.match(/\(\s*((?:ou|on|u)_[A-Za-z0-9]+|\d{6,})\s*\)$/i);
425
- return match?.[1] ? match[1] : "";
426
- }
427
-
428
- /**
429
- * Extract transport message id from current relay payload.
430
- */
431
- function extractRelayMessageId(text) {
432
- if (!text || typeof text !== "string") return "";
433
-
434
- const conversationMetaList = parseJsonFencesAfterLabel(
435
- text,
436
- "Conversation info \\(untrusted metadata\\)"
437
- );
438
- for (let i = conversationMetaList.length - 1; i >= 0; i--) {
439
- const conversationMeta = conversationMetaList[i];
440
- const messageId =
441
- normalizeParsedUserId(conversationMeta?.message_id) ||
442
- normalizeParsedUserId(conversationMeta?.messageId);
443
- if (messageId) return messageId;
444
- }
445
-
446
- const messageIdMatch = text.match(/\[message_id:\s*([^\]\n]+)\]/i);
447
- if (messageIdMatch?.[1]) return messageIdMatch[1].trim();
448
-
449
- return "";
450
- }
451
-
452
- /**
453
- * Parse identity from new "untrusted metadata" JSON blocks.
454
- * Priority:
455
- * 1) Sender (untrusted metadata).id
456
- * 2) Conversation info (untrusted metadata).sender_id
457
- */
458
- function parseIdentityFromUntrustedMetadata(text) {
459
- if (!text || typeof text !== "string") return null;
460
-
461
- const senderMetaList = parseJsonFencesAfterLabel(text, "Sender \\(untrusted metadata\\)");
462
- for (let i = senderMetaList.length - 1; i >= 0; i--) {
463
- const senderMeta = senderMetaList[i];
464
- const userId =
465
- normalizeParsedUserId(senderMeta?.id) ||
466
- extractUserIdFromLabel(senderMeta?.label);
467
- if (userId) {
468
- return {
469
- platform: null,
470
- userId,
471
- userName: senderMeta?.name || senderMeta?.username || senderMeta?.label || null,
472
- source: "sender-untrusted-metadata-json",
473
- };
474
- }
475
- }
476
-
477
- const conversationMetaList = parseJsonFencesAfterLabel(
478
- text,
479
- "Conversation info \\(untrusted metadata\\)"
480
- );
481
- for (let i = conversationMetaList.length - 1; i >= 0; i--) {
482
- const conversationMeta = conversationMetaList[i];
483
- const userId =
484
- normalizeParsedUserId(conversationMeta?.sender_id) ||
485
- normalizeParsedUserId(conversationMeta?.senderId);
486
- if (userId) {
487
- return {
488
- platform: null,
489
- userId,
490
- userName: conversationMeta?.sender || null,
491
- source: "conversation-untrusted-metadata-json",
492
- };
493
- }
494
- }
495
-
496
- return null;
497
- }
498
-
499
186
  /**
500
187
  * Parse platform identity hints from channel-formatted message text.
501
188
  * Returns null when no platform-specific id can be extracted.
@@ -503,14 +190,6 @@ function parseIdentityFromUntrustedMetadata(text) {
503
190
  function parsePlatformIdentity(text) {
504
191
  if (!text || typeof text !== "string") return null;
505
192
 
506
- // New Discord format first:
507
- // Sender (untrusted metadata): ```json { "id": "116..." } ```
508
- // Conversation info (untrusted metadata): ```json { "sender_id": "116..." } ```
509
- const metadataIdentity = parseIdentityFromUntrustedMetadata(text);
510
- if (metadataIdentity?.userId) {
511
- return metadataIdentity;
512
- }
513
-
514
193
  // Discord example:
515
194
  // [from: huang yongqing (1470374017541079042)]
516
195
  const discordFrom = text.match(/\[from:\s*([^\(\]\n]+?)\s*\((\d{6,})\)\]/i);
@@ -545,32 +224,11 @@ function parseAllPlatformUserIds(text) {
545
224
  if (!text || typeof text !== "string") return [];
546
225
 
547
226
  const ids = [];
548
- let match;
549
-
550
- // New format: "Sender (untrusted metadata)" JSON blocks.
551
- const senderMetaList = parseJsonFencesAfterLabel(text, "Sender \\(untrusted metadata\\)");
552
- for (const senderMeta of senderMetaList) {
553
- const parsedId =
554
- normalizeParsedUserId(senderMeta?.id) ||
555
- extractUserIdFromLabel(senderMeta?.label);
556
- if (parsedId) ids.push(parsedId);
557
- }
558
-
559
- // New format: "Conversation info (untrusted metadata)" JSON blocks.
560
- const conversationMetaList = parseJsonFencesAfterLabel(
561
- text,
562
- "Conversation info \\(untrusted metadata\\)"
563
- );
564
- for (const conversationMeta of conversationMetaList) {
565
- const parsedId =
566
- normalizeParsedUserId(conversationMeta?.sender_id) ||
567
- normalizeParsedUserId(conversationMeta?.senderId);
568
- if (parsedId) ids.push(parsedId);
569
- }
570
227
 
571
228
  // Discord example:
572
229
  // [from: huang yongqing (1470374017541079042)]
573
230
  const discordRegex = /\[from:\s*[^\(\]\n]+?\s*\((\d{6,})\)\]/gi;
231
+ let match;
574
232
  while ((match = discordRegex.exec(text)) !== null) {
575
233
  if (match[1]) ids.push(match[1]);
576
234
  }
@@ -608,90 +266,6 @@ function collectUniqueUserIdsFromMessages(messages, fallbackUserId) {
608
266
  return Array.from(unique);
609
267
  }
610
268
 
611
- function extractUserNameFromLabel(label) {
612
- const text = normalizeParsedUserId(label);
613
- if (!text) return "";
614
- const match = text.match(
615
- /^(.*?)\s*\(\s*(?:(?:ou|on|u)_[A-Za-z0-9]+|\d{6,})\s*\)\s*$/i
616
- );
617
- return match?.[1] ? match[1].trim() : text;
618
- }
619
-
620
- /**
621
- * Parse name-id pairs from supported relay payload formats.
622
- */
623
- function parseNameIdPairsFromText(text) {
624
- if (!text || typeof text !== "string") return [];
625
- const pairs = [];
626
-
627
- // New format: Sender (untrusted metadata)
628
- const senderMetaList = parseJsonFencesAfterLabel(text, "Sender \\(untrusted metadata\\)");
629
- for (const senderMeta of senderMetaList) {
630
- const id =
631
- normalizeParsedUserId(senderMeta?.id) ||
632
- extractUserIdFromLabel(senderMeta?.label);
633
- const name =
634
- normalizeParsedUserId(senderMeta?.name) ||
635
- normalizeParsedUserId(senderMeta?.username) ||
636
- extractUserNameFromLabel(senderMeta?.label);
637
- if (name && id) pairs.push([name, id]);
638
- }
639
-
640
- // New format: Conversation info (untrusted metadata)
641
- const conversationMetaList = parseJsonFencesAfterLabel(
642
- text,
643
- "Conversation info \\(untrusted metadata\\)"
644
- );
645
- for (const conversationMeta of conversationMetaList) {
646
- const id =
647
- normalizeParsedUserId(conversationMeta?.sender_id) ||
648
- normalizeParsedUserId(conversationMeta?.senderId);
649
- const name = normalizeParsedUserId(conversationMeta?.sender);
650
- if (name && id) pairs.push([name, id]);
651
- }
652
-
653
- // Old Discord: [from: name (id)]
654
- const discordRegex = /\[from:\s*([^\(\]\n]+?)\s*\((\d{6,})\)\]/gi;
655
- let match;
656
- while ((match = discordRegex.exec(text)) !== null) {
657
- const name = normalizeParsedUserId(match[1]);
658
- const id = normalizeParsedUserId(match[2]);
659
- if (name && id) pairs.push([name, id]);
660
- }
661
-
662
- // Old Feishu: [Feishu ...:ou_xxx ...] name:
663
- const feishuRegex = /\[Feishu[^\]]*:((?:ou|on|u)_[A-Za-z0-9]+)[^\]]*\]\s*([^:\n]+):/gi;
664
- while ((match = feishuRegex.exec(text)) !== null) {
665
- const id = normalizeParsedUserId(match[1]);
666
- const name = normalizeParsedUserId(match[2]);
667
- if (name && id) pairs.push([name, id]);
668
- }
669
-
670
- return pairs;
671
- }
672
-
673
- /**
674
- * Collect distinct name->id mapping from all messages.
675
- */
676
- function collectNameIdMapFromMessages(messages) {
677
- const map = {};
678
-
679
- if (!Array.isArray(messages)) return map;
680
-
681
- for (const msg of messages) {
682
- if (!msg) continue;
683
- const sourceText = msg.rawContent !== undefined ? msg.rawContent : msg.content;
684
- const text = typeof sourceText === "string" ? sourceText : extractText(sourceText);
685
- const pairs = parseNameIdPairsFromText(text);
686
- for (const [name, id] of pairs) {
687
- if (!name || !id) continue;
688
- map[name] = id;
689
- }
690
- }
691
-
692
- return map;
693
- }
694
-
695
269
  /**
696
270
  * Get latest user message text from cached messages.
697
271
  */
@@ -710,7 +284,7 @@ function getLatestUserMessageText(messages) {
710
284
  * Resolve request identity with fallback:
711
285
  * platform user id -> configured user id -> "openclaw-user"
712
286
  */
713
- function resolveRequestIdentity(promptText, cfg) {
287
+ function resolveRequestIdentity(promptText, cfg, ctx) {
714
288
  const parsed = parsePlatformIdentity(promptText);
715
289
  if (parsed?.userId) {
716
290
  return {
@@ -730,6 +304,15 @@ function resolveRequestIdentity(promptText, cfg) {
730
304
  };
731
305
  }
732
306
 
307
+ if (ctx?.userId) {
308
+ return {
309
+ userId: ctx.userId,
310
+ userName: null,
311
+ platform: null,
312
+ source: "ctx-user-id",
313
+ };
314
+ }
315
+
733
316
  return {
734
317
  userId: "openclaw-user",
735
318
  userName: null,
@@ -924,7 +507,7 @@ async function retrieveMemory(prompt, cfg, ctx, log) {
924
507
  if (log?.info) {
925
508
  log.info(`[Memory Plugin] Recall request URL: ${url}`);
926
509
  }
927
- const identity = resolveRequestIdentity(prompt, cfg);
510
+ const identity = resolveRequestIdentity(prompt, cfg, ctx);
928
511
  const userId = sanitizeUserId(identity.userId);
929
512
  const payload = {
930
513
  query: prompt,
@@ -971,7 +554,7 @@ async function retrieveMemory(prompt, cfg, ctx, log) {
971
554
  /**
972
555
  * Add memories to the API
973
556
  */
974
- async function addMemory(messages, cfg, ctx, log, explicitSessionId) {
557
+ async function addMemory(messages, cfg, ctx, log) {
975
558
  const baseUrl = cfg.baseUrl || process.env.HUMAN_LIKE_MEM_BASE_URL;
976
559
  const apiKey = cfg.apiKey || process.env.HUMAN_LIKE_MEM_API_KEY;
977
560
 
@@ -981,9 +564,9 @@ async function addMemory(messages, cfg, ctx, log, explicitSessionId) {
981
564
  if (log?.info) {
982
565
  log.info(`[Memory Plugin] Add-memory request URL: ${url}`);
983
566
  }
984
- const sessionId = explicitSessionId || resolveSessionId(ctx, null) || `session-${Date.now()}`;
567
+ const sessionId = resolveSessionId(ctx, null) || `session-${Date.now()}`;
985
568
  const latestUserText = getLatestUserMessageText(messages);
986
- const identity = resolveRequestIdentity(latestUserText, cfg);
569
+ const identity = resolveRequestIdentity(latestUserText, cfg, ctx);
987
570
  const userId = sanitizeUserId(identity.userId);
988
571
  const metadataUserIds = (() => {
989
572
  const parsed = collectUniqueUserIdsFromMessages(messages, null)
@@ -994,13 +577,6 @@ async function addMemory(messages, cfg, ctx, log, explicitSessionId) {
994
577
  }
995
578
  return [userId];
996
579
  })();
997
- const nameIdMap = (() => {
998
- const parsed = collectNameIdMapFromMessages(messages);
999
- if (identity?.userName && userId && !parsed[identity.userName]) {
1000
- parsed[identity.userName] = userId;
1001
- }
1002
- return parsed;
1003
- })();
1004
580
  const agentId = cfg.agentId || ctx?.agentId || "main";
1005
581
 
1006
582
  const payload = {
@@ -1018,7 +594,6 @@ async function addMemory(messages, cfg, ctx, log, explicitSessionId) {
1018
594
  metadata: JSON.stringify({
1019
595
  user_ids: metadataUserIds,
1020
596
  agent_ids: [agentId],
1021
- name_id_map: nameIdMap,
1022
597
  session_id: sessionId,
1023
598
  scenario: cfg.scenario || "openclaw-plugin",
1024
599
  }),
@@ -1028,7 +603,7 @@ async function addMemory(messages, cfg, ctx, log, explicitSessionId) {
1028
603
 
1029
604
  if (log?.debug) {
1030
605
  log.debug(
1031
- `[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)}`
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])}`
1032
607
  );
1033
608
  }
1034
609
 
@@ -1178,8 +753,8 @@ function formatMemoriesForContext(memories, options = {}) {
1178
753
  * @param {number} maxTurns - Maximum number of turns to extract
1179
754
  * @returns {Array} Recent messages
1180
755
  */
1181
- function pickRecentMessages(messages, maxTurns = 10) {
1182
- if (!messages || messages.length === 0) return [];
756
+ function pickRecentMessages(messages, maxTurns = 10) {
757
+ if (!messages || messages.length === 0) return [];
1183
758
 
1184
759
  const result = [];
1185
760
  let turnCount = 0;
@@ -1194,7 +769,7 @@ function pickRecentMessages(messages, maxTurns = 10) {
1194
769
  if (role === "system") continue;
1195
770
 
1196
771
  // Count a turn when we see a user message after an assistant message
1197
- if (role === "user" && isAssistantLikeRole(lastRole)) {
772
+ if (role === "user" && lastRole === "assistant") {
1198
773
  turnCount++;
1199
774
  }
1200
775
 
@@ -1220,230 +795,9 @@ function pickRecentMessages(messages, maxTurns = 10) {
1220
795
  lastRole = role;
1221
796
  }
1222
797
 
1223
- return result;
1224
- }
1225
-
1226
- function buildV2ConversationMessage(msg) {
1227
- if (!msg) return null;
1228
-
1229
- const role = normalizeCacheRole(msg.role);
1230
- if (role === "system") return null;
1231
- if (role !== "user" && role !== "assistant" && role !== "tool") return null;
1232
-
1233
- const rawSource = msg.rawContent !== undefined ? msg.rawContent : msg.content;
1234
- const rawContent = typeof rawSource === "string" ? rawSource : extractText(rawSource);
1235
- const content =
1236
- typeof msg.content === "string"
1237
- ? msg.content
1238
- : extractText(msg.content);
1239
- const toolCalls = role === "assistant" ? getMessageToolCalls(msg) : [];
1240
- const toolCallId = role === "tool" ? getToolResultCallId(msg) : null;
1241
- const toolName = role === "tool" ? getToolResultName(msg) : undefined;
1242
- const hasToolContext =
1243
- (role === "assistant" && toolCalls.length > 0) ||
1244
- (role === "tool" && (!!toolCallId || !!toolName));
1245
-
1246
- if (!content && !hasToolContext) return null;
1247
-
1248
- const result = {
1249
- role,
1250
- content: content || "",
1251
- rawContent: rawContent || undefined,
1252
- };
1253
-
1254
- if (toolCalls.length > 0) result.tool_calls = toolCalls;
1255
- if (toolCallId) result.tool_call_id = toolCallId;
1256
- if (toolName) result.name = toolName;
1257
-
1258
- return result;
1259
- }
1260
-
1261
- function pickRecentContextMessagesV2(messages, maxTurns = 10) {
1262
- if (!messages || messages.length === 0) return [];
1263
-
1264
- const result = [];
1265
- let turnCount = 0;
1266
- let lastRole = null;
1267
-
1268
- for (let i = messages.length - 1; i >= 0 && turnCount < maxTurns; i--) {
1269
- const msg = messages[i];
1270
- const role = normalizeCacheRole(msg?.role);
1271
-
1272
- if (role === "system") continue;
1273
-
1274
- if (role === "user" && isAssistantLikeRole(lastRole)) {
1275
- turnCount++;
1276
- }
1277
-
1278
- if (turnCount >= maxTurns) break;
1279
-
1280
- const contextMessage = buildV2ConversationMessage(msg);
1281
- if (contextMessage) {
1282
- result.unshift(contextMessage);
1283
- }
1284
-
1285
- lastRole = role;
1286
- }
1287
-
1288
- return result;
1289
- }
1290
-
1291
- function stripToolMessagesForV1(messages) {
1292
- if (!Array.isArray(messages)) return [];
1293
-
1294
- return messages
1295
- .filter((msg) => msg && (msg.role === "user" || msg.role === "assistant"))
1296
- .filter((msg) => String(msg.content || "").trim())
1297
- .map((msg) => ({
1298
- role: msg.role,
1299
- content: msg.content,
1300
- rawContent: msg.rawContent,
1301
- }));
1302
- }
1303
-
1304
- /**
1305
- * Extract tool calls from cached messages for the v2 context protocol.
1306
- * @param {Array} messages - All cached messages
1307
- * @returns {Array} Tool call records
1308
- */
1309
- function extractToolCalls(messages) {
1310
- if (!messages || messages.length === 0) return [];
1311
-
1312
- const calls = [];
1313
- for (const msg of messages) {
1314
- if (msg.role === "assistant") {
1315
- const toolCalls = getMessageToolCalls(msg);
1316
- for (const tc of toolCalls) {
1317
- calls.push({
1318
- tool_name: tc.function?.name || tc.name || "unknown",
1319
- arguments: tc.function?.arguments || tc.arguments || {},
1320
- call_id: tc.id || null,
1321
- result: null,
1322
- success: null,
1323
- duration_ms: null,
1324
- });
1325
- }
1326
- }
1327
-
1328
- if (msg.role === "tool") {
1329
- const toolCallId = getToolResultCallId(msg);
1330
- const toolName = getToolResultName(msg);
1331
- const match = calls.find((call) =>
1332
- (toolCallId && call.call_id === toolCallId) ||
1333
- (!toolCallId && toolName && call.tool_name === toolName && call.result == null)
1334
- );
1335
- if (match) {
1336
- const resultText = extractText(msg.rawContent !== undefined ? msg.rawContent : msg.content);
1337
- match.result = truncate(resultText, 2000);
1338
- match.success = !isErrorResult(resultText);
1339
- }
1340
- }
1341
- }
1342
-
1343
- return calls;
1344
- }
1345
-
1346
- /**
1347
- * Check if text looks like an error result
1348
- * @param {string} text - Result text
1349
- * @returns {boolean}
1350
- */
1351
- function isErrorResult(text) {
1352
- if (!text) return false;
1353
- const lower = String(text).toLowerCase();
1354
- return lower.includes("error") || lower.includes("exception") ||
1355
- lower.includes("failed") || lower.includes("traceback") ||
1356
- lower.includes("enoent") || lower.includes("permission denied");
798
+ return result;
1357
799
  }
1358
800
 
1359
- /**
1360
- * Collect context blocks from messages (v2 protocol)
1361
- * @param {Array} messages - All cached messages
1362
- * @param {Object} cfg - Configuration
1363
- * @returns {Array} Context blocks
1364
- */
1365
- function collectContextBlocks(messages, cfg) {
1366
- const blocks = [];
1367
-
1368
- const conversationMsgs =
1369
- cfg.captureToolCalls === false
1370
- ? pickRecentMessages(messages, cfg.maxTurnsToStore || 10)
1371
- : pickRecentContextMessagesV2(messages, cfg.maxTurnsToStore || 10);
1372
- if (conversationMsgs.length > 0) {
1373
- blocks.push({
1374
- type: "conversation",
1375
- data: { messages: conversationMsgs },
1376
- });
1377
- }
1378
-
1379
- return blocks;
1380
- }
1381
-
1382
- /**
1383
- * Add context via v2 protocol
1384
- */
1385
- async function addContextV2(contextBlocks, cfg, ctx, log, sessionId) {
1386
- const baseUrl = cfg.baseUrl || process.env.HUMAN_LIKE_MEM_BASE_URL;
1387
- const apiKey = cfg.apiKey || process.env.HUMAN_LIKE_MEM_API_KEY;
1388
-
1389
- if (!apiKey) return;
1390
-
1391
- const url = `${baseUrl}/api/plugin/v2/add/context`;
1392
- const latestUserText = getLatestUserMessageText(
1393
- contextBlocks.find((block) => block.type === "conversation")?.data?.messages || []
1394
- );
1395
- const identity = resolveRequestIdentity(latestUserText, cfg);
1396
- const userId = sanitizeUserId(identity.userId);
1397
- const agentId = cfg.agentId || ctx?.agentId || "main";
1398
-
1399
- const payload = {
1400
- user_id: userId,
1401
- conversation_id: sessionId,
1402
- agent_id: agentId,
1403
- tags: cfg.tags || ["openclaw"],
1404
- async_mode: true,
1405
- protocol_version: "2.0",
1406
- context_blocks: contextBlocks,
1407
- };
1408
-
1409
- if (log?.debug) {
1410
- log.debug(
1411
- `[Memory Plugin] v2 add/context: blocks=${contextBlocks.length}, types=${contextBlocks.map((block) => block.type).join(",")}`
1412
- );
1413
- }
1414
-
1415
- try {
1416
- const result = await httpRequest(url, {
1417
- method: "POST",
1418
- headers: {
1419
- "Content-Type": "application/json",
1420
- "x-api-key": apiKey,
1421
- "x-request-id": ctx?.requestId || `openclaw-${Date.now()}`,
1422
- "x-plugin-version": PLUGIN_VERSION,
1423
- "x-client-type": "plugin",
1424
- },
1425
- body: JSON.stringify(payload),
1426
- }, cfg, log);
1427
-
1428
- const memoryCount = result?.memories_count || 0;
1429
- if (log?.info) {
1430
- log.info(
1431
- `[Memory Plugin] v2 add/context success: ${memoryCount} memories from ${result?.blocks_processed || 0} blocks`
1432
- );
1433
- }
1434
- return result;
1435
- } catch (error) {
1436
- if (log?.warn) {
1437
- log.warn(`[Memory Plugin] v2 add/context failed, falling back to v1: ${error.message}`);
1438
- }
1439
- const conversationBlock = contextBlocks.find((block) => block.type === "conversation");
1440
- if (conversationBlock) {
1441
- return await addMemory(stripToolMessagesForV1(conversationBlock.data.messages), cfg, ctx, log, sessionId);
1442
- }
1443
- throw error;
1444
- }
1445
- }
1446
-
1447
801
  /**
1448
802
  * Check if conversation has enough turns to be worth storing
1449
803
  * @param {Array} messages - Messages to check
@@ -1463,7 +817,7 @@ function isConversationWorthStoring(messages, cfg) {
1463
817
  if (msg.role === "system") continue;
1464
818
 
1465
819
  if (msg.role === "user") {
1466
- if (isAssistantLikeRole(lastRole)) {
820
+ if (lastRole === "assistant") {
1467
821
  turns++;
1468
822
  }
1469
823
  }
@@ -1498,39 +852,13 @@ function getSessionCache(sessionId) {
1498
852
  */
1499
853
  function addToSessionCache(sessionId, message) {
1500
854
  const cache = getSessionCache(sessionId);
1501
- const now = Date.now();
1502
- const incoming = {
1503
- ...message,
1504
- cachedAt: now,
1505
- };
1506
-
1507
- const prev = cache.messages.length > 0 ? cache.messages[cache.messages.length - 1] : null;
1508
- if (prev) {
1509
- const sameRole = prev.role === incoming.role;
1510
- const sameMessageId =
1511
- incoming.messageId &&
1512
- prev.messageId &&
1513
- String(incoming.messageId) === String(prev.messageId);
1514
- const sameContent =
1515
- sameRole &&
1516
- String(prev.content || "") === String(incoming.content || "") &&
1517
- String(prev.rawContent || "") === String(incoming.rawContent || "");
1518
- const withinWindow = now - (prev.cachedAt || 0) <= CACHE_DEDUP_WINDOW_MS;
1519
-
1520
- // Dedup duplicate hook triggers for the same relay message.
1521
- if ((sameRole && sameMessageId) || (sameContent && withinWindow)) {
1522
- cache.lastActivity = now;
1523
- return cache;
1524
- }
1525
- }
1526
-
1527
- cache.messages.push(incoming);
855
+ cache.messages.push(message);
1528
856
  cache.lastActivity = Date.now();
1529
857
 
1530
858
  // Count turns (user message after assistant = new turn)
1531
- if (incoming.role === "user" && cache.messages.length > 1) {
859
+ if (message.role === "user" && cache.messages.length > 1) {
1532
860
  const prevMsg = cache.messages[cache.messages.length - 2];
1533
- if (prevMsg && isAssistantLikeRole(prevMsg.role)) {
861
+ if (prevMsg && prevMsg.role === "assistant") {
1534
862
  cache.turnCount++;
1535
863
  }
1536
864
  }
@@ -1642,15 +970,6 @@ async function flushSession(sessionId, cfg, ctx, log) {
1642
970
  if (log?.info) {
1643
971
  log.info(`[Memory Plugin] Flushing session ${sessionId}: ${messagesToSave.length} messages, ${cache.turnCount} turns`);
1644
972
  }
1645
-
1646
- if (cfg.useV2Protocol !== false) {
1647
- const contextBlocks = collectContextBlocks(cache.messages, cfg);
1648
- if (contextBlocks.length > 0) {
1649
- await addContextV2(contextBlocks, cfg, ctx, log, sessionId);
1650
- return;
1651
- }
1652
- }
1653
-
1654
973
  await addMemory(messagesToSave, cfg, ctx, log, sessionId);
1655
974
  } catch (error) {
1656
975
  if (log?.warn) {
@@ -1702,14 +1021,8 @@ function registerPlugin(api) {
1702
1021
  const sessionId = resolveSessionId(ctx, event) || `session-${Date.now()}`;
1703
1022
  const userContent = normalizeUserMessageContent(prompt);
1704
1023
  const rawUserContent = stripPrependedPrompt(prompt);
1705
- const messageId = extractRelayMessageId(rawUserContent || prompt);
1706
1024
  if (userContent) {
1707
- addToSessionCache(sessionId, {
1708
- role: "user",
1709
- content: userContent,
1710
- rawContent: rawUserContent,
1711
- messageId: messageId || undefined,
1712
- });
1025
+ addToSessionCache(sessionId, { role: "user", content: userContent, rawContent: rawUserContent });
1713
1026
  }
1714
1027
 
1715
1028
  try {
@@ -1757,28 +1070,22 @@ function registerPlugin(api) {
1757
1070
  return;
1758
1071
  }
1759
1072
 
1760
- if (event?.messages?.length) {
1761
- const pending = [];
1762
- for (let i = event.messages.length - 1; i >= 0; i--) {
1763
- const role = event.messages[i].role;
1764
- if (role === "system") continue;
1765
- if (role === "user") break;
1766
-
1767
- const cacheMessage = buildCacheMessageFromTranscript(event.messages[i]);
1768
- if (cacheMessage) pending.unshift(cacheMessage);
1073
+ const assistantContent = event?.response || event?.result;
1074
+ if (assistantContent) {
1075
+ const content = normalizeAssistantMessageContent(assistantContent);
1076
+ const rawContent = extractText(assistantContent);
1077
+ if (content) {
1078
+ addToSessionCache(sessionId, { role: "assistant", content, rawContent: rawContent || undefined });
1769
1079
  }
1770
- for (const cacheMessage of pending) {
1771
- addToSessionCache(sessionId, cacheMessage);
1772
- }
1773
- } else {
1774
- const assistantContent = event?.response || event?.result;
1775
- if (assistantContent) {
1776
- const cacheMessage = buildCacheMessageFromTranscript({
1777
- role: "assistant",
1778
- content: assistantContent,
1779
- });
1780
- if (cacheMessage) {
1781
- addToSessionCache(sessionId, cacheMessage);
1080
+ } else if (event?.messages?.length) {
1081
+ for (let i = event.messages.length - 1; i >= 0; i--) {
1082
+ if (event.messages[i].role === "assistant") {
1083
+ const content = normalizeAssistantMessageContent(event.messages[i].content);
1084
+ const rawContent = extractText(event.messages[i].content);
1085
+ if (content) {
1086
+ addToSessionCache(sessionId, { role: "assistant", content, rawContent: rawContent || undefined });
1087
+ }
1088
+ break;
1782
1089
  }
1783
1090
  }
1784
1091
  }
@@ -1846,14 +1153,8 @@ function createHooksPlugin(config) {
1846
1153
  const sessionId = resolveSessionId(ctx, event) || `session-${Date.now()}`;
1847
1154
  const userContent = normalizeUserMessageContent(prompt);
1848
1155
  const rawUserContent = stripPrependedPrompt(prompt);
1849
- const messageId = extractRelayMessageId(rawUserContent || prompt);
1850
1156
  if (userContent) {
1851
- addToSessionCache(sessionId, {
1852
- role: "user",
1853
- content: userContent,
1854
- rawContent: rawUserContent,
1855
- messageId: messageId || undefined,
1856
- });
1157
+ addToSessionCache(sessionId, { role: "user", content: userContent, rawContent: rawUserContent });
1857
1158
  }
1858
1159
 
1859
1160
  try {
@@ -1918,37 +1219,37 @@ function createHooksPlugin(config) {
1918
1219
  const cache = getSessionCache(sessionId);
1919
1220
  if (cache.messages.length === 0) {
1920
1221
  for (const msg of event.messages) {
1921
- const cacheMessage = buildCacheMessageFromTranscript(msg);
1922
- if (cacheMessage) {
1923
- addToSessionCache(sessionId, cacheMessage);
1222
+ if (msg.role === "system") continue;
1223
+ const content = msg.role === "user"
1224
+ ? normalizeUserMessageContent(msg.content)
1225
+ : normalizeAssistantMessageContent(msg.content);
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 });
1924
1231
  }
1925
1232
  }
1926
1233
  } else {
1927
- // Add trailing assistant/tool messages from the latest completion.
1928
- const pending = [];
1234
+ // Only add last assistant message
1929
1235
  for (let i = event.messages.length - 1; i >= 0; i--) {
1930
- const msg = event.messages[i];
1931
- if (msg.role === "system") continue;
1932
- if (msg.role === "user") break;
1933
-
1934
- const cacheMessage = buildCacheMessageFromTranscript(msg);
1935
- if (cacheMessage) {
1936
- pending.unshift(cacheMessage);
1236
+ if (event.messages[i].role === "assistant") {
1237
+ const content = normalizeAssistantMessageContent(event.messages[i].content);
1238
+ const rawContent = extractText(event.messages[i].content);
1239
+ if (content) {
1240
+ addToSessionCache(sessionId, { role: "assistant", content, rawContent: rawContent || undefined });
1241
+ }
1242
+ break;
1937
1243
  }
1938
1244
  }
1939
- for (const cacheMessage of pending) {
1940
- addToSessionCache(sessionId, cacheMessage);
1941
- }
1942
1245
  }
1943
1246
  } else {
1944
1247
  const assistantContent = event?.response || event?.result;
1945
1248
  if (assistantContent) {
1946
- const cacheMessage = buildCacheMessageFromTranscript({
1947
- role: "assistant",
1948
- content: assistantContent,
1949
- });
1950
- if (cacheMessage) {
1951
- addToSessionCache(sessionId, cacheMessage);
1249
+ const content = normalizeAssistantMessageContent(assistantContent);
1250
+ const rawContent = extractText(assistantContent);
1251
+ if (content) {
1252
+ addToSessionCache(sessionId, { role: "assistant", content, rawContent: rawContent || undefined });
1952
1253
  }
1953
1254
  }
1954
1255
  }
@@ -2010,7 +1311,7 @@ function buildConfig(config) {
2010
1311
  const configuredUserId = config?.userId || process.env.HUMAN_LIKE_MEM_USER_ID;
2011
1312
 
2012
1313
  return {
2013
- 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",
2014
1315
  apiKey: config?.apiKey || process.env.HUMAN_LIKE_MEM_API_KEY,
2015
1316
  configuredUserId: configuredUserId,
2016
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.13",
6
+ "version": "0.3.15",
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.13",
3
+ "version": "0.3.15",
4
4
  "description": "Long-term memory plugin for OpenClaw - AI Social Memory Integration",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -52,5 +52,5 @@
52
52
  },
53
53
  "publishConfig": {
54
54
  "access": "public"
55
- }
56
- }
55
+ }
56
+ }