@memtensor/memos-local-openclaw-plugin 1.0.4-beta.10 → 1.0.4-beta.11

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.
Files changed (67) hide show
  1. package/dist/client/connector.d.ts +1 -0
  2. package/dist/client/connector.d.ts.map +1 -1
  3. package/dist/client/connector.js +37 -8
  4. package/dist/client/connector.js.map +1 -1
  5. package/dist/hub/server.d.ts +1 -0
  6. package/dist/hub/server.d.ts.map +1 -1
  7. package/dist/hub/server.js +122 -28
  8. package/dist/hub/server.js.map +1 -1
  9. package/dist/hub/user-manager.d.ts +9 -0
  10. package/dist/hub/user-manager.d.ts.map +1 -1
  11. package/dist/hub/user-manager.js +26 -2
  12. package/dist/hub/user-manager.js.map +1 -1
  13. package/dist/recall/engine.d.ts.map +1 -1
  14. package/dist/recall/engine.js +2 -0
  15. package/dist/recall/engine.js.map +1 -1
  16. package/dist/sharing/types.d.ts +1 -1
  17. package/dist/sharing/types.d.ts.map +1 -1
  18. package/dist/skill/evolver.d.ts +2 -0
  19. package/dist/skill/evolver.d.ts.map +1 -1
  20. package/dist/skill/evolver.js +56 -5
  21. package/dist/skill/evolver.js.map +1 -1
  22. package/dist/skill/generator.d.ts +2 -0
  23. package/dist/skill/generator.d.ts.map +1 -1
  24. package/dist/skill/generator.js +45 -3
  25. package/dist/skill/generator.js.map +1 -1
  26. package/dist/skill/installer.d.ts +26 -0
  27. package/dist/skill/installer.d.ts.map +1 -1
  28. package/dist/skill/installer.js +80 -4
  29. package/dist/skill/installer.js.map +1 -1
  30. package/dist/skill/upgrader.d.ts +2 -0
  31. package/dist/skill/upgrader.d.ts.map +1 -1
  32. package/dist/skill/upgrader.js +139 -1
  33. package/dist/skill/upgrader.js.map +1 -1
  34. package/dist/skill/validator.d.ts +3 -0
  35. package/dist/skill/validator.d.ts.map +1 -1
  36. package/dist/skill/validator.js +75 -0
  37. package/dist/skill/validator.js.map +1 -1
  38. package/dist/storage/sqlite.d.ts +15 -0
  39. package/dist/storage/sqlite.d.ts.map +1 -1
  40. package/dist/storage/sqlite.js +91 -9
  41. package/dist/storage/sqlite.js.map +1 -1
  42. package/dist/types.d.ts +10 -0
  43. package/dist/types.d.ts.map +1 -1
  44. package/dist/types.js +4 -0
  45. package/dist/types.js.map +1 -1
  46. package/dist/viewer/html.d.ts.map +1 -1
  47. package/dist/viewer/html.js +44 -23
  48. package/dist/viewer/html.js.map +1 -1
  49. package/dist/viewer/server.d.ts.map +1 -1
  50. package/dist/viewer/server.js +35 -15
  51. package/dist/viewer/server.js.map +1 -1
  52. package/index.ts +316 -13
  53. package/package.json +1 -1
  54. package/src/client/connector.ts +41 -8
  55. package/src/hub/server.ts +123 -27
  56. package/src/hub/user-manager.ts +42 -6
  57. package/src/recall/engine.ts +2 -0
  58. package/src/sharing/types.ts +1 -1
  59. package/src/skill/evolver.ts +58 -6
  60. package/src/skill/generator.ts +44 -5
  61. package/src/skill/installer.ts +107 -4
  62. package/src/skill/upgrader.ts +139 -1
  63. package/src/skill/validator.ts +79 -0
  64. package/src/storage/sqlite.ts +105 -9
  65. package/src/types.ts +11 -0
  66. package/src/viewer/html.ts +44 -23
  67. package/src/viewer/server.ts +35 -15
package/index.ts CHANGED
@@ -19,6 +19,7 @@ import { IngestWorker } from "./src/ingest/worker";
19
19
  import { RecallEngine } from "./src/recall/engine";
20
20
  import { captureMessages, stripInboundMetadata } from "./src/capture";
21
21
  import { DEFAULTS } from "./src/types";
22
+ import type { SearchHit } from "./src/types";
22
23
  import { ViewerServer } from "./src/viewer/server";
23
24
  import { HubServer } from "./src/hub/server";
24
25
  import { hubGetMemoryDetail, hubRequestJson, hubSearchMemories, hubSearchSkills, resolveHubClient } from "./src/client/hub";
