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

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" =>
@@ -388,6 +427,7 @@ const memosLocalPlugin = {
388
427
  body: JSON.stringify({
389
428
  memory: {
390
429
  sourceChunkId: chunk.id,
430
+ sourceAgent: chunk.owner || "",
391
431
  role: chunk.role,
392
432
  content: chunk.content,
393
433
  summary: chunk.summary,
@@ -408,6 +448,7 @@ const memosLocalPlugin = {
408
448
  id: memoryId,
409
449
  sourceChunkId: chunk.id,
410
450
  sourceUserId: hubClient.userId,
451
+ sourceAgent: chunk.owner || "",
411
452
  role: chunk.role,
412
453
  content: chunk.content,
413
454
  summary: chunk.summary ?? "",
@@ -445,7 +486,7 @@ const memosLocalPlugin = {
445
486
  // ─── Tool: memory_search ───
446
487
 
447
488
  api.registerTool(
448
- {
489
+ (context) => ({
449
490
  name: "memory_search",
450
491
  label: "Memory Search",
451
492
  description:
@@ -461,7 +502,7 @@ const memosLocalPlugin = {
461
502
  hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for group/all search." })),
462
503
  userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for group/all search." })),
463
504
  }),
464
- execute: trackTool("memory_search", async (_toolCallId: any, params: any, context?: any) => {
505
+ execute: trackTool("memory_search", async (_toolCallId: any, params: any) => {
465
506
  const {
466
507
  query,
467
508
  scope: rawScope,
@@ -482,9 +523,6 @@ const memosLocalPlugin = {
482
523
  const role = rawRole === "user" || rawRole === "assistant" || rawRole === "tool" || rawRole === "system" ? rawRole : undefined;
483
524
  const minScore = typeof rawMinScore === "number" ? Math.max(0.35, Math.min(1, rawMinScore)) : undefined;
484
525
  let searchScope = resolveMemorySearchScope(rawScope);
485
- if (searchScope === "local" && ctx.config?.sharing?.enabled) {
486
- searchScope = "all";
487
- }
488
526
  const searchLimit = typeof maxResults === "number" ? Math.max(1, Math.min(20, Math.round(maxResults))) : 10;
489
527
 
490
528
  const agentId = context?.agentId ?? currentAgentId;
@@ -504,7 +542,7 @@ const memosLocalPlugin = {
504
542
 
505
543
  // Split local results: pure-local vs hub-memory (Hub role's hub_memories mixed in by RecallEngine)
506
544
  const localHits = result.hits.filter((h) => h.origin !== "hub-memory");
507
- const hubLocalHits = result.hits.filter((h) => h.origin === "hub-memory");
545
+ const hubLocalHits = searchScope !== "local" ? result.hits.filter((h) => h.origin === "hub-memory") : [];
508
546
 
509
547
  const rawLocalCandidates = localHits.map((h) => ({
510
548
  chunkId: h.ref.chunkId,
@@ -513,6 +551,7 @@ const memosLocalPlugin = {
513
551
  summary: h.summary,
514
552
  original_excerpt: (h.original_excerpt ?? "").slice(0, 200),
515
553
  origin: h.origin || "local",
554
+ owner: h.owner || "",
516
555
  }));
517
556
 
518
557
  // Hub remote candidates (from HTTP call) + hub-memory candidates (from RecallEngine for Hub role)
@@ -649,6 +688,7 @@ const memosLocalPlugin = {
649
688
  chunkId: h.ref.chunkId, taskId: effectiveTaskId, skillId: h.skillId,
650
689
  role: h.source.role, score: h.score, summary: h.summary,
651
690
  original_excerpt: (h.original_excerpt ?? "").slice(0, 200), origin: h.origin || "local",
691
+ owner: h.owner || "",
652
692
  };
653
693
  }),
654
694
  ...filteredHubRemoteHits.map((h: any) => ({
@@ -656,6 +696,7 @@ const memosLocalPlugin = {
656
696
  role: h.source?.role ?? h.role ?? "assistant", score: h.score ?? 0,
657
697
  summary: h.summary ?? "", original_excerpt: (h.excerpt ?? h.summary ?? "").slice(0, 200),
658
698
  origin: "hub-remote", ownerName: h.ownerName ?? "", groupName: h.groupName ?? "",
699
+ sourceAgent: h.sourceAgent ?? "",
659
700
  })),
660
701
  ];
661
702
 
@@ -669,14 +710,14 @@ const memosLocalPlugin = {
669
710
  },
670
711
  };
671
712
  }),
672
- },
713
+ }),
673
714
  { name: "memory_search" },
674
715
  );
675
716
 
676
717
  // ─── Tool: memory_timeline ───
677
718
 
678
719
  api.registerTool(
679
- {
720
+ (context) => ({
680
721
  name: "memory_timeline",
681
722
  label: "Memory Timeline",
682
723
  description:
@@ -686,7 +727,7 @@ const memosLocalPlugin = {
686
727
  chunkId: Type.String({ description: "The chunkId from a memory_search hit" }),
687
728
  window: Type.Optional(Type.Number({ description: "Context window ±N (default 2)" })),
688
729
  }),
689
- execute: trackTool("memory_timeline", async (_toolCallId: any, params: any, context?: any) => {
730
+ execute: trackTool("memory_timeline", async (_toolCallId: any, params: any) => {
690
731
  const agentId = context?.agentId ?? currentAgentId;
691
732
  ctx.log.debug(`memory_timeline called (agent=${agentId})`);
692
733
  const { chunkId, window: win } = params as {
@@ -730,14 +771,14 @@ const memosLocalPlugin = {
730
771
  details: { entries, anchorRef: { sessionKey: anchorChunk.sessionKey, chunkId, turnId: anchorChunk.turnId, seq: anchorChunk.seq } },
731
772
  };
732
773
  }),
733
- },
774
+ }),
734
775
  { name: "memory_timeline" },
735
776
  );
736
777
 
737
778
  // ─── Tool: memory_get ───
738
779
 
739
780
  api.registerTool(
740
- {
781
+ (context) => ({
741
782
  name: "memory_get",
742
783
  label: "Memory Get",
743
784
  description:
@@ -748,7 +789,7 @@ const memosLocalPlugin = {
748
789
  Type.Number({ description: `Max chars (default ${DEFAULTS.getMaxCharsDefault}, max ${DEFAULTS.getMaxCharsMax})` }),
749
790
  ),
750
791
  }),
751
- execute: trackTool("memory_get", async (_toolCallId: any, params: any, context?: any) => {
792
+ execute: trackTool("memory_get", async (_toolCallId: any, params: any) => {
752
793
  const { chunkId, maxChars } = params as { chunkId: string; maxChars?: number };
753
794
  const limit = Math.min(maxChars ?? DEFAULTS.getMaxCharsDefault, DEFAULTS.getMaxCharsMax);
754
795
 
@@ -774,7 +815,7 @@ const memosLocalPlugin = {
774
815
  },
775
816
  };
776
817
  }),
777
- },
818
+ }),
778
819
  { name: "memory_get" },
779
820
  );
780
821
 
@@ -1115,6 +1156,10 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1115
1156
  };
1116
1157
  }
1117
1158
 
1159
+ const disabledWarning = skill.status === "archived"
1160
+ ? "\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"
1161
+ : "";
1162
+
1118
1163
  const manifest = skillInstaller.getCompanionManifest(resolvedSkillId);
1119
1164
  let footer = "\n\n---\n";
1120
1165
 
@@ -1139,7 +1184,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1139
1184
  return {
1140
1185
  content: [{
1141
1186
  type: "text",
1142
- text: `## Skill: ${skill.name} (v${skill.version})\n\n${sv.content}${footer}`,
1187
+ text: `## Skill: ${skill.name} (v${skill.version})${disabledWarning}\n\n${sv.content}${footer}`,
1143
1188
  }],
1144
1189
  details: {
1145
1190
  skillId: skill.id,
@@ -1296,7 +1341,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1296
1341
  const viewerPort = (pluginCfg as any).viewerPort ?? (gatewayPort + 10);
1297
1342
 
1298
1343
  api.registerTool(
1299
- {
1344
+ (context) => ({
1300
1345
  name: "memory_viewer",
1301
1346
  label: "Open Memory Viewer",
1302
1347
  description:
@@ -1304,10 +1349,11 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1304
1349
  "or access their stored memories, or asks where the memory dashboard is. " +
1305
1350
  "Returns the URL the user can open in their browser.",
1306
1351
  parameters: Type.Object({}),
1307
- execute: trackTool("memory_viewer", async () => {
1352
+ execute: trackTool("memory_viewer", async (_toolCallId: any, params: any) => {
1308
1353
  ctx.log.debug(`memory_viewer called`);
1309
1354
  telemetry.trackViewerOpened();
1310
- const url = `http://127.0.0.1:${viewerPort}`;
1355
+ const agentId = context?.agentId ?? context?.profileId ?? currentAgentId;
1356
+ const url = `http://127.0.0.1:${viewerPort}?agentId=${encodeURIComponent(agentId)}`;
1311
1357
  return {
1312
1358
  content: [
1313
1359
  {
@@ -1328,7 +1374,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1328
1374
  details: { viewerUrl: url },
1329
1375
  };
1330
1376
  }),
1331
- },
1377
+ }),
1332
1378
  { name: "memory_viewer" },
1333
1379
  );
1334
1380
 
@@ -1401,9 +1447,11 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1401
1447
  name: "memory_share",
1402
1448
  label: "Share Memory",
1403
1449
  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.",
1450
+ "Share an existing stored memory (requires a real chunkId from the database) to the Hub team, or to both targets. " +
1451
+ "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. " +
1452
+ "target='agents' (default): when retrieved memories would clearly help other agents in the same OpenClaw workspace, you may share proactively without asking the user. " +
1453
+ "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). " +
1454
+ "To create a brand-new shared note with no existing chunk, use memory_write_public.",
1407
1455
  parameters: Type.Object({
1408
1456
  chunkId: Type.String({ description: "Existing local memory chunk ID to share" }),
1409
1457
  target: Type.Optional(Type.String({ description: "Share target: 'agents' (default), 'hub', or 'both'" })),
@@ -1557,7 +1605,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1557
1605
  // ─── Tool: skill_search ───
1558
1606
 
1559
1607
  api.registerTool(
1560
- {
1608
+ (context) => ({
1561
1609
  name: "skill_search",
1562
1610
  label: "Skill Search",
1563
1611
  description:
@@ -1567,10 +1615,11 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1567
1615
  query: Type.String({ description: "Natural language description of the needed skill" }),
1568
1616
  scope: Type.Optional(Type.String({ description: "Search scope: 'mix' (default), 'self', 'public', 'group', or 'all'." })),
1569
1617
  }),
1570
- execute: trackTool("skill_search", async (_toolCallId: any, params: any, context?: any) => {
1618
+ execute: trackTool("skill_search", async (_toolCallId: any, params: any) => {
1571
1619
  const { query: skillQuery, scope: rawScope } = params as { query: string; scope?: string };
1572
1620
  const scope = (rawScope === "self" || rawScope === "public") ? rawScope : "mix";
1573
- const currentOwner = getCurrentOwner();
1621
+ const agentId = context?.agentId ?? currentAgentId;
1622
+ const currentOwner = `agent:${agentId}`;
1574
1623
 
1575
1624
  if (rawScope === "group" || rawScope === "all") {
1576
1625
  const [localHits, hub] = await Promise.all([
@@ -1632,7 +1681,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1632
1681
  details: { query: skillQuery, scope, hits },
1633
1682
  };
1634
1683
  }),
1635
- },
1684
+ }),
1636
1685
  { name: "skill_search" },
1637
1686
  );
1638
1687
 
@@ -1777,7 +1826,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1777
1826
  if (!allowPromptInjection) return {};
1778
1827
  if (!event.prompt || event.prompt.length < 3) return;
1779
1828
 
1780
- const recallAgentId = hookCtx?.agentId ?? "main";
1829
+ const recallAgentId = hookCtx?.agentId ?? (event as any)?.agentId ?? (event as any)?.profileId ?? "main";
1781
1830
  currentAgentId = recallAgentId;
1782
1831
  const recallOwnerFilter = [`agent:${recallAgentId}`, "public"];
1783
1832
  ctx.log.info(`auto-recall: agentId=${recallAgentId} (from hookCtx)`);
@@ -1789,24 +1838,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1789
1838
  const rawPrompt = event.prompt;
1790
1839
  ctx.log.debug(`auto-recall: rawPrompt="${rawPrompt.slice(0, 300)}"`);
1791
1840
 
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();
1841
+ const query = normalizeAutoRecallQuery(rawPrompt);
1810
1842
  recallQuery = query;
1811
1843
 
1812
1844
  if (query.length < 2) {
@@ -1845,6 +1877,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1845
1877
  const rawLocalCandidates = localHits.map((h) => ({
1846
1878
  score: h.score, role: h.source.role, summary: h.summary,
1847
1879
  content: (h.original_excerpt ?? "").slice(0, 200), origin: h.origin || "local",
1880
+ owner: h.owner || "",
1848
1881
  }));
1849
1882
  const rawHubCandidates = allHubHits.map((h) => ({
1850
1883
  score: h.score, role: h.source.role, summary: h.summary,
@@ -1988,7 +2021,6 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1988
2021
  try {
1989
2022
  const skillCandidateMap = new Map<string, { name: string; description: string; skillId: string; source: string }>();
1990
2023
 
1991
- // Source 1: direct skill search based on user query
1992
2024
  try {
1993
2025
  const directSkillHits = await engine.searchSkills(query, "mix" as any, getCurrentOwner());
1994
2026
  for (const sh of directSkillHits.slice(0, skillLimit + 2)) {
@@ -2000,7 +2032,6 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
2000
2032
  ctx.log.debug(`auto-recall-skill: direct search failed: ${err}`);
2001
2033
  }
2002
2034
 
2003
- // Source 2: skills linked to tasks from memory hits
2004
2035
  const taskIds = new Set<string>();
2005
2036
  for (const h of filteredHits) {
2006
2037
  if (h.taskId) {
@@ -2054,7 +2085,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
2054
2085
  store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({
2055
2086
  candidates: rawLocalCandidates,
2056
2087
  hubCandidates: rawHubCandidates,
2057
- filtered: filteredHits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt, origin: h.origin || "local" })),
2088
+ filtered: filteredHits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt, origin: h.origin || "local", owner: h.owner || "" })),
2058
2089
  }), recallDur, true);
2059
2090
  telemetry.trackAutoRecall(filteredHits.length, recallDur);
2060
2091
 
@@ -2090,7 +2121,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
2090
2121
  if (!event.success || !event.messages || event.messages.length === 0) return;
2091
2122
 
2092
2123
  try {
2093
- const captureAgentId = hookCtx?.agentId ?? "main";
2124
+ const captureAgentId = hookCtx?.agentId ?? event?.agentId ?? event?.profileId ?? "main";
2094
2125
  currentAgentId = captureAgentId;
2095
2126
  const captureOwner = `agent:${captureAgentId}`;
2096
2127
  const sessionKey = hookCtx?.sessionKey ?? "default";
@@ -2308,48 +2339,54 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
2308
2339
 
2309
2340
  // ─── Service lifecycle ───
2310
2341
 
2311
- api.registerService({
2312
- id: "memos-local-openclaw-plugin",
2313
- start: async () => {
2314
- if (hubServer) {
2315
- const hubUrl = await hubServer.start();
2316
- api.logger.info(`memos-local: hub started at ${hubUrl}`);
2317
- }
2342
+ let serviceStarted = false;
2318
2343
 
2319
- // Auto-connect to Hub in client mode (handles both existing token and auto-join via teamToken)
2320
- if (ctx.config.sharing?.enabled && ctx.config.sharing.role === "client") {
2321
- try {
2322
- const session = await connectToHub(store, ctx.config, ctx.log);
2323
- api.logger.info(`memos-local: connected to Hub as "${session.username}" (${session.userId})`);
2324
- } catch (err) {
2325
- api.logger.warn(`memos-local: Hub connection failed: ${err}`);
2326
- }
2327
- }
2344
+ const startServiceCore = async () => {
2345
+ if (serviceStarted) return;
2346
+ serviceStarted = true;
2347
+
2348
+ if (hubServer) {
2349
+ const hubUrl = await hubServer.start();
2350
+ api.logger.info(`memos-local: hub started at ${hubUrl}`);
2351
+ }
2328
2352
 
2353
+ if (ctx.config.sharing?.enabled && ctx.config.sharing.role === "client") {
2329
2354
  try {
2330
- const viewerUrl = await viewer.start();
2331
- api.logger.info(`memos-local: started (embedding: ${embedder.provider})`);
2332
- api.logger.info(`╔══════════════════════════════════════════╗`);
2333
- api.logger.info(`║ MemOS Memory Viewer ║`);
2334
- api.logger.info(`║ → ${viewerUrl.padEnd(37)}║`);
2335
- api.logger.info(`║ Open in browser to manage memories ║`);
2336
- api.logger.info(`╚══════════════════════════════════════════╝`);
2337
- api.logger.info(`memos-local: password reset token: ${viewer.getResetToken()}`);
2338
- api.logger.info(`memos-local: forgot password? Use the reset token on the login page.`);
2339
- skillEvolver.recoverOrphanedTasks().then((count) => {
2340
- if (count > 0) api.logger.info(`memos-local: recovered ${count} orphaned skill tasks`);
2341
- }).catch((err) => {
2342
- api.logger.warn(`memos-local: skill recovery failed: ${err}`);
2343
- });
2355
+ const session = await connectToHub(store, ctx.config, ctx.log);
2356
+ api.logger.info(`memos-local: connected to Hub as "${session.username}" (${session.userId})`);
2344
2357
  } catch (err) {
2345
- api.logger.warn(`memos-local: viewer failed to start: ${err}`);
2346
- api.logger.info(`memos-local: started (embedding: ${embedder.provider})`);
2358
+ api.logger.warn(`memos-local: Hub connection failed: ${err}`);
2347
2359
  }
2348
- telemetry.trackPluginStarted(
2349
- ctx.config.embedding?.provider ?? "local",
2350
- ctx.config.summarizer?.provider ?? "none",
2351
- );
2352
- },
2360
+ }
2361
+
2362
+ try {
2363
+ const viewerUrl = await viewer.start();
2364
+ api.logger.info(`memos-local: started (embedding: ${embedder.provider})`);
2365
+ api.logger.info(`╔══════════════════════════════════════════╗`);
2366
+ api.logger.info(`║ MemOS Memory Viewer ║`);
2367
+ api.logger.info(`║ → ${viewerUrl.padEnd(37)}║`);
2368
+ api.logger.info(`║ Open in browser to manage memories ║`);
2369
+ api.logger.info(`╚══════════════════════════════════════════╝`);
2370
+ api.logger.info(`memos-local: password reset token: ${viewer.getResetToken()}`);
2371
+ api.logger.info(`memos-local: forgot password? Use the reset token on the login page.`);
2372
+ skillEvolver.recoverOrphanedTasks().then((count) => {
2373
+ if (count > 0) api.logger.info(`memos-local: recovered ${count} orphaned skill tasks`);
2374
+ }).catch((err) => {
2375
+ api.logger.warn(`memos-local: skill recovery failed: ${err}`);
2376
+ });
2377
+ } catch (err) {
2378
+ api.logger.warn(`memos-local: viewer failed to start: ${err}`);
2379
+ api.logger.info(`memos-local: started (embedding: ${embedder.provider})`);
2380
+ }
2381
+ telemetry.trackPluginStarted(
2382
+ ctx.config.embedding?.provider ?? "local",
2383
+ ctx.config.summarizer?.provider ?? "none",
2384
+ );
2385
+ };
2386
+
2387
+ api.registerService({
2388
+ id: "memos-local-openclaw-plugin",
2389
+ start: async () => { await startServiceCore(); },
2353
2390
  stop: async () => {
2354
2391
  await worker.flush();
2355
2392
  await telemetry.shutdown();
@@ -2359,6 +2396,19 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
2359
2396
  api.logger.info("memos-local: stopped");
2360
2397
  },
2361
2398
  });
2399
+
2400
+ // Fallback: OpenClaw may load this plugin via deferred reload after
2401
+ // startPluginServices has already run, so service.start() never fires.
2402
+ // Self-start the viewer after a grace period if it hasn't been started.
2403
+ const SELF_START_DELAY_MS = 3000;
2404
+ setTimeout(() => {
2405
+ if (!serviceStarted) {
2406
+ api.logger.info("memos-local: service.start() not called by host, self-starting viewer...");
2407
+ startServiceCore().catch((err) => {
2408
+ api.logger.warn(`memos-local: self-start failed: ${err}`);
2409
+ });
2410
+ }
2411
+ }, SELF_START_DELAY_MS);
2362
2412
  },
2363
2413
  };
2364
2414
 
@@ -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.9",
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.10",
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"