@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.
- package/dist/client/connector.d.ts.map +1 -1
- package/dist/client/connector.js +10 -4
- package/dist/client/connector.js.map +1 -1
- package/dist/hub/server.d.ts +2 -0
- package/dist/hub/server.d.ts.map +1 -1
- package/dist/hub/server.js +108 -54
- package/dist/hub/server.js.map +1 -1
- package/dist/ingest/providers/index.d.ts +4 -0
- package/dist/ingest/providers/index.d.ts.map +1 -1
- package/dist/ingest/providers/index.js +20 -4
- package/dist/ingest/providers/index.js.map +1 -1
- package/dist/ingest/providers/openai.d.ts +0 -3
- package/dist/ingest/providers/openai.d.ts.map +1 -1
- package/dist/ingest/providers/openai.js +9 -8
- package/dist/ingest/providers/openai.js.map +1 -1
- package/dist/recall/engine.d.ts.map +1 -1
- package/dist/recall/engine.js +35 -43
- package/dist/recall/engine.js.map +1 -1
- package/dist/storage/sqlite.d.ts +13 -0
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +43 -1
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/viewer/html.d.ts.map +1 -1
- package/dist/viewer/html.js +110 -22
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +86 -34
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +208 -253
- package/package.json +1 -1
- package/src/client/connector.ts +10 -4
- package/src/hub/server.ts +95 -51
- package/src/ingest/providers/index.ts +26 -18
- package/src/ingest/providers/openai.ts +5 -4
- package/src/recall/engine.ts +34 -41
- package/src/storage/sqlite.ts +43 -1
- package/src/viewer/html.ts +110 -22
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
|
|
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
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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 (
|
|
577
|
-
const
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
663
|
-
|
|
664
|
-
const
|
|
665
|
-
|
|
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 =
|
|
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:
|
|
705
|
-
|
|
706
|
-
|
|
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
|
-
|
|
1643
|
-
|
|
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 =
|
|
1646
|
-
?
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
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({
|
|
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
|
-
|
|
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 =
|
|
1912
|
+
let filteredHits = allRawHits;
|
|
1943
1913
|
let sufficient = false;
|
|
1944
1914
|
|
|
1945
|
-
const filterResult = await summarizer.filterRelevant(query,
|
|
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 =
|
|
1920
|
+
filteredHits = allRawHits.filter((_, i) => indexSet.has(i + 1));
|
|
1951
1921
|
} else {
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
const
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
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: ${
|
|
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:
|
|
2102
|
-
|
|
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
package/src/client/connector.ts
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
247
|
-
role:
|
|
252
|
+
username: latestUsername,
|
|
253
|
+
role: latestRole,
|
|
248
254
|
status: String(me.status ?? "active"),
|
|
249
255
|
groups: Array.isArray(me.groups) ? me.groups : [],
|
|
250
256
|
},
|