@@ -318,6 +319,21 @@ const memosLocalPlugin = {
318
319
  candidates: det.candidates,
319
320
  filtered: det.hits ?? det.filtered ?? [],
320
321
  });
322
+ } else if (det && det.local && det.hub) {
323
+ const localHits = det.local?.hits ?? [];
324
+ const hubHits = (det.hub?.hits ?? []).map((h: any) => ({
325
+ score: h.score ?? 0,
326
+ role: h.source?.role ?? h.role ?? "assistant",
327
+ summary: h.summary ?? "",
328
+ original_excerpt: h.excerpt ?? h.summary ?? "",
329
+ origin: "hub-remote",
330
+ ownerName: h.ownerName ?? "",
331
+ groupName: h.groupName ?? "",
332
+ }));
333
+ outputText = JSON.stringify({
334
+ candidates: [...localHits, ...hubHits],
335
+ filtered: [...localHits, ...hubHits],
336
+ });
321
337
  } else {
322
338
  outputText = result?.content?.[0]?.text ?? JSON.stringify(result ?? "");
323
339
  }
@@ -464,6 +480,7 @@ const memosLocalPlugin = {
464
480
  score: h.score,
465
481
  summary: h.summary,
466
482
  original_excerpt: (h.original_excerpt ?? "").slice(0, 200),
483
+ origin: h.origin || "local",
467
484
  }));
468
485
 
469
486
  if (result.hits.length === 0 && searchScope === "local") {
@@ -518,29 +535,79 @@ const memosLocalPlugin = {
518
535
  role: h.source.role,
519
536
  score: h.score,
520
537
  summary: h.summary,
538
+ origin: h.origin || "local",
521
539
  };
522
540
  });
523
541
 
524
542
  if (searchScope !== "local") {
525
543
  const hub = await hubSearchMemories(store, ctx, { query, maxResults: searchLimit, scope: searchScope as any, hubAddress, userToken }).catch(() => ({ hits: [], meta: { totalCandidates: 0, searchedGroups: [], includedPublic: searchScope === "all" } }));
544
+
545
+ let filteredHubHits = hub.hits;
546
+ if (hub.hits.length > 0) {
547
+ const hubCandidates = hub.hits.map((h, i) => ({
548
+ index: filteredHits.length + i + 1,
549
+ role: (h.source?.role || "assistant") as string,
550
+ content: (h.summary || h.excerpt || "").slice(0, 300),
551
+ time: h.source?.ts ? new Date(h.source.ts).toISOString().slice(0, 16) : "",
552
+ }));
553
+ const localCandidatesForMerge = filteredHits.map((h, i) => ({
554
+ index: i + 1,
555
+ role: h.source.role,
556
+ content: (h.original_excerpt ?? "").slice(0, 300),
557
+ time: h.source.ts ? new Date(h.source.ts).toISOString().slice(0, 16) : "",
558
+ }));
559
+ const mergedCandidates = [...localCandidatesForMerge, ...hubCandidates];
560
+ const mergedFilter = await summarizer.filterRelevant(query, mergedCandidates);
561
+ if (mergedFilter !== null && mergedFilter.relevant.length > 0) {
562
+ const relevantSet = new Set(mergedFilter.relevant);
563
+ const hubStartIdx = filteredHits.length + 1;
564
+ filteredHits = filteredHits.filter((_, i) => relevantSet.has(i + 1));
565
+ filteredHubHits = hub.hits.filter((_, i) => relevantSet.has(hubStartIdx + i));
566
+ ctx.log.debug(`memory_search LLM filter (merged): local ${localCandidatesForMerge.length}→${filteredHits.length}, hub ${hub.hits.length}→${filteredHubHits.length}`);
567
+ }
568
+ }
569
+
570
+ const originLabel = (h: SearchHit) => {
571
+ if (h.origin === "hub-memory") return " [团队缓存]";
572
+ if (h.origin === "local-shared") return " [本机共享]";
573
+ return "";
574
+ };
526
575
  const localText = filteredHits.length > 0
527
576
  ? filteredHits.map((h, i) => {
528
577
  const excerpt = h.original_excerpt.length > 220 ? h.original_excerpt.slice(0, 217) + "..." : h.original_excerpt;
529
- return `${i + 1}. [${h.source.role}] ${excerpt}`;
578
+ return `${i + 1}. [${h.source.role}]${originLabel(h)} ${excerpt}`;
530
579
  }).join("\n")
531
580
  : "(none)";
532
- const hubText = hub.hits.length > 0
533
- ? hub.hits.map((h, i) => `${i + 1}. [${h.ownerName}] ${h.summary}${h.groupName ? ` (${h.groupName})` : ""}`).join("\n")
581
+ const hubText = filteredHubHits.length > 0
582
+ ? filteredHubHits.map((h, i) => `${i + 1}. [${h.ownerName}] [团队] ${h.summary}${h.groupName ? ` (${h.groupName})` : ""}`).join("\n")
534
583
  : "(none)";
535
584
 
585
+ const localDetailsFiltered = filteredHits.map((h) => {
586
+ let effectiveTaskId = h.taskId;
587
+ if (effectiveTaskId) {
588
+ const t = store.getTask(effectiveTaskId);
589
+ if (t && t.status === "skipped") effectiveTaskId = null;
590
+ }
591
+ return {
592
+ ref: h.ref,
593
+ chunkId: h.ref.chunkId,
594
+ taskId: effectiveTaskId,
595
+ skillId: h.skillId,
596
+ role: h.source.role,
597
+ score: h.score,
598
+ summary: h.summary,
599
+ origin: h.origin,
600
+ };
601
+ });
602
+
536
603
  return {
537
604
  content: [{
538
605
  type: "text",
539
606
  text: `Local results:\n${localText}\n\nHub results:\n${hubText}`,
540
607
  }],
541
608
  details: {
542
- local: { hits: localDetailsHits, meta: result.meta },
543
- hub,
609
+ local: { hits: localDetailsFiltered, meta: result.meta },
610
+ hub: { ...hub, hits: filteredHubHits },
544
611
  },
545
612
  };
546
613
  }
