@memtensor/memos-local-openclaw-plugin 1.0.7 → 1.0.8-beta.2

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/.env.example CHANGED
@@ -18,6 +18,10 @@ SUMMARIZER_TEMPERATURE=0
18
18
  # Port for the web-based Memory Viewer (default: 18799)
19
19
  # VIEWER_PORT=18799
20
20
 
21
+ # ─── Tavily Search (optional) ───
22
+ # API key for Tavily web search (get from https://app.tavily.com)
23
+ # TAVILY_API_KEY=tvly-your-tavily-api-key
24
+
21
25
  # ─── Telemetry (opt-out) ───
22
26
  # Anonymous usage analytics to help improve the plugin.
23
27
  # No memory content, queries, or personal data is ever sent — only tool names, latencies, and version info.
package/index.ts CHANGED
@@ -53,6 +53,45 @@ function deduplicateHits<T extends { summary: string }>(hits: T[]): T[] {
53
53
  return kept;
54
54
  }
55
55
 
56
+ const NEW_SESSION_PROMPT_RE = /A new session was started via \/new or \/reset\./i;
57
+ const INTERNAL_CONTEXT_RE = /OpenClaw runtime context \(internal\):[\s\S]*/i;
58
+ const CONTINUE_PROMPT_RE = /^Continue where you left off\.[\s\S]*/i;
59
+
60
+ function normalizeAutoRecallQuery(rawPrompt: string): string {
61
+ let query = rawPrompt.trim();
62
+
63
+ const senderTag = "Sender (untrusted metadata):";
64
+ const senderPos = query.indexOf(senderTag);
65
+ if (senderPos !== -1) {
66
+ const afterSender = query.slice(senderPos);
67
+ const fenceStart = afterSender.indexOf("```json");
68
+ const fenceEnd = fenceStart >= 0 ? afterSender.indexOf("```\n", fenceStart + 7) : -1;
69
+ if (fenceEnd > 0) {
70
+ query = afterSender.slice(fenceEnd + 4).replace(/^\s*\n/, "").trim();
71
+ } else {
72
+ const firstDblNl = afterSender.indexOf("\n\n");
73
+ if (firstDblNl > 0) {
74
+ query = afterSender.slice(firstDblNl + 2).trim();
75
+ }
76
+ }
77
+ }
78
+
79
+ query = stripInboundMetadata(query);
80
+ query = query.replace(/<[^>]+>/g, "").trim();
81
+
82
+ if (NEW_SESSION_PROMPT_RE.test(query)) {
83
+ query = query.replace(NEW_SESSION_PROMPT_RE, "").trim();
84
+ query = query.replace(/^(Execute|Run) your Session Startup sequence[^\n]*\n?/im, "").trim();
85
+ query = query.replace(/^Current time:[^\n]*(\n|$)/im, "").trim();
86
+ }
87
+
88
+ query = query.replace(INTERNAL_CONTEXT_RE, "").trim();
89
+ query = query.replace(CONTINUE_PROMPT_RE, "").trim();
90
+
91
+ return query;
92
+ }
93
+
94
+
56
95
  const pluginConfigSchema = {
57
96
  type: "object" as const,
58
97
  additionalProperties: true,
@@ -278,7 +317,7 @@ const memosLocalPlugin = {
278
317
  const raw = fs.readFileSync(openclawJsonPath, "utf-8");
279
318
  const cfg = JSON.parse(raw);
280
319
  const allow: string[] | undefined = cfg?.tools?.allow;
281
- if (Array.isArray(allow) && allow.length > 0 && !allow.includes("group:plugins")) {
320
+ if (Array.isArray(allow) && allow.length > 0 && !allow.includes("group:plugins") && !allow.includes("*")) {
282
321
  const lastEntry = JSON.stringify(allow[allow.length - 1]);
283
322
  const patched = raw.replace(
284
323
  new RegExp(`(${lastEntry})(\\s*\\])`),
@@ -307,6 +346,7 @@ const memosLocalPlugin = {
307
346
  // Current agent ID — updated by hooks, read by tools for owner isolation.
308
347
  // Falls back to "main" when no hook has fired yet (single-agent setups).
309
348
  let currentAgentId = "main";
349
+ const getCurrentOwner = () => `agent:${currentAgentId}`;
310
350
 
311
351
  // ─── Check allowPromptInjection policy ───
312
352
  // When allowPromptInjection=false, the prompt mutation fields (such as prependContext) in the hook return value
@@ -354,7 +394,6 @@ const memosLocalPlugin = {
354
394
  }
355
395
  };
356
396
 
357
- const getCurrentOwner = () => `agent:${currentAgentId}`;
358
397
  const resolveMemorySearchScope = (scope?: string): "local" | "group" | "all" =>
359
398
  scope === "group" || scope === "all" ? scope : "local";
360
399
  const resolveMemoryShareTarget = (target?: string): "agents" | "hub" | "both" =>
@@ -445,7 +484,7 @@ const memosLocalPlugin = {
445
484
  // ─── Tool: memory_search ───
446
485
 
447
486
  api.registerTool(
448
- {
487
+ (context) => ({
449
488
  name: "memory_search",
450
489
  label: "Memory Search",
451
490
  description:
@@ -461,7 +500,7 @@ const memosLocalPlugin = {
461
500
  hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for group/all search." })),
462
501
  userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for group/all search." })),
463
502
  }),
464
- execute: trackTool("memory_search", async (_toolCallId: any, params: any, context?: any) => {
503
+ execute: trackTool("memory_search", async (_toolCallId: any, params: any) => {
465
504
  const {
466
505
  query,
467
506
  scope: rawScope,
@@ -482,9 +521,6 @@ const memosLocalPlugin = {
482
521
  const role = rawRole === "user" || rawRole === "assistant" || rawRole === "tool" || rawRole === "system" ? rawRole : undefined;
483
522
  const minScore = typeof rawMinScore === "number" ? Math.max(0.35, Math.min(1, rawMinScore)) : undefined;
484
523
  let searchScope = resolveMemorySearchScope(rawScope);
485
- if (searchScope === "local" && ctx.config?.sharing?.enabled) {
486
- searchScope = "all";
487
- }
488
524
  const searchLimit = typeof maxResults === "number" ? Math.max(1, Math.min(20, Math.round(maxResults))) : 10;
489
525
 
490
526
  const agentId = context?.agentId ?? currentAgentId;
@@ -504,7 +540,7 @@ const memosLocalPlugin = {
504
540
 
505
541
  // Split local results: pure-local vs hub-memory (Hub role's hub_memories mixed in by RecallEngine)
506
542
  const localHits = result.hits.filter((h) => h.origin !== "hub-memory");
507
- const hubLocalHits = result.hits.filter((h) => h.origin === "hub-memory");
543
+ const hubLocalHits = searchScope !== "local" ? result.hits.filter((h) => h.origin === "hub-memory") : [];
508
544
 
509
545
  const rawLocalCandidates = localHits.map((h) => ({
510
546
  chunkId: h.ref.chunkId,
@@ -669,14 +705,14 @@ const memosLocalPlugin = {
669
705
  },
670
706
  };
671
707
  }),
672
- },
708
+ }),
673
709
  { name: "memory_search" },
674
710
  );
675
711
 
676
712
  // ─── Tool: memory_timeline ───
677
713
 
678
714
  api.registerTool(
679
- {
715
+ (context) => ({
680
716
  name: "memory_timeline",
681
717
  label: "Memory Timeline",
682
718
  description:
@@ -686,7 +722,7 @@ const memosLocalPlugin = {
686
722
  chunkId: Type.String({ description: "The chunkId from a memory_search hit" }),
687
723
  window: Type.Optional(Type.Number({ description: "Context window ±N (default 2)" })),
688
724
  }),
689
- execute: trackTool("memory_timeline", async (_toolCallId: any, params: any, context?: any) => {
725
+ execute: trackTool("memory_timeline", async (_toolCallId: any, params: any) => {
690
726
  const agentId = context?.agentId ?? currentAgentId;
691
727
  ctx.log.debug(`memory_timeline called (agent=${agentId})`);
692
728
  const { chunkId, window: win } = params as {
@@ -730,14 +766,14 @@ const memosLocalPlugin = {
730
766
  details: { entries, anchorRef: { sessionKey: anchorChunk.sessionKey, chunkId, turnId: anchorChunk.turnId, seq: anchorChunk.seq } },
731
767
  };
732
768
  }),
733
- },
769
+ }),
734
770
  { name: "memory_timeline" },
735
771
  );
736
772
 
737
773
  // ─── Tool: memory_get ───
738
774
 
739
775
  api.registerTool(
740
- {
776
+ (context) => ({
741
777
  name: "memory_get",
742
778
  label: "Memory Get",
743
779
  description:
@@ -748,7 +784,7 @@ const memosLocalPlugin = {
748
784
  Type.Number({ description: `Max chars (default ${DEFAULTS.getMaxCharsDefault}, max ${DEFAULTS.getMaxCharsMax})` }),
749
785
  ),
750
786
  }),
751
- execute: trackTool("memory_get", async (_toolCallId: any, params: any, context?: any) => {
787
+ execute: trackTool("memory_get", async (_toolCallId: any, params: any) => {
752
788
  const { chunkId, maxChars } = params as { chunkId: string; maxChars?: number };
753
789
  const limit = Math.min(maxChars ?? DEFAULTS.getMaxCharsDefault, DEFAULTS.getMaxCharsMax);
754
790
 
@@ -774,7 +810,7 @@ const memosLocalPlugin = {
774
810
  },
775
811
  };
776
812
  }),
777
- },
813
+ }),
778
814
  { name: "memory_get" },
779
815
  );
780
816
 
@@ -1115,6 +1151,10 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1115
1151
  };
1116
1152
  }
1117
1153
 
1154
+ const disabledWarning = skill.status === "archived"
1155
+ ? "\n\n> **Warning:** This skill is currently **disabled** (archived). Its content is shown for reference only — it will not be used in search or auto-recall.\n\n"
1156
+ : "";
1157
+
1118
1158
  const manifest = skillInstaller.getCompanionManifest(resolvedSkillId);
1119
1159
  let footer = "\n\n---\n";
1120
1160
 
@@ -1139,7 +1179,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1139
1179
  return {
1140
1180
  content: [{
1141
1181
  type: "text",
1142
- text: `## Skill: ${skill.name} (v${skill.version})\n\n${sv.content}${footer}`,
1182
+ text: `## Skill: ${skill.name} (v${skill.version})${disabledWarning}\n\n${sv.content}${footer}`,
1143
1183
  }],
1144
1184
  details: {
1145
1185
  skillId: skill.id,
@@ -1296,7 +1336,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1296
1336
  const viewerPort = (pluginCfg as any).viewerPort ?? (gatewayPort + 10);
1297
1337
 
1298
1338
  api.registerTool(
1299
- {
1339
+ (context) => ({
1300
1340
  name: "memory_viewer",
1301
1341
  label: "Open Memory Viewer",
1302
1342
  description:
@@ -1304,10 +1344,11 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1304
1344
  "or access their stored memories, or asks where the memory dashboard is. " +
1305
1345
  "Returns the URL the user can open in their browser.",
1306
1346
  parameters: Type.Object({}),
1307
- execute: trackTool("memory_viewer", async () => {
1347
+ execute: trackTool("memory_viewer", async (_toolCallId: any, params: any) => {
1308
1348
  ctx.log.debug(`memory_viewer called`);
1309
1349
  telemetry.trackViewerOpened();
1310
- const url = `http://127.0.0.1:${viewerPort}`;
1350
+ const agentId = context?.agentId ?? context?.profileId ?? currentAgentId;
1351
+ const url = `http://127.0.0.1:${viewerPort}?agentId=${encodeURIComponent(agentId)}`;
1311
1352
  return {
1312
1353
  content: [
1313
1354
  {
@@ -1328,7 +1369,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1328
1369
  details: { viewerUrl: url },
1329
1370
  };
1330
1371
  }),
1331
- },
1372
+ }),
1332
1373
  { name: "memory_viewer" },
1333
1374
  );
1334
1375
 
@@ -1401,9 +1442,11 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1401
1442
  name: "memory_share",
1402
1443
  label: "Share Memory",
1403
1444
  description:
1404
- "Share an existing memory either with local OpenClaw agents, to the Hub team, or to both targets. " +
1405
- "Use this only for an existing chunkId. Use target='agents' for local multi-agent sharing, target='hub' for team sharing, or target='both' for both. " +
1406
- "If you need to create a brand new shared memory instead of exposing an existing one, use memory_write_public.",
1445
+ "Share an existing stored memory (requires a real chunkId from the database) to the Hub team, or to both targets. " +
1446
+ "If you want to share content from the conversation, please first retrieve the memories related to that content to obtain the correct chunkId(s), then proceed with the sharing. " +
1447
+ "target='agents' (default): when retrieved memories would clearly help other agents in the same OpenClaw workspace, you may share proactively without asking the user. " +
1448
+ "target='hub' or 'both': do not share to the team Hub without explicit user consent when the content would benefit collaborators—explain briefly, ask first, and only call hub/both after they agree (Hub must be configured). " +
1449
+ "To create a brand-new shared note with no existing chunk, use memory_write_public.",
1407
1450
  parameters: Type.Object({
1408
1451
  chunkId: Type.String({ description: "Existing local memory chunk ID to share" }),
1409
1452
  target: Type.Optional(Type.String({ description: "Share target: 'agents' (default), 'hub', or 'both'" })),
@@ -1557,7 +1600,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1557
1600
  // ─── Tool: skill_search ───
1558
1601
 
1559
1602
  api.registerTool(
1560
- {
1603
+ (context) => ({
1561
1604
  name: "skill_search",
1562
1605
  label: "Skill Search",
1563
1606
  description:
@@ -1567,10 +1610,11 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1567
1610
  query: Type.String({ description: "Natural language description of the needed skill" }),
1568
1611
  scope: Type.Optional(Type.String({ description: "Search scope: 'mix' (default), 'self', 'public', 'group', or 'all'." })),
1569
1612
  }),
1570
- execute: trackTool("skill_search", async (_toolCallId: any, params: any, context?: any) => {
1613
+ execute: trackTool("skill_search", async (_toolCallId: any, params: any) => {
1571
1614
  const { query: skillQuery, scope: rawScope } = params as { query: string; scope?: string };
1572
1615
  const scope = (rawScope === "self" || rawScope === "public") ? rawScope : "mix";
1573
- const currentOwner = getCurrentOwner();
1616
+ const agentId = context?.agentId ?? currentAgentId;
1617
+ const currentOwner = `agent:${agentId}`;
1574
1618
 
1575
1619
  if (rawScope === "group" || rawScope === "all") {
1576
1620
  const [localHits, hub] = await Promise.all([
@@ -1632,7 +1676,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1632
1676
  details: { query: skillQuery, scope, hits },
1633
1677
  };
1634
1678
  }),
1635
- },
1679
+ }),
1636
1680
  { name: "skill_search" },
1637
1681
  );
1638
1682
 
@@ -1777,7 +1821,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1777
1821
  if (!allowPromptInjection) return {};
1778
1822
  if (!event.prompt || event.prompt.length < 3) return;
1779
1823
 
1780
- const recallAgentId = hookCtx?.agentId ?? "main";
1824
+ const recallAgentId = hookCtx?.agentId ?? (event as any)?.agentId ?? (event as any)?.profileId ?? "main";
1781
1825
  currentAgentId = recallAgentId;
1782
1826
  const recallOwnerFilter = [`agent:${recallAgentId}`, "public"];
1783
1827
  ctx.log.info(`auto-recall: agentId=${recallAgentId} (from hookCtx)`);
@@ -1789,24 +1833,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1789
1833
  const rawPrompt = event.prompt;
1790
1834
  ctx.log.debug(`auto-recall: rawPrompt="${rawPrompt.slice(0, 300)}"`);
1791
1835
 
1792
- let query = rawPrompt;
1793
- const senderTag = "Sender (untrusted metadata):";
1794
- const senderPos = rawPrompt.indexOf(senderTag);
1795
- if (senderPos !== -1) {
1796
- const afterSender = rawPrompt.slice(senderPos);
1797
- const fenceStart = afterSender.indexOf("```json");
1798
- const fenceEnd = fenceStart >= 0 ? afterSender.indexOf("```\n", fenceStart + 7) : -1;
1799
- if (fenceEnd > 0) {
1800
- query = afterSender.slice(fenceEnd + 4).replace(/^\s*\n/, "").trim();
1801
- } else {
1802
- const firstDblNl = afterSender.indexOf("\n\n");
1803
- if (firstDblNl > 0) {
1804
- query = afterSender.slice(firstDblNl + 2).trim();
1805
- }
1806
- }
1807
- }
1808
- query = stripInboundMetadata(query);
1809
- query = query.replace(/<[^>]+>/g, "").trim();
1836
+ const query = normalizeAutoRecallQuery(rawPrompt);
1810
1837
  recallQuery = query;
1811
1838
 
1812
1839
  if (query.length < 2) {
@@ -1988,7 +2015,6 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1988
2015
  try {
1989
2016
  const skillCandidateMap = new Map<string, { name: string; description: string; skillId: string; source: string }>();
1990
2017
 
1991
- // Source 1: direct skill search based on user query
1992
2018
  try {
1993
2019
  const directSkillHits = await engine.searchSkills(query, "mix" as any, getCurrentOwner());
1994
2020
  for (const sh of directSkillHits.slice(0, skillLimit + 2)) {
@@ -2000,7 +2026,6 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
2000
2026
  ctx.log.debug(`auto-recall-skill: direct search failed: ${err}`);
2001
2027
  }
2002
2028
 
2003
- // Source 2: skills linked to tasks from memory hits
2004
2029
  const taskIds = new Set<string>();
2005
2030
  for (const h of filteredHits) {
2006
2031
  if (h.taskId) {
@@ -2090,7 +2115,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
2090
2115
  if (!event.success || !event.messages || event.messages.length === 0) return;
2091
2116
 
2092
2117
  try {
2093
- const captureAgentId = hookCtx?.agentId ?? "main";
2118
+ const captureAgentId = hookCtx?.agentId ?? event?.agentId ?? event?.profileId ?? "main";
2094
2119
  currentAgentId = captureAgentId;
2095
2120
  const captureOwner = `agent:${captureAgentId}`;
2096
2121
  const sessionKey = hookCtx?.sessionKey ?? "default";
@@ -3,7 +3,7 @@
3
3
  "name": "MemOS Local Memory",
4
4
  "description": "Full-write local conversation memory with hybrid search (RRF + MMR + recency), task summarization, skill evolution, and team sharing (Hub-Client). Provides memory_search, memory_get, task_summary, skill_search, task_share, network_skill_pull, network_team_info, memory_viewer for layered retrieval and team collaboration.",
5
5
  "kind": "memory",
6
- "version": "1.0.6-beta.11",
6
+ "version": "1.0.8-beta.2",
7
7
  "skills": [
8
8
  "skill/memos-memory-guide"
9
9
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memtensor/memos-local-openclaw-plugin",
3
- "version": "1.0.7",
3
+ "version": "1.0.8-beta.2",
4
4
  "description": "MemOS Local memory plugin for OpenClaw — full-write, hybrid-recall, progressive retrieval",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -45,12 +45,13 @@
45
45
  ],
46
46
  "license": "MIT",
47
47
  "engines": {
48
- "node": ">=22.0.0"
48
+ "node": ">=18.0.0 <25.0.0"
49
49
  },
50
50
  "dependencies": {
51
51
  "@huggingface/transformers": "^3.8.0",
52
52
  "@sinclair/typebox": "^0.34.48",
53
- "better-sqlite3": "^12.6.2",
53
+ "better-sqlite3": "^12.6.3",
54
+ "posthog-node": "^5.28.0",
54
55
  "puppeteer": "^24.38.0",
55
56
  "semver": "^7.7.4",
56
57
  "uuid": "^10.0.0"
@@ -30,6 +30,9 @@ function normalizePathForMatch(p) {
30
30
  return path.resolve(p).replace(/^\\\\\?\\/, "").replace(/\\/g, "/").toLowerCase();
31
31
  }
32
32
 
33
+ const nodeVersion = process.version;
34
+ const nodeMajor = parseInt(nodeVersion.slice(1).split('.')[0], 10);
35
+
33
36
  console.log(`
34
37
  ${CYAN}${BOLD}┌──────────────────────────────────────────────────┐
35
38
  │ MemOS Local Memory — postinstall setup │
@@ -37,7 +40,13 @@ ${CYAN}${BOLD}┌─────────────────────
37
40
  `);
38
41
 
39
42
  log(`Plugin dir: ${DIM}${pluginDir}${RESET}`);
40
- log(`Node: ${process.version} Platform: ${process.platform}-${process.arch}`);
43
+ log(`Node: ${GREEN}${nodeVersion}${RESET} Platform: ${process.platform}-${process.arch}`);
44
+
45
+ if (nodeMajor >= 25) {
46
+ warn(`Node.js ${nodeVersion} detected. This version may have compatibility issues with native modules.`);
47
+ log(`Recommended: Use Node.js LTS (v20 or v22) for best compatibility.`);
48
+ log(`You can use nvm to switch versions: ${CYAN}nvm use 22${RESET}`);
49
+ }
41
50
 
42
51
  /* ═══════════════════════════════════════════════════════════
43
52
  * Pre-phase: Clean stale build artifacts on upgrade
@@ -61,21 +70,30 @@ function cleanStaleArtifacts() {
61
70
  installedVer = pkg.version || "unknown";
62
71
  } catch { /* ignore */ }
63
72
 
73
+ const nodeMajor = process.versions.node.split(".")[0];
74
+ const currentFingerprint = `${installedVer}+node${nodeMajor}`;
75
+
64
76
  const markerPath = path.join(pluginDir, ".installed-version");
65
- let prevVer = "";
66
- try { prevVer = fs.readFileSync(markerPath, "utf-8").trim(); } catch { /* first install */ }
77
+ let prevFingerprint = "";
78
+ try { prevFingerprint = fs.readFileSync(markerPath, "utf-8").trim(); } catch { /* first install */ }
79
+
80
+ const writeMarker = () => {
81
+ try { fs.writeFileSync(markerPath, currentFingerprint + "\n", "utf-8"); } catch { /* ignore */ }
82
+ };
67
83
 
68
- if (prevVer === installedVer) {
69
- log(`Version unchanged (${installedVer}), skipping artifact cleanup.`);
84
+ if (prevFingerprint === currentFingerprint) {
85
+ log(`Version unchanged (${currentFingerprint}), skipping artifact cleanup.`);
70
86
  return;
71
87
  }
72
88
 
73
- if (prevVer) {
74
- log(`Upgrade detected: ${DIM}${prevVer}${RESET} → ${GREEN}${installedVer}${RESET}`);
75
- } else {
76
- log(`Fresh install: ${GREEN}${installedVer}${RESET}`);
89
+ if (!prevFingerprint) {
90
+ log(`Fresh install: ${GREEN}${currentFingerprint}${RESET}`);
91
+ writeMarker();
92
+ return;
77
93
  }
78
94
 
95
+ log(`Environment changed: ${DIM}${prevFingerprint}${RESET} → ${GREEN}${currentFingerprint}${RESET}`);
96
+
79
97
  const dirsToClean = ["dist", "node_modules"];
80
98
  let cleaned = 0;
81
99
  for (const dir of dirsToClean) {
@@ -99,7 +117,7 @@ function cleanStaleArtifacts() {
99
117
  }
100
118
  }
101
119
 
102
- try { fs.writeFileSync(markerPath, installedVer + "\n", "utf-8"); } catch { /* ignore */ }
120
+ writeMarker();
103
121
 
104
122
  if (cleaned > 0) {
105
123
  ok(`Cleaned ${cleaned} stale artifact(s). Fresh install will follow.`);
@@ -418,23 +436,39 @@ if (sqliteBindingsExist()) {
418
436
  else { fail(`Rebuild completed but bindings still missing (${elapsed}s).`); fail(`Looked in: ${sqliteModulePath}/build/`); }
419
437
  console.log(`
420
438
  ${YELLOW}${BOLD} ╔══════════════════════════════════════════════════════════════╗
421
- ║ ✖ better-sqlite3 native module build failed
439
+ ║ ✖ better-sqlite3 native module build failed
422
440
  ╠══════════════════════════════════════════════════════════════╣${RESET}
423
- ${YELLOW} ║${RESET} ${YELLOW}║${RESET}
424
- ${YELLOW} ║${RESET} This plugin requires C/C++ build tools to compile ${YELLOW}║${RESET}
425
- ${YELLOW} ║${RESET} the SQLite native module on first install. ${YELLOW}║${RESET}
426
- ${YELLOW} ║${RESET} ${YELLOW}║${RESET}
427
- ${YELLOW} ║${RESET} ${BOLD}Install build tools:${RESET} ${YELLOW}║${RESET}
428
- ${YELLOW} ║${RESET} ${YELLOW}║${RESET}
429
- ${YELLOW} ║${RESET} ${CYAN}macOS:${RESET} xcode-select --install ${YELLOW}║${RESET}
430
- ${YELLOW} ║${RESET} ${CYAN}Ubuntu:${RESET} sudo apt install build-essential python3 ${YELLOW}║${RESET}
431
- ${YELLOW} ║${RESET} ${CYAN}Windows:${RESET} npm install -g windows-build-tools ${YELLOW}║${RESET}
432
- ${YELLOW} ║${RESET} ${YELLOW}║${RESET}
433
- ${YELLOW} ║${RESET} ${BOLD}Then retry:${RESET} ${YELLOW}║${RESET}
441
+ ${YELLOW} ║${RESET} ${YELLOW}║${RESET}
442
+ ${YELLOW} ║${RESET} This plugin requires C/C++ build tools to compile ${YELLOW}║${RESET}
443
+ ${YELLOW} ║${RESET} the SQLite native module on first install. ${YELLOW}║${RESET}
444
+ ${YELLOW} ║${RESET} ${YELLOW}║${RESET}
445
+ ${YELLOW} ║${RESET} ${BOLD}Install build tools:${RESET} ${YELLOW}║${RESET}
446
+ ${YELLOW} ║${RESET} ${YELLOW}║${RESET}
447
+ ${YELLOW} ║${RESET} ${CYAN}macOS:${RESET} xcode-select --install ${YELLOW}║${RESET}
448
+ ${YELLOW} ║${RESET} ${CYAN}Ubuntu:${RESET} sudo apt install build-essential python3 ${YELLOW}║${RESET}
449
+ ${YELLOW} ║${RESET} ${CYAN}Windows:${RESET} npm install -g windows-build-tools ${YELLOW}║${RESET}
450
+ ${YELLOW} ║${RESET} ${YELLOW}║${RESET}`);
451
+
452
+ if (nodeMajor >= 25) {
453
+ console.log(`${YELLOW} ║${RESET} ${BOLD}${RED}Node.js v25+ compatibility issue detected:${RESET} ${YELLOW}║${RESET}
454
+ ${YELLOW} ║${RESET} ${YELLOW}║${RESET}
455
+ ${YELLOW} ║${RESET} better-sqlite3 may not have prebuilt binaries for Node 25. ${YELLOW}║${RESET}
456
+ ${YELLOW} ║${RESET} ${BOLD}Recommended solutions:${RESET} ${YELLOW}║${RESET}
457
+ ${YELLOW} ║${RESET} ${YELLOW}║${RESET}
458
+ ${YELLOW} ║${RESET} 1. Use Node.js LTS (v20 or v22): ${YELLOW}║${RESET}
459
+ ${YELLOW} ║${RESET} ${GREEN}nvm install 22 && nvm use 22${RESET} ${YELLOW}║${RESET}
460
+ ${YELLOW} ║${RESET} ${YELLOW}║${RESET}
461
+ ${YELLOW} ║${RESET} 2. Or use MemOS Cloud version instead: ${YELLOW}║${RESET}
462
+ ${YELLOW} ║${RESET} ${CYAN}https://github.com/MemTensor/MemOS/tree/main/apps/memos-cloud${RESET}
463
+ ${YELLOW} ║${RESET} ${YELLOW}║${RESET}`);
464
+ }
465
+
466
+ console.log(`${YELLOW} ║${RESET} ${YELLOW}║${RESET}
467
+ ${YELLOW} ║${RESET} ${BOLD}Then retry:${RESET} ${YELLOW}║${RESET}
434
468
  ${YELLOW} ║${RESET} ${GREEN}cd ${pluginDir}${RESET}
435
- ${YELLOW} ║${RESET} ${GREEN}npm rebuild better-sqlite3${RESET} ${YELLOW}║${RESET}
436
- ${YELLOW} ║${RESET} ${GREEN}openclaw gateway stop && openclaw gateway start${RESET} ${YELLOW}║${RESET}
437
- ${YELLOW} ║${RESET} ${YELLOW}║${RESET}
469
+ ${YELLOW} ║${RESET} ${GREEN}npm rebuild better-sqlite3${RESET} ${YELLOW}║${RESET}
470
+ ${YELLOW} ║${RESET} ${GREEN}openclaw gateway stop && openclaw gateway start${RESET} ${YELLOW}║${RESET}
471
+ ${YELLOW} ║${RESET} ${YELLOW}║${RESET}
438
472
  ${YELLOW}${BOLD} ╚══════════════════════════════════════════════════════════════╝${RESET}
439
473
  `);
440
474
  }
@@ -73,8 +73,11 @@ Two sharing planes exist and must not be confused:
73
73
  ### memory_share
74
74
 
75
75
  - **What it does:** Share an existing memory either with local OpenClaw agents, to the team, or to both.
76
- - **When to call:** You already have a useful memory chunk and want to expose it beyond the current agent.
77
- - **Do not use when:** You are creating a new shared note from scratch. In that case use `memory_write_public`.
76
+ - **When to call:**
77
+ - If you want to share conversation content to team or hub, first retrieve memories related to that content to obtain the right `chunkId`(s), then share.
78
+ - `target='agents'` (default): When those memories would clearly help other agents in the same workspace, you may share proactively without asking the user.
79
+ - `target='hub'` or `'both'`: Only after explicit user consent when the content would benefit collaborators—explain briefly, ask first, then call `hub`/`both` (Hub must be configured). Never silently Hub-share.
80
+ - **Do not use when:** You are creating a brand-new shared note with **no** existing chunk—use `memory_write_public` instead.
78
81
  - **Parameters:**
79
82
  - `chunkId` (string, **required**) — Existing memory chunk ID.
80
83
  - `target` (string, optional) — `'agents'` (default), `'hub'`, or `'both'`.
@@ -8,6 +8,19 @@ import { summarizeAnthropic, summarizeTaskAnthropic, generateTaskTitleAnthropic,
8
8
  import { summarizeGemini, summarizeTaskGemini, generateTaskTitleGemini, judgeNewTopicGemini, filterRelevantGemini, judgeDedupGemini } from "./gemini";
9
9
  import { summarizeBedrock, summarizeTaskBedrock, generateTaskTitleBedrock, judgeNewTopicBedrock, filterRelevantBedrock, judgeDedupBedrock } from "./bedrock";
10
10
 
11
+ /**
12
+ * Resolve a SecretInput (string | SecretRef) to a plain string.
13
+ * Supports env-sourced SecretRef from OpenClaw's credential system.
14
+ */
15
+ function resolveApiKey(
16
+ input: string | { source: string; provider?: string; id: string } | undefined,
17
+ ): string | undefined {
18
+ if (!input) return undefined;
19
+ if (typeof input === "string") return input;
20
+ if (input.source === "env") return process.env[input.id];
21
+ return undefined;
22
+ }
23
+
11
24
  /**
12
25
  * Detect provider type from provider key name or base URL.
13
26
  */
@@ -68,7 +81,7 @@ function loadOpenClawFallbackConfig(log: Logger): SummarizerConfig | undefined {
68
81
  if (!providerCfg) return undefined;
69
82
 
70
83
  const baseUrl: string | undefined = providerCfg.baseUrl;
71
- const apiKey: string | undefined = providerCfg.apiKey;
84
+ const apiKey = resolveApiKey(providerCfg.apiKey);
72
85
  if (!baseUrl || !apiKey) return undefined;
73
86
 
74
87
  const provider = detectProvider(providerKey, baseUrl);
@@ -77,7 +77,7 @@ export class RecallEngine {
77
77
  }
78
78
  const shortTerms = [...new Set([...spaceSplit, ...cjkBigrams])];
79
79
  const patternHits = shortTerms.length > 0
80
- ? this.store.patternSearch(shortTerms, { limit: candidatePool })
80
+ ? this.store.patternSearch(shortTerms, { limit: candidatePool, ownerFilter })
81
81
  : [];
82
82
  const patternRanked = patternHits.map((h, i) => ({
83
83
  id: h.chunkId,
@@ -2,6 +2,19 @@ import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import type { SummarizerConfig, SummaryProvider, Logger, PluginContext, OpenClawAPI } from "../types";
4
4
 
5
+ /**
6
+ * Resolve a SecretInput (string | SecretRef) to a plain string.
7
+ * Supports env-sourced SecretRef from OpenClaw's credential system.
8
+ */
9
+ function resolveApiKey(
10
+ input: string | { source: string; provider?: string; id: string } | undefined,
11
+ ): string | undefined {
12
+ if (!input) return undefined;
13
+ if (typeof input === "string") return input;
14
+ if (input.source === "env") return process.env[input.id];
15
+ return undefined;
16
+ }
17
+
5
18
  /**
6
19
  * Detect provider type from provider key name or base URL.
7
20
  */
@@ -56,7 +69,7 @@ export function loadOpenClawFallbackConfig(log: Logger): SummarizerConfig | unde
56
69
  if (!providerCfg) return undefined;
57
70
 
58
71
  const baseUrl: string | undefined = providerCfg.baseUrl;
59
- const apiKey: string | undefined = providerCfg.apiKey;
72
+ const apiKey = resolveApiKey(providerCfg.apiKey);
60
73
  if (!baseUrl || !apiKey) return undefined;
61
74
 
62
75
  const provider = detectProvider(providerKey, baseUrl);