@memtensor/memos-local-openclaw-plugin 1.0.6-beta.1 → 1.0.6-beta.3

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 (38) hide show
  1. package/dist/client/connector.d.ts.map +1 -1
  2. package/dist/client/connector.js +10 -4
  3. package/dist/client/connector.js.map +1 -1
  4. package/dist/hub/server.d.ts +2 -0
  5. package/dist/hub/server.d.ts.map +1 -1
  6. package/dist/hub/server.js +108 -54
  7. package/dist/hub/server.js.map +1 -1
  8. package/dist/ingest/providers/index.d.ts +4 -0
  9. package/dist/ingest/providers/index.d.ts.map +1 -1
  10. package/dist/ingest/providers/index.js +20 -4
  11. package/dist/ingest/providers/index.js.map +1 -1
  12. package/dist/ingest/providers/openai.d.ts +0 -3
  13. package/dist/ingest/providers/openai.d.ts.map +1 -1
  14. package/dist/ingest/providers/openai.js +9 -8
  15. package/dist/ingest/providers/openai.js.map +1 -1
  16. package/dist/recall/engine.d.ts.map +1 -1
  17. package/dist/recall/engine.js +35 -43
  18. package/dist/recall/engine.js.map +1 -1
  19. package/dist/storage/sqlite.d.ts +13 -0
  20. package/dist/storage/sqlite.d.ts.map +1 -1
  21. package/dist/storage/sqlite.js +43 -1
  22. package/dist/storage/sqlite.js.map +1 -1
  23. package/dist/viewer/html.d.ts.map +1 -1
  24. package/dist/viewer/html.js +110 -22
  25. package/dist/viewer/html.js.map +1 -1
  26. package/dist/viewer/server.d.ts.map +1 -1
  27. package/dist/viewer/server.js +86 -34
  28. package/dist/viewer/server.js.map +1 -1
  29. package/index.ts +208 -253
  30. package/package.json +1 -1
  31. package/src/client/connector.ts +10 -4
  32. package/src/hub/server.ts +95 -51
  33. package/src/ingest/providers/index.ts +26 -18
  34. package/src/ingest/providers/openai.ts +5 -4
  35. package/src/recall/engine.ts +34 -41
  36. package/src/storage/sqlite.ts +43 -1
  37. package/src/viewer/html.ts +110 -22
  38. package/src/viewer/server.ts +91 -31