@@ -552,9 +619,15 @@ const memosLocalPlugin = {
552
619
  };
553
620
  }
554
621
 
622
+ const originTag = (o?: string) => {
623
+ if (o === "local-shared") return " [本机共享]";
624
+ if (o === "hub-memory") return " [团队缓存]";
625
+ if (o === "hub-remote") return " [团队]";
626
+ return "";
627
+ };
555
628
  const lines = filteredHits.map((h, i) => {
556
629
  const excerpt = h.original_excerpt;
557
- const parts = [`${i + 1}. [${h.source.role}]`];
630
+ const parts = [`${i + 1}. [${h.source.role}]${originTag(h.origin)}`];
558
631
  if (excerpt) parts.push(` ${excerpt}`);
559
632
  parts.push(` chunkId="${h.ref.chunkId}"`);
560
633
  if (h.taskId) {
@@ -609,6 +682,7 @@ const memosLocalPlugin = {
609
682
  score: h.score,
610
683
  summary: h.summary,
611
684
  original_excerpt: (h.original_excerpt ?? "").slice(0, 200),
685
+ origin: h.origin || "local",
612
686
  };
613
687
  }),
614
688
  meta: result.meta,
@@ -1058,10 +1132,31 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1058
1132
  };
1059
1133
  }
1060
1134
 
1135
+ const manifest = skillInstaller.getCompanionManifest(resolvedSkillId);
1136
+ let footer = "\n\n---\n";
1137
+
1138
+ if (manifest && manifest.hasCompanionFiles) {
1139
+ const fileSummary = manifest.files
1140
+ .filter(f => f.type !== "eval")
1141
+ .map(f => `\`${f.relativePath}\``)
1142
+ .join(", ");
1143
+ footer += `**Companion files available:** ${fileSummary}\n`;
1144
+ footer += `→ call \`skill_files(skillId="${resolvedSkillId}")\` to list all files\n`;
1145
+ footer += `→ call \`skill_file_get(skillId="${resolvedSkillId}", path="...")\` to read a specific file\n`;
1146
+ if (manifest.installMode === "install_recommended") {
1147
+ footer += `→ **Recommended:** call \`skill_install(skillId="${resolvedSkillId}")\` for persistent workspace access (many/large files)\n`;
1148
+ }
1149
+ if (manifest.installed && manifest.installedPath) {
1150
+ footer += `> Already installed at: ${manifest.installedPath}/\n`;
1151
+ }
1152
+ } else {
1153
+ footer += `To install this skill for persistent use: call skill_install(skillId="${resolvedSkillId}")`;
1154
+ }
1155
+
1061
1156
  return {
1062
1157
  content: [{
1063
1158
  type: "text",
1064
- text: `## Skill: ${skill.name} (v${skill.version})\n\n${sv.content}\n\n---\nTo install this skill for persistent use: call skill_install(skillId="${resolvedSkillId}")`,
1159
+ text: `## Skill: ${skill.name} (v${skill.version})\n\n${sv.content}${footer}`,
1065
1160
  }],
1066
1161
  details: {
1067
1162
  skillId: skill.id,
@@ -1069,6 +1164,8 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1069
1164
  version: skill.version,
1070
1165
  status: skill.status,
1071
1166
  installed: skill.installed,
1167
+ companionFiles: manifest?.hasCompanionFiles ?? false,
1168
+ installMode: manifest?.installMode ?? "inline",
1072
1169
  },
1073
1170
  };
