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

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 (56) hide show
  1. package/dist/capture/index.d.ts.map +1 -1
  2. package/dist/capture/index.js +24 -0
  3. package/dist/capture/index.js.map +1 -1
  4. package/dist/client/connector.d.ts.map +1 -1
  5. package/dist/client/connector.js +23 -1
  6. package/dist/client/connector.js.map +1 -1
  7. package/dist/client/hub.d.ts.map +1 -1
  8. package/dist/client/hub.js +4 -0
  9. package/dist/client/hub.js.map +1 -1
  10. package/dist/hub/server.d.ts +1 -1
  11. package/dist/hub/server.d.ts.map +1 -1
  12. package/dist/hub/server.js +39 -31
  13. package/dist/hub/server.js.map +1 -1
  14. package/dist/ingest/providers/index.d.ts.map +1 -1
  15. package/dist/ingest/providers/index.js +16 -86
  16. package/dist/ingest/providers/index.js.map +1 -1
  17. package/dist/ingest/providers/openai.d.ts +3 -0
  18. package/dist/ingest/providers/openai.d.ts.map +1 -1
  19. package/dist/ingest/providers/openai.js +34 -19
  20. package/dist/ingest/providers/openai.js.map +1 -1
  21. package/dist/recall/engine.d.ts.map +1 -1
  22. package/dist/recall/engine.js +28 -19
  23. package/dist/recall/engine.js.map +1 -1
  24. package/dist/storage/sqlite.d.ts +30 -7
  25. package/dist/storage/sqlite.d.ts.map +1 -1
  26. package/dist/storage/sqlite.js +139 -60
  27. package/dist/storage/sqlite.js.map +1 -1
  28. package/dist/tools/memory-get.d.ts.map +1 -1
  29. package/dist/tools/memory-get.js +4 -1
  30. package/dist/tools/memory-get.js.map +1 -1
  31. package/dist/types.d.ts +1 -1
  32. package/dist/types.d.ts.map +1 -1
  33. package/dist/types.js.map +1 -1
  34. package/dist/viewer/server.d.ts +24 -0
  35. package/dist/viewer/server.d.ts.map +1 -1
  36. package/dist/viewer/server.js +332 -130
  37. package/dist/viewer/server.js.map +1 -1
  38. package/index.ts +65 -29
  39. package/package.json +1 -1
  40. package/scripts/postinstall.cjs +21 -5
  41. package/src/capture/index.ts +36 -0
  42. package/src/client/connector.ts +22 -1
  43. package/src/client/hub.ts +4 -0
  44. package/src/hub/server.ts +42 -26
  45. package/src/ingest/providers/index.ts +30 -93
  46. package/src/ingest/providers/openai.ts +32 -15
  47. package/src/recall/engine.ts +28 -19
  48. package/src/storage/sqlite.ts +156 -65
  49. package/src/tools/memory-get.ts +4 -1
  50. package/src/types.ts +2 -0
  51. package/src/viewer/server.ts +313 -125
  52. package/prebuilds/darwin-arm64/better_sqlite3.node +0 -0
  53. package/prebuilds/darwin-x64/better_sqlite3.node +0 -0
  54. package/prebuilds/linux-x64/better_sqlite3.node +0 -0
  55. package/prebuilds/win32-x64/better_sqlite3.node +0 -0
  56. package/telemetry.credentials.json +0 -5
@@ -1,7 +1,20 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import type { SummarizerConfig, SummaryProvider, Logger, OpenClawAPI } from "../../types";
4
- import { summarizeOpenAI, summarizeTaskOpenAI, generateTaskTitleOpenAI, judgeNewTopicOpenAI, filterRelevantOpenAI, judgeDedupOpenAI, parseFilterResult, parseDedupResult } from "./openai";
4
+ import {
5
+ summarizeOpenAI,
6
+ summarizeTaskOpenAI,
7
+ generateTaskTitleOpenAI,
8
+ judgeNewTopicOpenAI,
9
+ filterRelevantOpenAI,
10
+ judgeDedupOpenAI,
11
+ parseFilterResult,
12
+ parseDedupResult,
13
+ TASK_SUMMARY_PROMPT,
14
+ TOPIC_JUDGE_PROMPT,
15
+ FILTER_RELEVANT_PROMPT,
16
+ DEDUP_JUDGE_PROMPT,
17
+ } from "./openai";
5
18
  import type { FilterResult, DedupResult } from "./openai";