package/index.ts CHANGED
@@ -340,25 +340,11 @@ const memosLocalPlugin = {
340
340
  try {
341
341
  let outputText: string;
342
342
  const det = result?.details;
343
- if (det && Array.isArray(det.candidates)) {
343
+ if (det && (Array.isArray(det.candidates) || Array.isArray(det.filtered))) {
344
344
  outputText = JSON.stringify({
345
- candidates: det.candidates,
346
- filtered: det.hits ?? det.filtered ?? [],
347
- });
348
- } else if (det && det.local && det.hub) {
349
- const localHits = det.local?.hits ?? [];
350
- const hubHits = (det.hub?.hits ?? []).map((h: any) => ({
351
- score: h.score ?? 0,
352
- role: h.source?.role ?? h.role ?? "assistant",
353
- summary: h.summary ?? "",
354
- original_excerpt: h.excerpt ?? h.summary ?? "",
355
- origin: "hub-remote",
356
- ownerName: h.ownerName ?? "",
357
- groupName: h.groupName ?? "",
358
- }));
359
- outputText = JSON.stringify({
360
- candidates: [...localHits, ...hubHits],
361
- filtered: [...localHits, ...hubHits],
345
+ candidates: det.candidates ?? [],
346
+ hubCandidates: det.hubCandidates ?? [],
347
+ filtered: det.filtered ?? det.hits ?? [],
362
348
  });
363
349
  } else {
364
350
  outputText = result?.content?.[0]?.text ?? JSON.stringify(result ?? "");
@@ -505,10 +491,22 @@ const memosLocalPlugin = {
505
491
  const ownerFilter = [`agent:${agentId}`, "public"];
506
492
  const effectiveMaxResults = searchLimit;
507
493
  ctx.log.debug(`memory_search query="${query}" maxResults=${effectiveMaxResults} minScore=${minScore ?? 0.45} role=${role ?? "all"} owner=agent:${agentId}`);
508
- const result = await engine.search({ query, maxResults: effectiveMaxResults, minScore, role, ownerFilter });
509
- ctx.log.debug(`memory_search raw candidates: ${result.hits.length}`);
510
494
 
511
- const rawCandidates = result.hits.map((h) => ({
495
+ // ── Phase 1: Local search ∥ Hub search (parallel) ──
496
+ const localSearchP = engine.search({ query, maxResults: effectiveMaxResults, minScore, role, ownerFilter });
497
+ const hubSearchP = searchScope !== "local"
498
+ ? hubSearchMemories(store, ctx, { query, maxResults: searchLimit, scope: searchScope as any, hubAddress, userToken })
499
+ .catch(() => ({ hits: [] as any[], meta: { totalCandidates: 0, searchedGroups: [] as string[], includedPublic: searchScope === "all" } }))
500
+ : Promise.resolve(null);
501
+
502
+ const [result, hubResult] = await Promise.all([localSearchP, hubSearchP]);
503
+ ctx.log.debug(`memory_search raw candidates: local=${result.hits.length}, hub=${hubResult?.hits?.length ?? 0}`);
504
+
505
+ // Split local results: pure-local vs hub-memory (Hub role's hub_memories mixed in by RecallEngine)
506
+ const localHits = result.hits.filter((h) => h.origin !== "hub-memory");
507
+ const hubLocalHits = result.hits.filter((h) => h.origin === "hub-memory");
508
+
509
+ const rawLocalCandidates = localHits.map((h) => ({
512
510
  chunkId: h.ref.chunkId,
513
511
  role: h.source.role,
514
512
  score: h.score,
@@ -517,208 +515,156 @@ const memosLocalPlugin = {
517
515
  origin: h.origin || "local",
518
516
  }));
519
517
 
520
- if (result.hits.length === 0 && searchScope === "local") {
518
+ // Hub remote candidates (from HTTP call) + hub-memory candidates (from RecallEngine for Hub role)
519
+ const hubRemoteHits = hubResult?.hits ?? [];
520
+ const rawHubCandidates = [
521
+ ...hubLocalHits.map((h) => ({
522
+ score: h.score,
523
+ role: h.source.role,
524
+ summary: h.summary,
525
+ original_excerpt: (h.original_excerpt ?? "").slice(0, 200),
526
+ origin: "hub-memory" as const,
527
+ ownerName: "",
528
+ groupName: "",
529
+ })),
530
+ ...hubRemoteHits.map((h: any) => ({
531
+ score: h.score ?? 0,
532
+ role: h.source?.role ?? h.role ?? "assistant",
533
+ summary: h.summary ?? "",
534
+ original_excerpt: (h.excerpt ?? h.summary ?? "").slice(0, 200),
535
+ origin: "hub-remote" as const,
536
+ ownerName: h.ownerName ?? "",
537
+ groupName: h.groupName ?? "",
538
+ })),
539
+ ];
540
+
541
+ if (localHits.length === 0 && rawHubCandidates.length === 0) {
521
542
  return {
522
543
  content: [{ type: "text", text: result.meta.note ?? "No relevant memories found." }],
523
- details: { candidates: [], meta: result.meta },
544
+ details: { candidates: rawLocalCandidates, hubCandidates: [], filtered: [], meta: result.meta },
524
545
  };
525
546
  }
526
547
 
527
- let filteredHits = result.hits;
528
- let sufficient = false;
529
-
530
- const candidates = result.hits.map((h, i) => ({
531
- index: i + 1,
532
- role: h.source.role,
533
- content: (h.original_excerpt ?? "").slice(0, 300),
534
- time: h.source.ts ? new Date(h.source.ts).toISOString().slice(0, 16) : "",
535
- }));
536
-
537
- const filterResult = await summarizer.filterRelevant(query, candidates);
538
- if (filterResult !== null) {
539
- sufficient = filterResult.sufficient;
540
- if (filterResult.relevant.length > 0) {
541
- const indexSet = new Set(filterResult.relevant);
542
- filteredHits = result.hits.filter((_, i) => indexSet.has(i + 1));
543
- ctx.log.debug(`memory_search LLM filter: ${result.hits.length} → ${filteredHits.length} hits, sufficient=${sufficient}`);
544
- } else if (searchScope === "local") {
545
- return {
546
- content: [{ type: "text", text: "No relevant memories found for this query." }],
547
- details: { candidates: rawCandidates, filtered: [], meta: result.meta },
548
- };
549
- } else {
550
- filteredHits = [];
551
- }
552
- }
553
-
554
- const beforeDedup = filteredHits.length;
555
- filteredHits = deduplicateHits(filteredHits);
556
- ctx.log.debug(`memory_search dedup: ${beforeDedup} → ${filteredHits.length}`);
557
-
558
- const localDetailsHits = filteredHits.map((h) => {
559
- let effectiveTaskId = h.taskId;
560
- if (effectiveTaskId) {
561
- const t = store.getTask(effectiveTaskId);
562
- if (t && t.status === "skipped") effectiveTaskId = null;
563
- }
564
- return {
565
- ref: h.ref,
566
- chunkId: h.ref.chunkId,
567
- taskId: effectiveTaskId,
568
- skillId: h.skillId,
548
+ // ── Phase 2: Merge all candidates → single LLM filter ──
549
+ const allHitsForFilter = [...localHits, ...hubLocalHits];
550
+ const hubRemoteForFilter = hubRemoteHits;
551
+ const mergedCandidates = [
552
+ ...allHitsForFilter.map((h, i) => ({
553
+ index: i + 1,
569
554
  role: h.source.role,
570
- score: h.score,
571
- summary: h.summary,
572
- origin: h.origin || "local",
573
- };
574
- });
555
+ content: (h.original_excerpt ?? "").slice(0, 300),
556
+ time: h.source.ts ? new Date(h.source.ts).toISOString().slice(0, 16) : "",
557
+ })),
558
+ ...hubRemoteForFilter.map((h: any, i: number) => ({
559
+ index: allHitsForFilter.length + i + 1,
560
+ role: (h.source?.role || "assistant") as string,
561
+ content: (h.summary || h.excerpt || "").slice(0, 300),
562
+ time: h.source?.ts ? new Date(h.source.ts).toISOString().slice(0, 16) : "",
563
+ })),
564
+ ];
565
+
566
+ let filteredLocalHits = allHitsForFilter;
567
+ let filteredHubRemoteHits = hubRemoteForFilter;
568
+ let sufficient = false;
575
569
 
576
- if (searchScope !== "local") {
577
- const hub = await hubSearchMemories(store, ctx, { query, maxResults: searchLimit, scope: searchScope as any, hubAddress, userToken }).catch(() => ({ hits: [], meta: { totalCandidates: 0, searchedGroups: [], includedPublic: searchScope === "all" } }));
578
-
579
- let filteredHubHits = hub.hits;
580
- if (hub.hits.length > 0) {
581
- const hubCandidates = hub.hits.map((h, i) => ({
582
- index: filteredHits.length + i + 1,
583
- role: (h.source?.role || "assistant") as string,
584
- content: (h.summary || h.excerpt || "").slice(0, 300),
585
- time: h.source?.ts ? new Date(h.source.ts).toISOString().slice(0, 16) : "",
586
- }));
587
- const localCandidatesForMerge = filteredHits.map((h, i) => ({
588
- index: i + 1,
589
- role: h.source.role,
590
- content: (h.original_excerpt ?? "").slice(0, 300),
591
- time: h.source.ts ? new Date(h.source.ts).toISOString().slice(0, 16) : "",
592
- }));
593
- const mergedCandidates = [...localCandidatesForMerge, ...hubCandidates];
594
- const mergedFilter = await summarizer.filterRelevant(query, mergedCandidates);
595
- if (mergedFilter !== null && mergedFilter.relevant.length > 0) {
596
- const relevantSet = new Set(mergedFilter.relevant);
597
- const hubStartIdx = filteredHits.length + 1;
598
- filteredHits = filteredHits.filter((_, i) => relevantSet.has(i + 1));
599
- filteredHubHits = hub.hits.filter((_, i) => relevantSet.has(hubStartIdx + i));
600
- ctx.log.debug(`memory_search LLM filter (merged): local ${localCandidatesForMerge.length}→${filteredHits.length}, hub ${hub.hits.length}→${filteredHubHits.length}`);
570
+ if (mergedCandidates.length > 0) {
571
+ const filterResult = await summarizer.filterRelevant(query, mergedCandidates);
572
+ if (filterResult !== null) {
573
+ sufficient = filterResult.sufficient;
574
+ if (filterResult.relevant.length > 0) {
575
+ const relevantSet = new Set(filterResult.relevant);
576
+ const hubStartIdx = allHitsForFilter.length + 1;
577
+ filteredLocalHits = allHitsForFilter.filter((_, i) => relevantSet.has(i + 1));
578
+ filteredHubRemoteHits = hubRemoteForFilter.filter((_: any, i: number) => relevantSet.has(hubStartIdx + i));
579
+ ctx.log.debug(`memory_search LLM filter: merged ${mergedCandidates.length} local ${filteredLocalHits.length}, hub ${filteredHubRemoteHits.length}`);
580
+ } else {
581
+ filteredLocalHits = [];
582
+ filteredHubRemoteHits = [];
601
583
  }
602
584
  }
603
-
604
- const originLabel = (h: SearchHit) => {
605
- if (h.origin === "hub-memory") return " [团队缓存]";
606
- if (h.origin === "local-shared") return " [本机共享]";
607
- return "";
608
- };
609
- const localText = filteredHits.length > 0
610
- ? filteredHits.map((h, i) => {
611
- const excerpt = h.original_excerpt.length > 220 ? h.original_excerpt.slice(0, 217) + "..." : h.original_excerpt;
612
- return `${i + 1}. [${h.source.role}]${originLabel(h)} ${excerpt}`;
613
- }).join("\n")
614
- : "(none)";
615
- const hubText = filteredHubHits.length > 0
616
- ? filteredHubHits.map((h, i) => `${i + 1}. [${h.ownerName}] [团队] ${h.summary}${h.groupName ? ` (${h.groupName})` : ""}`).join("\n")
617
- : "(none)";
618
-
619
- const localDetailsFiltered = filteredHits.map((h) => {
620
- let effectiveTaskId = h.taskId;
621
- if (effectiveTaskId) {
622
- const t = store.getTask(effectiveTaskId);
623
- if (t && t.status === "skipped") effectiveTaskId = null;
624
- }
625
- return {
626
- ref: h.ref,
627
- chunkId: h.ref.chunkId,
628
- taskId: effectiveTaskId,
629
- skillId: h.skillId,
630
- role: h.source.role,
631
- score: h.score,
632
- summary: h.summary,
633
- origin: h.origin,
634
- };
635
- });
636
-
637
- return {
638
- content: [{
639
- type: "text",
640
- text: `Local results:\n${localText}\n\nHub results:\n${hubText}`,
641
- }],
642
- details: {
643
- local: { hits: localDetailsFiltered, meta: result.meta },
644
- hub: { ...hub, hits: filteredHubHits },
645
- },
646
- };
647
585
  }
648
586
 
649
- if (filteredHits.length === 0) {
587
+ const beforeDedup = filteredLocalHits.length;
588
+ filteredLocalHits = deduplicateHits(filteredLocalHits);
589
+ ctx.log.debug(`memory_search dedup: ${beforeDedup} → ${filteredLocalHits.length}`);
590
+
591
+ if (filteredLocalHits.length === 0 && filteredHubRemoteHits.length === 0) {
650
592
  return {
651
593
  content: [{ type: "text", text: "No relevant memories found for this query." }],
652
- details: { candidates: rawCandidates, filtered: [], meta: result.meta },
594
+ details: { candidates: rawLocalCandidates, hubCandidates: rawHubCandidates, filtered: [], meta: result.meta },
653
595
  };
654
596
  }
655
597
 
598
+ // ── Phase 3: Build response text ──
656
599
  const originTag = (o?: string) => {
657
600
  if (o === "local-shared") return " [本机共享]";
658
601
  if (o === "hub-memory") return " [团队缓存]";
659
602
  if (o === "hub-remote") return " [团队]";
660
603
  return "";
661
604
  };
662
- const lines = filteredHits.map((h, i) => {
663
- const excerpt = h.original_excerpt;
664
- const parts = [`${i + 1}. [${h.source.role}]${originTag(h.origin)}`];
665
- if (excerpt) parts.push(` ${excerpt}`);
605
+
606
+ const localLines = filteredLocalHits.map((h, i) => {
607
+ const excerpt = h.original_excerpt.length > 220 ? h.original_excerpt.slice(0, 217) + "..." : h.original_excerpt;
608
+ const parts = [`${i + 1}. [${h.source.role}]${originTag(h.origin)} ${excerpt}`];
666
609
  parts.push(` chunkId="${h.ref.chunkId}"`);
667
610
  if (h.taskId) {
668
611
  const task = store.getTask(h.taskId);
669
- if (task && task.status !== "skipped") {
670
- parts.push(` task_id="${h.taskId}"`);
671
- }
612
+ if (task && task.status !== "skipped") parts.push(` task_id="${h.taskId}"`);
672
613
  }
673
614
  return parts.join("\n");
674
615
  });
675
616
 
617
+ const hubLines = filteredHubRemoteHits.map((h: any, i: number) =>
618
+ `${i + 1}. [${h.ownerName ?? "team"}] [团队] ${h.summary ?? ""}${h.groupName ? ` (${h.groupName})` : ""}`
619
+ );
620
+
676
621
  let tipsText = "";
677
622
  if (!sufficient) {
678
- const hasTask = filteredHits.some((h) => {
623
+ const hasTask = filteredLocalHits.some((h) => {
679
624
  if (!h.taskId) return false;
680
625
  const t = store.getTask(h.taskId);
681
626
  return t && t.status !== "skipped";
682
627
  });
683
-
684
628
  const tips: string[] = [];
685
629
  if (hasTask) {
686
630
  tips.push("→ call task_summary(taskId) for full task context");
687
631
  tips.push("→ call skill_get(taskId=...) if the task has a proven experience guide");
688
632
  }
689
633
  tips.push("→ call memory_timeline(chunkId) to expand surrounding conversation");
690
-
691
- if (tips.length > 0) {
692
- tipsText = "\n\nThese memories may not be enough. You can fetch more context:\n" + tips.join("\n");
693
- }
634
+ if (tips.length > 0) tipsText = "\n\nThese memories may not be enough. You can fetch more context:\n" + tips.join("\n");
694
635
  }
695
636
 
637
+ const localText = localLines.length > 0 ? localLines.join("\n\n") : "(none)";
638
+ const hubText = hubLines.length > 0 ? hubLines.join("\n") : "(none)";
639
+ const totalFiltered = filteredLocalHits.length + filteredHubRemoteHits.length;
640
+ const responseText = filteredHubRemoteHits.length > 0
641
+ ? `Found ${totalFiltered} relevant memories:\n\nLocal results:\n${localText}\n\nHub results:\n${hubText}${tipsText}`
642
+ : `Found ${totalFiltered} relevant memories:\n\n${localText}${tipsText}`;
643
+
644
+ const filteredDetails = [
645
+ ...filteredLocalHits.map((h) => {
646
+ let effectiveTaskId = h.taskId;
647
+ if (effectiveTaskId) { const t = store.getTask(effectiveTaskId); if (t && t.status === "skipped") effectiveTaskId = null; }
648
+ return {
649
+ chunkId: h.ref.chunkId, taskId: effectiveTaskId, skillId: h.skillId,
650
+ role: h.source.role, score: h.score, summary: h.summary,
651
+ original_excerpt: (h.original_excerpt ?? "").slice(0, 200), origin: h.origin || "local",
652
+ };
653
+ }),
654
+ ...filteredHubRemoteHits.map((h: any) => ({
655
+ chunkId: "", taskId: null, skillId: null,
656
+ role: h.source?.role ?? h.role ?? "assistant", score: h.score ?? 0,
657
+ summary: h.summary ?? "", original_excerpt: (h.excerpt ?? h.summary ?? "").slice(0, 200),
658
+ origin: "hub-remote", ownerName: h.ownerName ?? "", groupName: h.groupName ?? "",
659
+ })),
660
+ ];
661
+
696
662
  return {
697
- content: [
698
- {
699
- type: "text",
700
- text: `Found ${filteredHits.length} relevant memories:\n\n${lines.join("\n\n")}${tipsText}`,
701
- },
702
- ],
663
+ content: [{ type: "text", text: responseText }],
703
664
  details: {
704
- candidates: rawCandidates,
705
- hits: filteredHits.map((h) => {
706
- let effectiveTaskId = h.taskId;
707
- if (effectiveTaskId) {
708
- const t = store.getTask(effectiveTaskId);
709
- if (t && t.status === "skipped") effectiveTaskId = null;
710
- }
711
- return {
712
- chunkId: h.ref.chunkId,
713
- taskId: effectiveTaskId,
714
- skillId: h.skillId,
715
- role: h.source.role,
716
- score: h.score,
717
- summary: h.summary,
718
- original_excerpt: (h.original_excerpt ?? "").slice(0, 200),
719
- origin: h.origin || "local",
720
- };
721
- }),
665
+ candidates: rawLocalCandidates,
666
+ hubCandidates: rawHubCandidates,
667
+ filtered: filteredDetails,
722
668
  meta: result.meta,
723
669
  },
724
670
  };
@@ -1639,16 +1585,32 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1639
1585
  };
1640
1586
  }
1641
1587
 
1642
- const localText = localHits.length > 0
1643
- ? localHits.map((h, i) => `${i + 1}. [${h.name}] ${h.description.slice(0, 150)}${h.visibility === "public" ? " (shared to local agents)" : ""}`).join("\n")
1588
+ let filteredLocal = localHits;
1589
+ let filteredHub = hub.hits;
1590
+ if (localHits.length > 0 && hub.hits.length > 0) {
1591
+ const allCandidates = [
1592
+ ...localHits.map((h, i) => ({ index: i + 1, role: "skill" as const, content: `[${h.name}] ${h.description.slice(0, 200)}` })),
1593
+ ...hub.hits.map((h, i) => ({ index: localHits.length + i + 1, role: "skill" as const, content: `[${h.name}] ${h.description.slice(0, 200)}` })),
1594
+ ];
1595
+ const mergedFilter = await summarizer.filterRelevant(skillQuery, allCandidates);
1596
+ if (mergedFilter !== null && mergedFilter.relevant.length > 0) {
1597
+ const relevantSet = new Set(mergedFilter.relevant);
1598
+ filteredLocal = localHits.filter((_, i) => relevantSet.has(i + 1));
1599
+ filteredHub = hub.hits.filter((_, i) => relevantSet.has(localHits.length + i + 1));
1600
+ ctx.log.debug(`skill_search LLM filter (merged): local ${localHits.length}→${filteredLocal.length}, hub ${hub.hits.length}→${filteredHub.length}`);
1601
+ }
1602
+ }
1603
+
1604
+ const localText = filteredLocal.length > 0
1605
+ ? filteredLocal.map((h, i) => `${i + 1}. [${h.name}] ${h.description.slice(0, 150)}${h.visibility === "public" ? " (shared to local agents)" : ""}`).join("\n")
1644
1606
  : "(none)";
1645
- const hubText = hub.hits.length > 0
1646
- ? hub.hits.map((h, i) => `${i + 1}. [${h.name}] ${h.description.slice(0, 150)} (${h.visibility}${h.groupName ? `:${h.groupName}` : ""}, owner=${h.ownerName})`).join("\n")
1607
+ const hubText = filteredHub.length > 0
1608
+ ? filteredHub.map((h, i) => `${i + 1}. [${h.name}] ${h.description.slice(0, 150)} (${h.visibility}${h.groupName ? `:${h.groupName}` : ""}, owner=${h.ownerName})`).join("\n")
1647
1609
  : "(none)";
1648
1610
 
1649
1611
  return {
1650
1612
  content: [{ type: "text", text: `Local skills:\n${localText}\n\nHub skills:\n${hubText}` }],
1651
- details: { query: skillQuery, scope: rawScope, local: { hits: localHits }, hub },
1613
+ details: { query: skillQuery, scope: rawScope, local: { hits: filteredLocal }, hub: { hits: filteredHub } },
1652
1614
  };
1653
1615
  }
1654
1616
 
@@ -1853,46 +1815,53 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1853
1815
  }
1854
1816
  ctx.log.debug(`auto-recall: query="${query.slice(0, 80)}"`);
1855
1817
 
1856
- const result = await engine.search({ query, maxResults: 10, minScore: 0.45, ownerFilter: recallOwnerFilter });
1818
+ // ── Phase 1: Local search Hub search (parallel) ──
1819
+ const arLocalP = engine.search({ query, maxResults: 10, minScore: 0.45, ownerFilter: recallOwnerFilter });
1820
+ const arHubP = ctx.config?.sharing?.enabled
1821
+ ? hubSearchMemories(store, ctx, { query, maxResults: 10, scope: "all" })
1822
+ .catch((err: any) => { ctx.log.debug(`auto-recall: hub search failed (${err})`); return { hits: [] as any[], meta: {} }; })
1823
+ : Promise.resolve({ hits: [] as any[], meta: {} });
1824
+
1825
+ const [result, arHubResult] = await Promise.all([arLocalP, arHubP]);
1826
+
1827
+ const localHits = result.hits.filter((h) => h.origin !== "hub-memory");
1828
+ const hubLocalHits = result.hits.filter((h) => h.origin === "hub-memory");
1829
+ const hubRemoteHits: SearchHit[] = (arHubResult.hits ?? []).map((h: any) => ({
1830
+ summary: h.summary,
1831
+ original_excerpt: h.excerpt || h.summary,
1832
+ ref: { sessionKey: "", chunkId: h.remoteHitId ?? "", turnId: "", seq: 0 },
1833
+ score: 0.9,
1834
+ taskId: null,
1835
+ skillId: null,
1836
+ origin: "hub-remote" as const,
1837
+ source: { ts: h.source?.ts, role: h.source?.role ?? "assistant", sessionKey: "" },
1838
+ ownerName: h.ownerName,
1839
+ groupName: h.groupName,
1840
+ }));
1841
+ const allHubHits = [...hubLocalHits, ...hubRemoteHits];
1857
1842
 
1858
- // Hub fallback helper: search team shared memories when local search has no relevant results
1859
- const hubFallback = async (): Promise<SearchHit[]> => {
1860
- if (!ctx.config?.sharing?.enabled) return [];
1861
- try {
1862
- const hubResult = await hubSearchMemories(store, ctx, { query, maxResults: 10, scope: "all" });
1863
- if (hubResult.hits.length === 0) return [];
1864
- ctx.log.debug(`auto-recall: hub fallback returned ${hubResult.hits.length} hit(s)`);
1865
- return hubResult.hits.map((h) => ({
1866
- summary: h.summary,
1867
- original_excerpt: h.excerpt || h.summary,
1868
- ref: { sessionKey: "", chunkId: h.remoteHitId, turnId: "", seq: 0 },
1869
- score: 0.9,
1870
- taskId: null,
1871
- skillId: null,
1872
- origin: "hub-remote" as const,
1873
- source: { ts: h.source.ts, role: h.source.role, sessionKey: "" },
1874
- }));
1875
- } catch (err) {
1876
- ctx.log.debug(`auto-recall: hub fallback failed (${err})`);
1877
- return [];
1878
- }
1879
- };
1843
+ ctx.log.debug(`auto-recall: local=${localHits.length}, hub-memory=${hubLocalHits.length}, hub-remote=${hubRemoteHits.length}`);
1880
1844
 
1881
- if (result.hits.length === 0) {
1882
- // Local found nothing try hub before giving up
1883
- const hubHits = await hubFallback();
1884
- if (hubHits.length > 0) {
1885
- result.hits.push(...hubHits);
1886
- ctx.log.debug(`auto-recall: local empty, using ${hubHits.length} hub hit(s)`);
1887
- }
1888
- }
1889
- if (result.hits.length === 0) {
1845
+ const rawLocalCandidates = localHits.map((h) => ({
1846
+ score: h.score, role: h.source.role, summary: h.summary,
1847
+ content: (h.original_excerpt ?? "").slice(0, 200), origin: h.origin || "local",
1848
+ }));
1849
+ const rawHubCandidates = allHubHits.map((h) => ({
1850
+ score: h.score, role: h.source.role, summary: h.summary,
1851
+ content: (h.original_excerpt ?? "").slice(0, 200), origin: h.origin || "hub-remote",
1852
+ ownerName: (h as any).ownerName ?? "", groupName: (h as any).groupName ?? "",
1853
+ }));
1854
+
1855
+ const allRawHits = [...localHits, ...allHubHits];
1856
+
1857
+ if (allRawHits.length === 0) {
1890
1858
  ctx.log.debug("auto-recall: no memory candidates found");
1891
1859
  const dur = performance.now() - recallT0;
1892
1860
  store.recordToolCall("memory_search", dur, true);
1893
- store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({ candidates: [], filtered: [] }), dur, true);
1861
+ store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({
1862
+ candidates: rawLocalCandidates, hubCandidates: rawHubCandidates, filtered: [],
1863
+ }), dur, true);
1894
1864
 
1895
- // Even without memory hits, try skill recall
1896
1865
  const skillAutoRecallEarly = ctx.config.skillEvolution?.autoRecallSkills ?? DEFAULTS.skillAutoRecall;
1897
1866
  if (skillAutoRecallEarly) {
1898
1867
  try {
@@ -1932,59 +1901,44 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1932
1901
  return;
1933
1902
  }
1934
1903
 
1935
- const candidates = result.hits.map((h, i) => ({
1904
+ // ── Phase 2: Merge all → single LLM filter ──
1905
+ const mergedForFilter = allRawHits.map((h, i) => ({
1936
1906
  index: i + 1,
1937
1907
  role: h.source.role,
1938
1908
  content: (h.original_excerpt ?? "").slice(0, 300),
1939
1909
  time: h.source.ts ? new Date(h.source.ts).toISOString().slice(0, 16) : "",
1940
1910
  }));
1941
1911
 
1942
- let filteredHits = result.hits;
1912
+ let filteredHits = allRawHits;
1943
1913
  let sufficient = false;
1944
1914
 
1945
- const filterResult = await summarizer.filterRelevant(query, candidates);
1915
+ const filterResult = await summarizer.filterRelevant(query, mergedForFilter);
1946
1916
  if (filterResult !== null) {
1947
1917
  sufficient = filterResult.sufficient;
1948
1918
  if (filterResult.relevant.length > 0) {
1949
1919
  const indexSet = new Set(filterResult.relevant);
1950
- filteredHits = result.hits.filter((_, i) => indexSet.has(i + 1));
1920
+ filteredHits = allRawHits.filter((_, i) => indexSet.has(i + 1));
1951
1921
  } else {
1952
- ctx.log.debug("auto-recall: LLM filter returned no relevant local hits, trying hub fallback");
1953
- const hubHits = await hubFallback();
1954
- if (hubHits.length > 0) {
1955
- ctx.log.debug(`auto-recall: hub fallback provided ${hubHits.length} hit(s) after local filter yielded 0`);
1956
- filteredHits = hubHits;
1957
- } else {
1958
- const dur = performance.now() - recallT0;
1959
- store.recordToolCall("memory_search", dur, true);
1960
- store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({
1961
- candidates: result.hits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt, origin: h.origin || "local" })),
1962
- filtered: []
1963
- }), dur, true);
1964
- if (query.length > 50) {
1965
- const noRecallHint =
1966
- "## Memory system — ACTION REQUIRED\n\n" +
1967
- "Auto-recall found no relevant results for a long query. " +
1968
- "You MUST call `memory_search` now with a shortened query (2-5 key words) before answering. " +
1969
- "Do NOT skip this step. Do NOT answer without searching first.";
1970
- return { prependContext: noRecallHint };
1971
- }
1972
- return;
1922
+ const dur = performance.now() - recallT0;
1923
+ store.recordToolCall("memory_search", dur, true);
1924
+ store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({
1925
+ candidates: rawLocalCandidates, hubCandidates: rawHubCandidates, filtered: [],
1926
+ }), dur, true);
1927
+ if (query.length > 50) {
1928
+ const noRecallHint =
1929
+ "## Memory system — ACTION REQUIRED\n\n" +
1930
+ "Auto-recall found no relevant results for a long query. " +
1931
+ "You MUST call `memory_search` now with a shortened query (2-5 key words) before answering. " +
1932
+ "Do NOT skip this step. Do NOT answer without searching first.";
1933
+ return { prependContext: noRecallHint };
1973
1934
  }
1974
- }
1975
- }
1976
-
1977
- if (!sufficient && filteredHits.length > 0 && ctx.config?.sharing?.enabled) {
1978
- const hubSupp = await hubFallback();
1979
- if (hubSupp.length > 0) {
1980
- ctx.log.debug(`auto-recall: local insufficient, supplementing with ${hubSupp.length} hub hit(s)`);
1981
- filteredHits.push(...hubSupp);
1935
+ return;
1982
1936
  }
1983
1937
  }
1984
1938
 
1985
1939
  const beforeDedup = filteredHits.length;
1986
1940
  filteredHits = deduplicateHits(filteredHits);
1987
- ctx.log.debug(`auto-recall: ${result.hits.length} → ${beforeDedup} relevant → ${filteredHits.length} after dedup, sufficient=${sufficient}`);
1941
+ ctx.log.debug(`auto-recall: merged ${allRawHits.length} → ${beforeDedup} relevant → ${filteredHits.length} after dedup, sufficient=${sufficient}`);
1988
1942
 
1989
1943
  const lines = filteredHits.map((h, i) => {
1990
1944
  const excerpt = h.original_excerpt;
@@ -2098,8 +2052,9 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
2098
2052
  const recallDur = performance.now() - recallT0;
2099
2053
  store.recordToolCall("memory_search", recallDur, true);
2100
2054
  store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({
2101
- candidates: result.hits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt, origin: h.origin || "local" })),
2102
- filtered: filteredHits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt, origin: h.origin || "local" }))
2055
+ candidates: rawLocalCandidates,
2056
+ 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" })),
2103
2058
  }), recallDur, true);
2104
2059
  telemetry.trackAutoRecall(filteredHits.length, recallDur);
2105
2060
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memtensor/memos-local-openclaw-plugin",
3
- "version": "1.0.6-beta.1",
3
+ "version": "1.0.6-beta.3",
4
4
  "description": "MemOS Local memory plugin for OpenClaw — full-write, hybrid-recall, progressive retrieval",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -229,22 +229,28 @@ export async function getHubStatus(store: SqliteStore, config: MemosLocalConfig)
229
229
  body: JSON.stringify({ teamToken, userId: conn.userId }),
230
230
  }) as any;
231
231
  if (regResult.status === "active" && regResult.userToken) {
232
- store.setClientHubConnection({
232
+ const updatedConn = {
233
233
  ...conn,
234
234
  hubUrl: normalizeHubUrl(hubAddress),
235
235
  userToken: regResult.userToken,
236
236
  connectedAt: Date.now(),
237
237
  lastKnownStatus: "active",
238
- });
238
+ };
239
+ store.setClientHubConnection(updatedConn);
239
240
  try {
240
241
  const me = await hubRequestJson(normalizeHubUrl(hubAddress), regResult.userToken, "/api/v1/hub/me", { method: "GET" }) as any;
242
+ const latestUsername = String(me.username ?? "");
243
+ const latestRole = String(me.role ?? "member") as UserRole;
244
+ if (latestUsername !== conn.username || latestRole !== conn.role) {
245
+ store.setClientHubConnection({ ...updatedConn, username: latestUsername, role: latestRole });
246
+ }
241
247
  return {
242
248
  connected: true,
243
249
  hubUrl: normalizeHubUrl(hubAddress),
244
250
  user: {
245
251
  id: String(me.id),
246
- username: String(me.username ?? ""),
247
- role: String(me.role ?? "member") as UserRole,
252
+ username: latestUsername,
253
+ role: latestRole,
248
254
  status: String(me.status ?? "active"),
249
255
  groups: Array.isArray(me.groups) ? me.groups : [],
250
256
  },