1074
1171
  }),
@@ -1104,6 +1201,112 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1104
1201
  { name: "skill_install" },
1105
1202
  );
1106
1203
 
1204
+ // ─── Tool: skill_files ───
1205
+
1206
+ api.registerTool(
1207
+ {
1208
+ name: "skill_files",
1209
+ label: "List Skill Companion Files",
1210
+ description:
1211
+ "List companion files (scripts, references, evals) for a skill. " +
1212
+ "Use this after skill_get to see what additional files are available. " +
1213
+ "Returns file names, sizes, and whether the skill recommends installation.",
1214
+ parameters: Type.Object({
1215
+ skillId: Type.String({ description: "The skill_id to inspect" }),
1216
+ }),
1217
+ execute: trackTool("skill_files", async (_toolCallId: any, params: any) => {
1218
+ const { skillId } = params as { skillId: string };
1219
+ ctx.log.debug(`skill_files called for skill=${skillId}`);
1220
+
1221
+ const manifest = skillInstaller.getCompanionManifest(skillId);
1222
+ if (!manifest) {
1223
+ return {
1224
+ content: [{ type: "text", text: `Skill not found: ${skillId}` }],
1225
+ details: { error: "not_found" },
1226
+ };
1227
+ }
1228
+
1229
+ if (!manifest.hasCompanionFiles) {
1230
+ return {
1231
+ content: [{ type: "text", text: "This skill has no companion files (scripts, references). The SKILL.md from skill_get contains everything." }],
1232
+ details: manifest,
1233
+ };
1234
+ }
1235
+
1236
+ const lines: string[] = [`## Companion Files (${manifest.files.length} files, ${Math.round(manifest.totalSize / 1024)}KB total)\n`];
1237
+ if (manifest.scriptsCount > 0) {
1238
+ lines.push(`### Scripts (${manifest.scriptsCount})`);
1239
+ for (const f of manifest.files.filter(f => f.type === "script")) {
1240
+ lines.push(`- \`${f.relativePath}\` (${f.size} bytes) → call \`skill_file_get(skillId="${skillId}", path="${f.relativePath}")\``);
1241
+ }
1242
+ }
1243
+ if (manifest.referencesCount > 0) {
1244
+ lines.push(`\n### References (${manifest.referencesCount})`);
1245
+ for (const f of manifest.files.filter(f => f.type === "reference")) {
1246
+ lines.push(`- \`${f.relativePath}\` (${f.size} bytes) → call \`skill_file_get(skillId="${skillId}", path="${f.relativePath}")\``);
1247
+ }
1248
+ }
1249
+ if (manifest.evalsCount > 0) {
1250
+ lines.push(`\n### Evals (${manifest.evalsCount})`);
1251
+ for (const f of manifest.files.filter(f => f.type === "eval")) {
1252
+ lines.push(`- \`${f.relativePath}\` (${f.size} bytes)`);
1253
+ }
1254
+ }
1255
+
1256
+ if (manifest.installMode === "install_recommended") {
1257
+ lines.push(`\n> **Recommendation:** This skill has many/large companion files. Consider \`skill_install(skillId="${skillId}")\` for persistent workspace access.`);
1258
+ }
1259
+ if (manifest.installed && manifest.installedPath) {
1260
+ lines.push(`\n> **Installed at:** ${manifest.installedPath}/`);
1261
+ }
1262
+
1263
+ return {
1264
+ content: [{ type: "text", text: lines.join("\n") }],
1265
+ details: manifest,
1266
+ };
1267
+ }),
1268
+ },
1269
+ { name: "skill_files" },
1270
+ );
1271
+
1272
+ // ─── Tool: skill_file_get ───
1273
+
1274
+ api.registerTool(
1275
+ {
1276
+ name: "skill_file_get",
1277
+ label: "Get Skill Companion File",
1278
+ description:
1279
+ "Read the content of a specific companion file (script, reference) from a skill. " +
1280
+ "Use after skill_files to retrieve a script or reference document. " +
1281
+ "Pass the relative path like 'scripts/deploy.sh' or 'references/api-notes.md'.",
1282
+ parameters: Type.Object({
1283
+ skillId: Type.String({ description: "The skill_id" }),
1284
+ path: Type.String({ description: "Relative path within the skill, e.g. 'scripts/deploy.sh'" }),
1285
+ }),
1286
+ execute: trackTool("skill_file_get", async (_toolCallId: any, params: any) => {
1287
+ const { skillId, path: filePath } = params as { skillId: string; path: string };
1288
+ ctx.log.debug(`skill_file_get called for skill=${skillId} path=${filePath}`);
1289
+
1290
+ const result = skillInstaller.readCompanionFile(skillId, filePath);
1291
+ if ("error" in result) {
1292
+ return {
1293
+ content: [{ type: "text", text: `Error: ${result.error}` }],
1294
+ details: result,
1295
+ };
1296
+ }
1297
+
1298
+ const ext = filePath.split(".").pop() || "";
1299
+ const lang = { sh: "bash", py: "python", ts: "typescript", js: "javascript", json: "json", md: "markdown", yml: "yaml", yaml: "yaml" }[ext] || "";
1300
+
1301
+ return {
1302
+ content: [{ type: "text", text: `## ${filePath}\n\n\`\`\`${lang}\n${result.content}\n\`\`\`` }],
1303
+ details: { path: filePath, size: result.size },
1304
+ };
1305
+ }),
1306
+ },
1307
+ { name: "skill_file_get" },
1308
+ );
1309
+
1107
1310
  // ─── Tool: memory_viewer ───
