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

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