6
19
  export type { FilterResult, DedupResult } from "./openai";
7
20
  import { summarizeAnthropic, summarizeTaskAnthropic, generateTaskTitleAnthropic, judgeNewTopicAnthropic, filterRelevantAnthropic, judgeDedupAnthropic } from "./anthropic";
@@ -360,7 +373,7 @@ export class Summarizer {
360
373
  private async summarizeTaskOpenClaw(text: string): Promise<string> {
361
374
  this.requireOpenClawAPI();
362
375
  const prompt = [
363
- OPENCLAW_TASK_SUMMARY_PROMPT,
376
+ TASK_SUMMARY_PROMPT,
364
377
  ``,
365
378
  text,
366
379
  ].join("\n");
@@ -378,7 +391,7 @@ export class Summarizer {
378
391
  private async judgeNewTopicOpenClaw(currentContext: string, newMessage: string): Promise<boolean> {
379
392
  this.requireOpenClawAPI();
380
393
  const prompt = [
381
- OPENCLAW_TOPIC_JUDGE_PROMPT,
394
+ TOPIC_JUDGE_PROMPT,
382
395
  ``,
383
396
  `CURRENT CONVERSATION SUMMARY:`,
384
397
  currentContext,
@@ -409,7 +422,7 @@ export class Summarizer {
409
422
  .join("\n");
410
423
 
411
424
  const prompt = [
412
- OPENCLAW_FILTER_RELEVANT_PROMPT,
425
+ FILTER_RELEVANT_PROMPT,
413
426
  ``,
414
427
  `QUERY: ${query}`,
415
428
  ``,
@@ -437,7 +450,7 @@ export class Summarizer {
437
450
  .join("\n");
438
451
 
439
452
  const prompt = [
440
- OPENCLAW_DEDUP_JUDGE_PROMPT,
453
+ DEDUP_JUDGE_PROMPT,
441
454
  ``,
442
455
  `NEW MEMORY:`,
443
456
  newSummary,
@@ -466,6 +479,8 @@ function callSummarize(cfg: SummarizerConfig, text: string, log: Logger): Promis
466
479
  case "azure_openai":
467
480
  case "zhipu":
468
481
  case "siliconflow":
482
+ case "deepseek":
483
+ case "moonshot":
469
484
  case "bailian":
470
485
  case "cohere":
471
486
  case "mistral":
@@ -489,6 +504,8 @@ function callSummarizeTask(cfg: SummarizerConfig, text: string, log: Logger): Pr
489
504
  case "azure_openai":
490
505
  case "zhipu":
491
506
  case "siliconflow":
507
+ case "deepseek":
508
+ case "moonshot":
492
509
  case "bailian":
493
510
  case "cohere":
494
511
  case "mistral":
@@ -512,6 +529,8 @@ function callGenerateTaskTitle(cfg: SummarizerConfig, text: string, log: Logger)
512
529
  case "azure_openai":
513
530
  case "zhipu":
514
531
  case "siliconflow":
532
+ case "deepseek":
533
+ case "moonshot":
515
534
  case "bailian":
516
535
  case "cohere":
517
536
  case "mistral":
@@ -535,6 +554,8 @@ function callTopicJudge(cfg: SummarizerConfig, currentContext: string, newMessag
535
554
  case "azure_openai":
536
555
  case "zhipu":
537
556
  case "siliconflow":
557
+ case "deepseek":
558
+ case "moonshot":
538
559
  case "bailian":
539
560
  case "cohere":
540
561
  case "mistral":
@@ -558,6 +579,8 @@ function callFilterRelevant(cfg: SummarizerConfig, query: string, candidates: Ar
558
579
  case "azure_openai":
559
580
  case "zhipu":
560
581
  case "siliconflow":
582
+ case "deepseek":
583
+ case "moonshot":
561
584
  case "bailian":
562
585
  case "cohere":
563
586
  case "mistral":
@@ -581,6 +604,8 @@ function callJudgeDedup(cfg: SummarizerConfig, newSummary: string, candidates: A
581
604
  case "azure_openai":
582
605
  case "zhipu":
583
606
  case "siliconflow":
607
+ case "deepseek":
608
+ case "moonshot":
584
609
  case "bailian":
585
610
  case "cohere":
586
611
  case "mistral":
@@ -629,91 +654,3 @@ function wordCount(text: string): number {
629
654
  if (noCjk) count += noCjk.split(/\s+/).filter(Boolean).length;
630
655
  return count;
631
656
  }
632
-
633
- // ─── OpenClaw Prompt Templates ───
634
-
635
- const OPENCLAW_TASK_SUMMARY_PROMPT = `You create a DETAILED task summary from a multi-turn conversation. This summary will be the ONLY record of this conversation, so it must preserve ALL important information.
636
-
637
- CRITICAL LANGUAGE RULE: You MUST write in the SAME language as the user's messages. Chinese input → Chinese output. English input → English output. NEVER mix languages.
638
-
639
- Output EXACTLY this structure:
640
-
641
- 📌 Title
642
- A short, descriptive title (10-30 characters). Like a chat group name.
643
-
644
- 🎯 Goal
645
- One sentence: what the user wanted to accomplish.
646
-
647
- 📋 Key Steps
648
- - Describe each meaningful step in detail
649
- - Include the ACTUAL content produced: code snippets, commands, config blocks, formulas, key paragraphs
650
- - For code: include the function signature and core logic (up to ~30 lines per block), use fenced code blocks
651
- - For configs: include the actual config values and structure
652
- - For lists/instructions: include the actual items, not just "provided a list"
653
- - Merge only truly trivial back-and-forth (like "ok" / "sure")
654
- - Do NOT over-summarize: "provided a function" is BAD; show the actual function
655
-
656
- ✅ Result
657
- What was the final outcome? Include the final version of any code/config/content produced.
658
-
659
- 💡 Key Details
660
- - Decisions made, trade-offs discussed, caveats noted, alternative approaches mentioned
661
- - Specific values: numbers, versions, thresholds, URLs, file paths, model names
662
- - Omit this section only if there truly are no noteworthy details
663
-
664
- RULES:
665
- - This summary is a KNOWLEDGE BASE ENTRY, not a brief note. Be thorough.
666
- - PRESERVE verbatim: code, commands, URLs, file paths, error messages, config values, version numbers, names, amounts
667
- - DISCARD only: greetings, filler, the assistant explaining what it will do before doing it
668
- - Replace secrets (API keys, tokens, passwords) with [REDACTED]
669
- - Target length: 30-50% of the original conversation length. Longer conversations need longer summaries.
670
- - Output summary only, no preamble.`;
671
-
672
- const OPENCLAW_TOPIC_JUDGE_PROMPT = `You are a conversation topic boundary detector. Given a summary of the CURRENT conversation and a NEW user message, determine if the new message starts a DIFFERENT topic/task.
673
-
674
- Answer ONLY "NEW" or "SAME".
675
-
676
- Rules:
677
- - "NEW" = the new message is about a completely different subject, project, or task
678
- - "SAME" = the new message continues, follows up on, or is closely related to the current topic
679
- - Follow-up questions, clarifications, refinements, bug fixes, or next steps on the same task = SAME
680
- - Greetings or meta-questions like "你好" or "谢谢" without new substance = SAME
681
- - A clearly unrelated request (e.g., current topic is deployment, new message asks about cooking) = NEW
682
-
683
- Output exactly one word: NEW or SAME`;
684
-
685
- const OPENCLAW_FILTER_RELEVANT_PROMPT = `You are a memory relevance judge. Given a user's QUERY and a list of CANDIDATE memory summaries, do two things:
686
-
687
- 1. Select ALL candidates that could be useful for answering the query. When in doubt, INCLUDE the candidate.
688
- - For questions about lists, history, or "what/where/who" across multiple items, include ALL matching items.
689
- - For factual lookups, a single direct answer is enough.
690
- 2. Judge whether the selected memories are SUFFICIENT to fully answer the query WITHOUT fetching additional context.
691
-
692
- IMPORTANT for "sufficient" judgment:
693
- - sufficient=true ONLY when the memories contain a concrete ANSWER, fact, decision, or actionable information that directly addresses the query.
694
- - sufficient=false when the memories only repeat the question, show related topics but lack the specific detail, or contain partial information.
695
-
696
- Output a JSON object with exactly two fields:
697
- {"relevant":[1,3,5],"sufficient":true}
698
-
699
- - "relevant": array of candidate numbers that are useful. Empty array [] if none are relevant.
700
- - "sufficient": true ONLY if the memories contain a direct answer; false otherwise.
701
-
702
- Output ONLY the JSON object, nothing else.`;
703
-
704
- const OPENCLAW_DEDUP_JUDGE_PROMPT = `You are a memory deduplication system. Given a NEW memory summary and several EXISTING memory summaries, determine the relationship.
705
-
706
- For each EXISTING memory, the NEW memory is either:
707
- - "DUPLICATE": NEW is fully covered by an EXISTING memory — no new information at all
708
- - "UPDATE": NEW contains information that supplements or updates an EXISTING memory (new data, status change, additional detail)
709
- - "NEW": NEW is a different topic/event despite surface similarity
710
-
711
- Pick the BEST match among all candidates. If none match well, choose "NEW".
712
-
713
- Output a single JSON object:
714
- - If DUPLICATE: {"action":"DUPLICATE","targetIndex":2,"reason":"..."}
715
- - If UPDATE: {"action":"UPDATE","targetIndex":3,"reason":"...","mergedSummary":"a combined summary preserving all info from both old and new, same language as input"}
716
- - If NEW: {"action":"NEW","reason":"..."}
717
-
718
- CRITICAL: mergedSummary must use the SAME language as the input. Output ONLY the JSON object.`;
719
-
@@ -16,7 +16,7 @@ Requirements:
16
16
  - Non-Chinese: 5-15 words (aim for 8-12)
17
17
  - Output title only`;
18
18
 
19
- const TASK_SUMMARY_PROMPT = `You create a DETAILED task summary from a multi-turn conversation. This summary will be the ONLY record of this conversation, so it must preserve ALL important information.
19
+ export const TASK_SUMMARY_PROMPT = `You create a DETAILED task summary from a multi-turn conversation. This summary will be the ONLY record of this conversation, so it must preserve ALL important information.
20
20
 
21
21
  ## LANGUAGE RULE (HIGHEST PRIORITY)
22
22
  Detect the PRIMARY language of the user's messages. If most user messages are Chinese, ALL output (title, goal, steps, result, details) MUST be in Chinese. If English, output in English. NEVER mix. This rule overrides everything below.
@@ -70,7 +70,7 @@ export async function summarizeTaskOpenAI(
70
70
  const resp = await fetch(endpoint, {
71
71
  method: "POST",
72
72
  headers,
73
- body: JSON.stringify({
73
+ body: JSON.stringify(buildRequestBody(cfg, {
74
74
  model,
75
75
  temperature: cfg.temperature ?? 0.1,
76
76
  max_tokens: 4096,
@@ -78,7 +78,7 @@ export async function summarizeTaskOpenAI(
78
78
  { role: "system", content: TASK_SUMMARY_PROMPT },
79
79
  { role: "user", content: text },
80
80
  ],
81
- }),
81
+ })),
82
82
  signal: AbortSignal.timeout(cfg.timeoutMs ?? 60_000),
83
83
  });
84
84
 
@@ -119,7 +119,7 @@ export async function generateTaskTitleOpenAI(
119
119
  const resp = await fetch(endpoint, {
120
120
  method: "POST",
121
121
  headers,
122
- body: JSON.stringify({
122
+ body: JSON.stringify(buildRequestBody(cfg, {
123
123
  model,
124
124
  temperature: 0,
125
125
  max_tokens: 100,
@@ -127,7 +127,7 @@ export async function generateTaskTitleOpenAI(
127
127
  { role: "system", content: TASK_TITLE_PROMPT },
128
128
  { role: "user", content: text },
129
129
  ],
130
- }),
130
+ })),
131
131
  signal: AbortSignal.timeout(cfg.timeoutMs ?? 15_000),
132
132
  });
133
133
 
@@ -156,14 +156,14 @@ export async function summarizeOpenAI(
156
156
  const resp = await fetch(endpoint, {
157
157
  method: "POST",
158
158
  headers,
159
- body: JSON.stringify({
159
+ body: JSON.stringify(buildRequestBody(cfg, {
160
160
  model,
161
161
  temperature: cfg.temperature ?? 0,
162
162
  messages: [
163
163
  { role: "system", content: SYSTEM_PROMPT },
164
164
  { role: "user", content: `[TEXT TO SUMMARIZE]\n${text}\n[/TEXT TO SUMMARIZE]` },
165
165
  ],
166
- }),
166
+ })),
167
167
  signal: AbortSignal.timeout(cfg.timeoutMs ?? 30_000),
168
168
  });
169
169
 
@@ -178,7 +178,7 @@ export async function summarizeOpenAI(
178
178
  return json.choices[0]?.message?.content?.trim() ?? "";
179
179
  }
180
180
 
181
- const TOPIC_JUDGE_PROMPT = `You are a conversation topic boundary detector. Given the CURRENT task context and a NEW user message, decide if the new message belongs to the SAME task or starts a NEW one.
181
+ export const TOPIC_JUDGE_PROMPT = `You are a conversation topic boundary detector. Given the CURRENT task context and a NEW user message, decide if the new message belongs to the SAME task or starts a NEW one.
182
182
 
183
183
  Answer ONLY "NEW" or "SAME".
184
184
 
@@ -223,7 +223,7 @@ export async function judgeNewTopicOpenAI(
223
223
  const resp = await fetch(endpoint, {
224
224
  method: "POST",
225
225
  headers,
226
- body: JSON.stringify({
226
+ body: JSON.stringify(buildRequestBody(cfg, {
227
227
  model,
228
228
  temperature: 0,
229
229
  max_tokens: 10,
@@ -231,7 +231,7 @@ export async function judgeNewTopicOpenAI(
231
231
  { role: "system", content: TOPIC_JUDGE_PROMPT },
232
232
  { role: "user", content: userContent },
233
233
  ],
234
- }),
234
+ })),
235
235
  signal: AbortSignal.timeout(cfg.timeoutMs ?? 15_000),
236
236
  });
237
237
 
@@ -246,7 +246,7 @@ export async function judgeNewTopicOpenAI(
246
246
  return answer.startsWith("NEW");
247
247
  }
248
248
 
249
- const FILTER_RELEVANT_PROMPT = `You are a memory relevance judge.
249
+ export const FILTER_RELEVANT_PROMPT = `You are a memory relevance judge.
250
250
 
251
251
  Given a QUERY and CANDIDATE memories, decide: does each candidate's content contain information that would HELP ANSWER the query?
252
252
 
@@ -293,7 +293,7 @@ export async function filterRelevantOpenAI(
293
293
  const resp = await fetch(endpoint, {
294
294
  method: "POST",
295
295
  headers,
296
- body: JSON.stringify({
296
+ body: JSON.stringify(buildRequestBody(cfg, {
297
297
  model,
298
298
  temperature: 0,
299
299
  max_tokens: 200,
@@ -301,7 +301,7 @@ export async function filterRelevantOpenAI(
301
301
  { role: "system", content: FILTER_RELEVANT_PROMPT },
302
302
  { role: "user", content: `QUERY: ${query}\n\nCANDIDATES:\n${candidateText}` },
303
303
  ],
304
- }),
304
+ })),
305
305
  signal: AbortSignal.timeout(cfg.timeoutMs ?? 15_000),
306
306
  });
307
307
 
@@ -385,7 +385,7 @@ export async function judgeDedupOpenAI(
385
385
  const resp = await fetch(endpoint, {
386
386
  method: "POST",
387
387
  headers,
388
- body: JSON.stringify({
388
+ body: JSON.stringify(buildRequestBody(cfg, {
389
389
  model,
390
390
  temperature: 0,
391
391
  max_tokens: 300,
@@ -393,7 +393,7 @@ export async function judgeDedupOpenAI(
393
393
  { role: "system", content: DEDUP_JUDGE_PROMPT },
394
394
  { role: "user", content: `NEW MEMORY:\n${newSummary}\n\nEXISTING MEMORIES:\n${candidateText}` },
395
395
  ],
396
- }),
396
+ })),
397
397
  signal: AbortSignal.timeout(cfg.timeoutMs ?? 15_000),
398
398
  });
399
399
 
@@ -432,3 +432,20 @@ function normalizeChatEndpoint(url: string): string {
432
432
  if (stripped.endsWith("/completions")) return stripped;
433
433
  return `${stripped}/chat/completions`;
434
434
  }
435
+
436
+ /**
437
+ * Zhipu AI (glm-4.7, glm-5, etc.) uses reasoning tokens that consume max_tokens budget,
438
+ * leaving no room for actual output. This helper injects {"thinking":{"type":"disabled"}}
439
+ * for zhipu endpoints to disable the built-in reasoning mode.
440
+ */
441
+ function isZhipuEndpoint(endpoint: string): boolean {
442
+ return /bigmodel\.cn|zhipuai/.test(endpoint);
443
+ }
444
+
445
+ function buildRequestBody(cfg: SummarizerConfig, body: Record<string, unknown>): Record<string, unknown> {
446
+ const endpoint = cfg.endpoint ?? "";
447
+ if (isZhipuEndpoint(endpoint)) {
448
+ body.thinking = { type: "disabled" };
449
+ }
450
+ return body;
451
+ }
@@ -74,49 +74,58 @@ export class RecallEngine {
74
74
  score: 1 / (i + 1),
75
75
  }));
76
76
 
77
- // Step 1c: Hub memories search only in Hub mode where local DB owns the
78
- // hub_memories data and embeddings were generated by the same Embedder.
79
- // Client mode must use remote API (hubSearchMemories) to avoid cross-model
80
- // embedding mismatch.
77
+ // Step 1c: Hub memories — two-stage retrieval (no cached embeddings).
78
+ // Stage 1: FTS + pattern to get candidates.
79
+ // Stage 2: embed candidates on-the-fly + cosine rerank.
81
80
  let hubMemFtsRanked: Array<{ id: string; score: number }> = [];
82
81
  let hubMemVecRanked: Array<{ id: string; score: number }> = [];
83
82
  let hubMemPatternRanked: Array<{ id: string; score: number }> = [];
84
83
  if (query && this.ctx.config.sharing?.enabled && this.ctx.config.sharing.role === "hub") {
84
+ // Stage 1: cheap text retrieval
85
+ const hubCandidateTexts = new Map<string, string>();
85
86
  try {
86
87
  const hubFtsHits = this.store.searchHubMemories(query, { maxResults: candidatePool });
87
- hubMemFtsRanked = hubFtsHits.map(({ hit }, i) => ({
88
- id: `hubmem:${hit.id}`, score: 1 / (i + 1),
89
- }));
88
+ hubMemFtsRanked = hubFtsHits.map(({ hit }, i) => {
89
+ hubCandidateTexts.set(hit.id, (hit.summary || hit.content || "").slice(0, 500));
90
+ return { id: `hubmem:${hit.id}`, score: 1 / (i + 1) };
91
+ });
90
92
  } catch { /* hub_memories table may not exist */ }
91
93
  if (shortTerms.length > 0) {
92
94
  try {
93
95
  const hubPatternHits = this.store.hubMemoryPatternSearch(shortTerms, { limit: candidatePool });
94
- hubMemPatternRanked = hubPatternHits.map((h, i) => ({
95
- id: `hubmem:${h.memoryId}`, score: 1 / (i + 1),
96
- }));
96
+ hubMemPatternRanked = hubPatternHits.map((h, i) => {
97
+ hubCandidateTexts.set(h.memoryId, (h.content || "").slice(0, 500));
98
+ return { id: `hubmem:${h.memoryId}`, score: 1 / (i + 1) };
99
+ });
97
100
  } catch { /* best-effort */ }
98
101
  }
99
- try {
100
- const hubMemEmbs = this.store.getVisibleHubMemoryEmbeddings("");
101
- if (hubMemEmbs.length > 0) {
102
+
103
+ // Stage 2: embed candidates on-the-fly and cosine rerank
104
+ if (hubCandidateTexts.size > 0) {
105
+ try {
102
106
  const qv = await this.embedder.embedQuery(query).catch(() => null);
103
107
  if (qv) {
108
+ const ids = [...hubCandidateTexts.keys()];
109
+ const texts = ids.map(id => hubCandidateTexts.get(id)!);
110
+ const vecs = await this.embedder.embed(texts);
104
111
  const scored: Array<{ id: string; score: number }> = [];
105
- for (const e of hubMemEmbs) {
112
+ for (let j = 0; j < ids.length; j++) {
113
+ if (!vecs[j]) continue;
114
+ const v = vecs[j];
106
115
  let dot = 0, nA = 0, nB = 0;
107
- for (let i = 0; i < qv.length && i < e.vector.length; i++) {
108
- dot += qv[i] * e.vector[i]; nA += qv[i] * qv[i]; nB += e.vector[i] * e.vector[i];
116
+ for (let i = 0; i < qv.length && i < v.length; i++) {
117
+ dot += qv[i] * v[i]; nA += qv[i] * qv[i]; nB += v[i] * v[i];
109
118
  }
110
119
  const sim = nA > 0 && nB > 0 ? dot / (Math.sqrt(nA) * Math.sqrt(nB)) : 0;
111
120
  if (sim > 0.3) {
112
- scored.push({ id: `hubmem:${e.memoryId}`, score: sim });
121
+ scored.push({ id: `hubmem:${ids[j]}`, score: sim });
113
122
  }
114
123
  }
115
124
  scored.sort((a, b) => b.score - a.score);
116
125
  hubMemVecRanked = scored.slice(0, candidatePool);
117
126
  }
118
- }
119
- } catch { /* best-effort */ }
127
+ } catch { /* best-effort */ }
128
+ }
120
129
  const hubTotal = hubMemFtsRanked.length + hubMemVecRanked.length + hubMemPatternRanked.length;
121
130
  if (hubTotal > 0) {
122
131
  this.ctx.log.debug(`recall: hub_memories candidates: fts=${hubMemFtsRanked.length}, vec=${hubMemVecRanked.length}, pattern=${hubMemPatternRanked.length}`);