1108
1311
 
1109
1312
  const viewerPort = (pluginCfg as any).viewerPort ?? 18799;
@@ -1614,10 +1817,40 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1614
1817
 
1615
1818
  const result = await engine.search({ query, maxResults: 10, minScore: 0.45, ownerFilter: recallOwnerFilter });
1616
1819
  if (result.hits.length === 0) {
1617
- ctx.log.debug("auto-recall: no candidates found");
1820
+ ctx.log.debug("auto-recall: no memory candidates found");
1618
1821
  const dur = performance.now() - recallT0;
1619
1822
  store.recordToolCall("memory_search", dur, true);
1620
1823
  store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({ candidates: [], filtered: [] }), dur, true);
1824
+
1825
+ // Even without memory hits, try skill recall
1826
+ const skillAutoRecallEarly = ctx.config.skillEvolution?.autoRecallSkills ?? DEFAULTS.skillAutoRecall;
1827
+ if (skillAutoRecallEarly) {
1828
+ try {
1829
+ const skillLimit = ctx.config.skillEvolution?.autoRecallSkillLimit ?? DEFAULTS.skillAutoRecallLimit;
1830
+ const skillHits = await engine.searchSkills(query, "mix" as any, getCurrentOwner());
1831
+ const topSkills = skillHits.slice(0, skillLimit);
1832
+ if (topSkills.length > 0) {
1833
+ const skillLines = topSkills.map((sc, i) => {
1834
+ const manifest = skillInstaller.getCompanionManifest(sc.skillId);
1835
+ let badge = "";
1836
+ if (manifest?.installed) badge = " [installed]";
1837
+ else if (manifest?.installMode === "install_recommended") badge = " [has scripts, install recommended]";
1838
+ else if (manifest?.hasCompanionFiles) badge = " [has companion files]";
1839
+ return `${i + 1}. **${sc.name}**${badge} — ${sc.description.slice(0, 200)}\n → call \`skill_get(skillId="${sc.skillId}")\` for the full guide`;
1840
+ });
1841
+ const skillContext = "## Relevant skills from past experience\n\n" +
1842
+ "No direct memory matches were found, but these skills from past tasks may help:\n\n" +
1843
+ skillLines.join("\n\n") +
1844
+ "\n\nYou SHOULD call `skill_get` to retrieve the full guide before attempting the task.";
1845
+ ctx.log.info(`auto-recall-skill (no-memory path): injecting ${topSkills.length} skill(s)`);
1846
+ try { store.recordApiLog("skill_search", { type: "auto_recall_skill", query }, JSON.stringify(topSkills), dur, true); } catch { /* best-effort */ }
1847
+ return { prependContext: skillContext };
1848
+ }
1849
+ } catch (err) {
1850
+ ctx.log.debug(`auto-recall-skill (no-memory path): failed: ${err}`);
1851
+ }
1852
+ }
1853
+
1621
1854
  if (query.length > 50) {
1622
1855
  const noRecallHint =
1623
1856
  "## Memory system — ACTION REQUIRED\n\n" +
@@ -1650,7 +1883,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1650
1883
  const dur = performance.now() - recallT0;
1651
1884
  store.recordToolCall("memory_search", dur, true);
1652
1885
  store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({
1653
- candidates: result.hits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt })),
1886
+ candidates: result.hits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt, origin: h.origin || "local" })),
1654
1887
  filtered: []
1655
1888
  }), dur, true);
1656
1889
  if (query.length > 50) {
@@ -1671,7 +1904,8 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1671
1904
 
1672
1905
  const lines = filteredHits.map((h, i) => {
1673
1906
  const excerpt = h.original_excerpt;
1674
- const parts: string[] = [`${i + 1}. [${h.source.role}]`];
1907
+ const oTag = h.origin === "local-shared" ? " [本机共享]" : h.origin === "hub-memory" ? " [团队缓存]" : "";
1908
+ const parts: string[] = [`${i + 1}. [${h.source.role}]${oTag}`];
1675
1909
  if (excerpt) parts.push(` ${excerpt}`);
1676
1910
  parts.push(` chunkId="${h.ref.chunkId}"`);
1677
1911
  if (h.taskId) {
@@ -1706,17 +1940,86 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1706
1940
  lines.join("\n\n"),
1707
1941
  ];
1708
1942
  if (tipsText) contextParts.push(tipsText);
1943
+
1944
+ // ─── Skill auto-recall ───
1945
+ const skillAutoRecall = ctx.config.skillEvolution?.autoRecallSkills ?? DEFAULTS.skillAutoRecall;
1946
+ const skillLimit = ctx.config.skillEvolution?.autoRecallSkillLimit ?? DEFAULTS.skillAutoRecallLimit;
1947
+ let skillSection = "";
1948
+
1949
+ if (skillAutoRecall) {
1950
+ try {
1951
+ const skillCandidateMap = new Map<string, { name: string; description: string; skillId: string; source: string }>();
1952
+
1953
+ // Source 1: direct skill search based on user query
1954
+ try {
1955
+ const directSkillHits = await engine.searchSkills(query, "mix" as any, getCurrentOwner());
1956
+ for (const sh of directSkillHits.slice(0, skillLimit + 2)) {
1957
+ if (!skillCandidateMap.has(sh.skillId)) {
1958
+ skillCandidateMap.set(sh.skillId, { name: sh.name, description: sh.description, skillId: sh.skillId, source: "query" });
1959
+ }
1960
+ }
1961
+ } catch (err) {
1962
+ ctx.log.debug(`auto-recall-skill: direct search failed: ${err}`);
1963
+ }
1964
+
1965
+ // Source 2: skills linked to tasks from memory hits
1966
+ const taskIds = new Set<string>();
1967
+ for (const h of filteredHits) {
1968
+ if (h.taskId) {
1969
+ const t = store.getTask(h.taskId);
1970
+ if (t && t.status !== "skipped") taskIds.add(h.taskId);
1971
+ }
1972
+ }
1973
+ for (const tid of taskIds) {
1974
+ const linked = store.getSkillsByTask(tid);
1975
+ for (const rs of linked) {
1976
+ if (!skillCandidateMap.has(rs.skill.id)) {
1977
+ skillCandidateMap.set(rs.skill.id, { name: rs.skill.name, description: rs.skill.description, skillId: rs.skill.id, source: `task:${tid}` });
1978
+ }
1979
+ }
1980
+ }
1981
+
1982
+ const skillCandidates = [...skillCandidateMap.values()].slice(0, skillLimit);
1983
+
1984
+ if (skillCandidates.length > 0) {
1985
+ const skillLines = skillCandidates.map((sc, i) => {
1986
+ const manifest = skillInstaller.getCompanionManifest(sc.skillId);
1987
+ let badge = "";
1988
+ if (manifest?.installed) badge = " [installed]";
1989
+ else if (manifest?.installMode === "install_recommended") badge = " [has scripts, install recommended]";
1990
+ else if (manifest?.hasCompanionFiles) badge = " [has companion files]";
1991
+ const action = `call \`skill_get(skillId="${sc.skillId}")\``;
1992
+ return `${i + 1}. **${sc.name}**${badge} — ${sc.description.slice(0, 200)}\n → ${action}`;
1993
+ });
1994
+ skillSection = "\n\n## Relevant skills from past experience\n\n" +
1995
+ "The following skills were distilled from similar previous tasks. " +
1996
+ "You SHOULD call `skill_get` to retrieve the full guide before attempting the task.\n\n" +
1997
+ skillLines.join("\n\n");
1998
+
1999
+ ctx.log.info(`auto-recall-skill: injecting ${skillCandidates.length} skill(s): ${skillCandidates.map(s => s.name).join(", ")}`);
2000
+ try {
2001
+ store.recordApiLog("skill_search", { type: "auto_recall_skill", query }, JSON.stringify(skillCandidates), performance.now() - recallT0, true);
2002
+ } catch { /* best-effort */ }
2003
+ } else {
2004
+ ctx.log.debug("auto-recall-skill: no matching skills found");
2005
+ }
2006
+ } catch (err) {
2007
+ ctx.log.debug(`auto-recall-skill: failed: ${err}`);
2008
+ }
2009
+ }
2010
+
2011
+ if (skillSection) contextParts.push(skillSection);
1709
2012
  const context = contextParts.join("\n");
1710
2013
 
1711
2014
  const recallDur = performance.now() - recallT0;
1712
2015
  store.recordToolCall("memory_search", recallDur, true);
1713
2016
  store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({
1714
- candidates: result.hits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt })),
1715
- filtered: filteredHits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt }))
2017
+ candidates: result.hits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt, origin: h.origin || "local" })),
2018
+ filtered: filteredHits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt, origin: h.origin || "local" }))
1716
2019
  }), recallDur, true);
1717
2020
  telemetry.trackAutoRecall(filteredHits.length, recallDur);
1718
2021
 
1719
- ctx.log.info(`auto-recall: returning prependContext (${context.length} chars), sufficient=${sufficient}`);
2022
+ ctx.log.info(`auto-recall: returning prependContext (${context.length} chars), sufficient=${sufficient}, skills=${skillSection ? "yes" : "no"}`);
1720
2023
 
1721
2024
  if (!sufficient) {
1722
2025
  const searchHint =
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memtensor/memos-local-openclaw-plugin",
3
- "version": "1.0.4-beta.10",
3
+ "version": "1.0.4-beta.11",
4
4
  "description": "MemOS Local memory plugin for OpenClaw — full-write, hybrid-recall, progressive retrieval",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -10,6 +10,7 @@ export interface HubSessionInfo {
10
10
  userToken: string;
11
11
  role: UserRole;
12
12
  connectedAt: number;
13
+ identityKey?: string;
13
14
  }
14
15
 
15
16
  export interface HubStatusInfo {
@@ -54,6 +55,8 @@ export async function connectToHub(store: SqliteStore, config: MemosLocalConfig,
54
55
  userToken: result.userToken,
55
56
  role: "member",
56
57
  connectedAt: Date.now(),
58
+ identityKey: persisted.identityKey || "",
59
+ lastKnownStatus: "active",
57
60
  });
58
61
  return store.getClientHubConnection()!;
59
62
  }
@@ -63,6 +66,12 @@ export async function connectToHub(store: SqliteStore, config: MemosLocalConfig,
63
66
  if (result.status === "rejected") {
64
67
  throw new Error("Join request was rejected by the Hub admin.");
65
68
  }
69
+ if (result.status === "blocked") {
70
+ throw new Error("Your account has been blocked by the Hub admin.");
71
+ }
72
+ if (result.status === "left" || result.status === "removed") {
73
+ log.info(`User status is "${result.status}", will try to rejoin.`);
74
+ }
66
75
  } catch (err) {
67
76
  if (err instanceof PendingApprovalError) throw err;
68
77
  log.warn(`registration-status check failed, falling back to autoJoinHub: ${err}`);
@@ -78,6 +87,7 @@ export async function connectToHub(store: SqliteStore, config: MemosLocalConfig,
78
87
 
79
88
  const hubUrl = normalizeHubUrl(hubAddress);
80
89
  const me = await hubRequestJson(hubUrl, userToken, "/api/v1/hub/me", { method: "GET" }) as any;
90
+ const persisted = store.getClientHubConnection();
81
91
  store.setClientHubConnection({
82
92
  hubUrl,
83
93
  userId: String(me.id),
@@ -85,6 +95,8 @@ export async function connectToHub(store: SqliteStore, config: MemosLocalConfig,
85
95
  userToken,
86
96
  role: String(me.role ?? "member") as UserRole,
87
97
  connectedAt: Date.now(),
98
+ identityKey: persisted?.identityKey || String(me.identityKey ?? ""),
99
+ lastKnownStatus: "active",
88
100
  });
89
101
  return store.getClientHubConnection()!;
90
102
  }
@@ -95,9 +107,13 @@ export async function getHubStatus(store: SqliteStore, config: MemosLocalConfig)
95
107
  const hubAddress = conn?.hubUrl || (configHubAddress ? normalizeHubUrl(configHubAddress) : "");
96
108
  const userToken = conn?.userToken || config.sharing?.client?.userToken || "";
97
109
 
98
- // If DB has a connection to a different Hub than config, the DB data is stale
99
110
  if (conn && configHubAddress && conn.hubUrl && normalizeHubUrl(configHubAddress) !== conn.hubUrl) {
100
- store.clearClientHubConnection();
111
+ store.setClientHubConnection({
112
+ ...conn,
113
+ hubUrl: normalizeHubUrl(configHubAddress),
114
+ userToken: "",
115
+ lastKnownStatus: "hub_changed",
116
+ });
101
117
  return { connected: false, user: null };
102
118
  }
103
119
 
@@ -129,6 +145,8 @@ export async function getHubStatus(store: SqliteStore, config: MemosLocalConfig)
129
145
  userToken: result.userToken,
130
146
  role: "member",
131
147
  connectedAt: Date.now(),
148
+ identityKey: conn.identityKey || "",
149
+ lastKnownStatus: "active",
132
150
  });
133
151
  const me = await hubRequestJson(normalizeHubUrl(hubAddress), result.userToken, "/api/v1/hub/me", { method: "GET" }) as any;
134
152
  return {
@@ -169,12 +187,10 @@ export async function getHubStatus(store: SqliteStore, config: MemosLocalConfig)
169
187
  const latestRole = String(me.role ?? "member") as UserRole;
170
188
  if (conn && (conn.username !== latestUsername || conn.role !== latestRole)) {
171
189
  store.setClientHubConnection({
172
- hubUrl: conn.hubUrl,
173
- userId: conn.userId,
190
+ ...conn,
174
191
  username: latestUsername,
175
- userToken: conn.userToken,
176
192
  role: latestRole,
177
- connectedAt: conn.connectedAt,
193
+ lastKnownStatus: "active",
178
194
  });
179
195
  }
180
196
  return {
@@ -190,7 +206,11 @@ export async function getHubStatus(store: SqliteStore, config: MemosLocalConfig)
190
206
  } catch (err: any) {
191
207
  const is401 = typeof err?.message === "string" && err.message.includes("(401)");
192
208
  if (is401 && conn) {
193
- store.clearClientHubConnection();
209
+ store.setClientHubConnection({
210
+ ...conn,
211
+ userToken: "",
212
+ lastKnownStatus: "removed",
213
+ });
194
214
  return {
195
215
  connected: false,
196
216
  hubUrl: normalizeHubUrl(hubAddress),
@@ -232,12 +252,17 @@ export async function autoJoinHub(
232
252
  }
233
253
  }
234
254
 
255
+ const persisted = store.getClientHubConnection();
256
+ const existingIdentityKey = persisted?.identityKey || "";
257
+
235
258
  log.info(`Joining Hub at ${hubUrl} as "${username}"...`);
236
259
  const result = await hubRequestJson(hubUrl, "", "/api/v1/hub/join", {
237
260
  method: "POST",
238
- body: JSON.stringify({ teamToken, username, deviceName: hostname, clientIp }),
261
+ body: JSON.stringify({ teamToken, username, deviceName: hostname, clientIp, identityKey: existingIdentityKey }),
239
262
  }) as any;
240
263
 
264
+ const returnedIdentityKey = String(result.identityKey || existingIdentityKey || "");
265
+
241
266
  if (result.status === "pending") {
242
267
  log.info(`Join request submitted, awaiting admin approval. userId=${result.userId}`);
243
268
  store.setClientHubConnection({
@@ -247,6 +272,8 @@ export async function autoJoinHub(
247
272
  userToken: "",
248
273
  role: "member",
249
274
  connectedAt: Date.now(),
275
+ identityKey: returnedIdentityKey,
276
+ lastKnownStatus: "pending",
250
277
  });
251
278
  throw new PendingApprovalError(result.userId);
252
279
  }
@@ -255,6 +282,10 @@ export async function autoJoinHub(
255
282
  throw new Error(`Join request was rejected by the Hub admin.`);
256
283
  }
257
284
 
285
+ if (result.status === "blocked") {
286
+ throw new Error(`Your account has been blocked by the Hub admin.`);
287
+ }
288
+
258
289
  if (!result.userToken) {
259
290
  throw new Error(`Hub join failed: ${JSON.stringify(result)}`);
260
291
  }
@@ -267,6 +298,8 @@ export async function autoJoinHub(
267
298
  userToken: result.userToken,
268
299
  role: "member",
269
300
  connectedAt: Date.now(),
301
+ identityKey: returnedIdentityKey,
302
+ lastKnownStatus: "active",
270
303
  });
271
304
  return store.getClientHubConnection()!;
272
305
  }