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

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 (66) 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 +33 -5
  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 +2 -0
  11. package/dist/hub/server.d.ts.map +1 -1
  12. package/dist/hub/server.js +116 -54
  13. package/dist/hub/server.js.map +1 -1
  14. package/dist/ingest/providers/index.d.ts +4 -0
  15. package/dist/ingest/providers/index.d.ts.map +1 -1
  16. package/dist/ingest/providers/index.js +32 -86
  17. package/dist/ingest/providers/index.js.map +1 -1
  18. package/dist/ingest/providers/openai.d.ts.map +1 -1
  19. package/dist/ingest/providers/openai.js +29 -13
  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 +33 -32
  23. package/dist/recall/engine.js.map +1 -1
  24. package/dist/storage/sqlite.d.ts +43 -7
  25. package/dist/storage/sqlite.d.ts.map +1 -1
  26. package/dist/storage/sqlite.js +179 -58
  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/update-check.d.ts.map +1 -1
  35. package/dist/update-check.js +2 -7
  36. package/dist/update-check.js.map +1 -1
  37. package/dist/viewer/html.d.ts.map +1 -1
  38. package/dist/viewer/html.js +115 -27
  39. package/dist/viewer/html.js.map +1 -1
  40. package/dist/viewer/server.d.ts +25 -0
  41. package/dist/viewer/server.d.ts.map +1 -1
  42. package/dist/viewer/server.js +503 -206
  43. package/dist/viewer/server.js.map +1 -1
  44. package/index.ts +273 -282
  45. package/openclaw.plugin.json +1 -1
  46. package/package.json +2 -1
  47. package/scripts/native-binding.cjs +32 -0
  48. package/scripts/postinstall.cjs +24 -11
  49. package/src/capture/index.ts +36 -0
  50. package/src/client/connector.ts +32 -5
  51. package/src/client/hub.ts +4 -0
  52. package/src/hub/server.ts +110 -50
  53. package/src/ingest/providers/index.ts +37 -92
  54. package/src/ingest/providers/openai.ts +31 -13
  55. package/src/recall/engine.ts +32 -30
  56. package/src/storage/sqlite.ts +196 -63
  57. package/src/tools/memory-get.ts +4 -1
  58. package/src/types.ts +2 -0
  59. package/src/update-check.ts +2 -7
  60. package/src/viewer/html.ts +115 -27
  61. package/src/viewer/server.ts +483 -172
  62. package/prebuilds/darwin-arm64/better_sqlite3.node +0 -0
  63. package/prebuilds/darwin-x64/better_sqlite3.node +0 -0
  64. package/prebuilds/linux-x64/better_sqlite3.node +0 -0
  65. package/prebuilds/win32-x64/better_sqlite3.node +0 -0
  66. package/telemetry.credentials.json +0 -5
@@ -329,6 +329,27 @@ export class Summarizer {
329
329
  return this.strongCfg;
330
330
  }
331
331
 
332
+ // ─── OpenClaw Prompts ───
333
+
334
+ static readonly OPENCLAW_TOPIC_JUDGE_PROMPT = `You are a conversation topic change detector.
335
+ Given a CURRENT CONVERSATION SUMMARY and a NEW USER MESSAGE, decide: has the user started a COMPLETELY NEW topic that is unrelated to the current conversation?
336
+ Reply with a single word: "NEW" if topic changed, "SAME" if it continues.`;
337
+
338
+ static readonly OPENCLAW_FILTER_RELEVANT_PROMPT = `You are a memory relevance judge.
339
+ Given a QUERY and CANDIDATE memories, decide: does each candidate help answer the query?
340
+ RULES:
341
+ 1. Include candidates whose content provides useful facts/context for the query.
342
+ 2. Exclude candidates that merely share a topic but contain no useful information.
343
+ 3. DEDUPLICATION: When multiple candidates convey the same or very similar information, keep ONLY the most complete one and exclude the rest.
344
+ 4. If none help, return {"relevant":[],"sufficient":false}.
345
+ OUTPUT — JSON only: {"relevant":[1,3],"sufficient":true}`;
346
+
347
+ static readonly OPENCLAW_DEDUP_JUDGE_PROMPT = `You are a memory deduplication system.
348
+ Given a NEW memory summary and EXISTING candidates, decide if the new memory duplicates any existing one.
349
+ Reply with JSON: {"action":"MERGE","mergeTarget":2,"reason":"..."} or {"action":"NEW","reason":"..."}`;
350
+
351
+ static readonly OPENCLAW_TASK_SUMMARY_PROMPT = `Summarize the following task conversation into a structured report. Preserve key decisions, code, commands, and outcomes. Use the same language as the input.`;
352
+
332
353
  // ─── OpenClaw API Implementation ───
333
354
 
334
355
  private requireOpenClawAPI(): void {
@@ -360,7 +381,7 @@ export class Summarizer {
360
381
  private async summarizeTaskOpenClaw(text: string): Promise<string> {
361
382
  this.requireOpenClawAPI();
362
383
  const prompt = [
363
- OPENCLAW_TASK_SUMMARY_PROMPT,
384
+ Summarizer.OPENCLAW_TASK_SUMMARY_PROMPT,
364
385
  ``,
365
386
  text,
366
387
  ].join("\n");
@@ -378,7 +399,7 @@ export class Summarizer {
378
399
  private async judgeNewTopicOpenClaw(currentContext: string, newMessage: string): Promise<boolean> {
379
400
  this.requireOpenClawAPI();
380
401
  const prompt = [
381
- OPENCLAW_TOPIC_JUDGE_PROMPT,
402
+ Summarizer.OPENCLAW_TOPIC_JUDGE_PROMPT,
382
403
  ``,
383
404
  `CURRENT CONVERSATION SUMMARY:`,
384
405
  currentContext,
@@ -409,7 +430,7 @@ export class Summarizer {
409
430
  .join("\n");
410
431
 
411
432
  const prompt = [
412
- OPENCLAW_FILTER_RELEVANT_PROMPT,
433
+ Summarizer.OPENCLAW_FILTER_RELEVANT_PROMPT,
413
434
  ``,
414
435
  `QUERY: ${query}`,
415
436
  ``,
@@ -437,7 +458,7 @@ export class Summarizer {
437
458
  .join("\n");
438
459
 
439
460
  const prompt = [
440
- OPENCLAW_DEDUP_JUDGE_PROMPT,
461
+ Summarizer.OPENCLAW_DEDUP_JUDGE_PROMPT,
441
462
  ``,
442
463
  `NEW MEMORY:`,
443
464
  newSummary,
@@ -466,6 +487,8 @@ function callSummarize(cfg: SummarizerConfig, text: string, log: Logger): Promis
466
487
  case "azure_openai":
467
488
  case "zhipu":
468
489
  case "siliconflow":
490
+ case "deepseek":
491
+ case "moonshot":
469
492
  case "bailian":
470
493
  case "cohere":
471
494
  case "mistral":
@@ -489,6 +512,8 @@ function callSummarizeTask(cfg: SummarizerConfig, text: string, log: Logger): Pr
489
512
  case "azure_openai":
490
513
  case "zhipu":
491
514
  case "siliconflow":
515
+ case "deepseek":
516
+ case "moonshot":
492
517
  case "bailian":
493
518
  case "cohere":
494
519
  case "mistral":
@@ -512,6 +537,8 @@ function callGenerateTaskTitle(cfg: SummarizerConfig, text: string, log: Logger)
512
537
  case "azure_openai":
513
538
  case "zhipu":
514
539
  case "siliconflow":
540
+ case "deepseek":
541
+ case "moonshot":
515
542
  case "bailian":
516
543
  case "cohere":
517
544
  case "mistral":
@@ -535,6 +562,8 @@ function callTopicJudge(cfg: SummarizerConfig, currentContext: string, newMessag
535
562
  case "azure_openai":
536
563
  case "zhipu":
537
564
  case "siliconflow":
565
+ case "deepseek":
566
+ case "moonshot":
538
567
  case "bailian":
539
568
  case "cohere":
540
569
  case "mistral":
@@ -558,6 +587,8 @@ function callFilterRelevant(cfg: SummarizerConfig, query: string, candidates: Ar
558
587
  case "azure_openai":
559
588
  case "zhipu":
560
589
  case "siliconflow":
590
+ case "deepseek":
591
+ case "moonshot":
561
592
  case "bailian":
562
593
  case "cohere":
563
594
  case "mistral":
@@ -581,6 +612,8 @@ function callJudgeDedup(cfg: SummarizerConfig, newSummary: string, candidates: A
581
612
  case "azure_openai":
582
613
  case "zhipu":
583
614
  case "siliconflow":
615
+ case "deepseek":
616
+ case "moonshot":
584
617
  case "bailian":
585
618
  case "cohere":
586
619
  case "mistral":
@@ -629,91 +662,3 @@ function wordCount(text: string): number {
629
662
  if (noCjk) count += noCjk.split(/\s+/).filter(Boolean).length;
630
663
  return count;
631
664
  }
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
-
@@ -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
 
@@ -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
 
@@ -258,10 +258,11 @@ RULES:
258
258
  1. A candidate is relevant if its content provides facts, context, or data that directly supports answering the query.
259
259
  2. A candidate that merely shares the same broad topic/domain but contains NO useful information for answering is NOT relevant.
260
260
  3. If NO candidate can help answer the query, return {"relevant":[],"sufficient":false} — do NOT force-pick the "least irrelevant" one.
261
+ 4. DEDUPLICATION: When multiple candidates convey the same or very similar information, keep ONLY the most complete/detailed one and exclude the rest. Do NOT return near-duplicate snippets.
261
262
 
262
263
  OUTPUT — JSON only:
263
264
  {"relevant":[1,3],"sufficient":true}
264
- - "relevant": candidate numbers whose content helps answer the query. [] if none can help.
265
+ - "relevant": candidate numbers whose content helps answer the query. [] if none can help. Duplicates removed — only unique information.
265
266
  - "sufficient": true only if the selected memories fully answer the query.`;
266
267
 
267
268
  export interface FilterResult {
@@ -293,7 +294,7 @@ export async function filterRelevantOpenAI(
293
294
  const resp = await fetch(endpoint, {
294
295
  method: "POST",
295
296
  headers,
296
- body: JSON.stringify({
297
+ body: JSON.stringify(buildRequestBody(cfg, {
297
298
  model,
298
299
  temperature: 0,
299
300
  max_tokens: 200,
@@ -301,7 +302,7 @@ export async function filterRelevantOpenAI(
301
302
  { role: "system", content: FILTER_RELEVANT_PROMPT },
302
303
  { role: "user", content: `QUERY: ${query}\n\nCANDIDATES:\n${candidateText}` },
303
304
  ],
304
- }),
305
+ })),
305
306
  signal: AbortSignal.timeout(cfg.timeoutMs ?? 15_000),
306
307
  });
307
308
 
@@ -385,7 +386,7 @@ export async function judgeDedupOpenAI(
385
386
  const resp = await fetch(endpoint, {
386
387
  method: "POST",
387
388
  headers,
388
- body: JSON.stringify({
389
+ body: JSON.stringify(buildRequestBody(cfg, {
389
390
  model,
390
391
  temperature: 0,
391
392
  max_tokens: 300,
@@ -393,7 +394,7 @@ export async function judgeDedupOpenAI(
393
394
  { role: "system", content: DEDUP_JUDGE_PROMPT },
394
395
  { role: "user", content: `NEW MEMORY:\n${newSummary}\n\nEXISTING MEMORIES:\n${candidateText}` },
395
396
  ],
396
- }),
397
+ })),
397
398
  signal: AbortSignal.timeout(cfg.timeoutMs ?? 15_000),
398
399
  });
399
400
 
@@ -432,3 +433,20 @@ function normalizeChatEndpoint(url: string): string {
432
433
  if (stripped.endsWith("/completions")) return stripped;
433
434
  return `${stripped}/chat/completions`;
434
435
  }
436
+
437
+ /**
438
+ * Zhipu AI (glm-4.7, glm-5, etc.) uses reasoning tokens that consume max_tokens budget,
439
+ * leaving no room for actual output. This helper injects {"thinking":{"type":"disabled"}}
440
+ * for zhipu endpoints to disable the built-in reasoning mode.
441
+ */
442
+ function isZhipuEndpoint(endpoint: string): boolean {
443
+ return /bigmodel\.cn|zhipuai/.test(endpoint);
444
+ }
445
+
446
+ function buildRequestBody(cfg: SummarizerConfig, body: Record<string, unknown>): Record<string, unknown> {
447
+ const endpoint = cfg.endpoint ?? "";
448
+ if (isZhipuEndpoint(endpoint)) {
449
+ body.thinking = { type: "disabled" };
450
+ }
451
+ return body;
452
+ }
@@ -62,10 +62,20 @@ export class RecallEngine {
62
62
 
63
63
  // Step 1b: Pattern search (LIKE-based) as fallback for short terms that
64
64
  // trigram FTS cannot match (trigram requires >= 3 chars).
65
- const shortTerms = query
66
- .replace(/[."""(){}[\]*:^~!@#$%&\\/<>,;'`??。,!、:""''()【】《》]/g, " ")
67
- .split(/\s+/)
68
- .filter((t) => t.length === 2);
65
+ // For CJK text without spaces, extract bigrams (2-char sliding windows)
66
+ // so that queries like "唐波是谁" produce ["唐波", "波是", "是谁"].
67
+ const cleaned = query.replace(/[."""(){}[\]*:^~!@#$%&\\/<>,;'`??。,!、:""''()【】《》]/g, " ");
68
+ const spaceSplit = cleaned.split(/\s+/).filter((t) => t.length === 2);
69
+ const cjkBigrams: string[] = [];
70
+ const cjkRuns = cleaned.match(/[\u4e00-\u9fff\u3400-\u4dbf\uF900-\uFAFF]{2,}/g);
71
+ if (cjkRuns) {
72
+ for (const run of cjkRuns) {
73
+ for (let i = 0; i <= run.length - 2; i++) {
74
+ cjkBigrams.push(run.slice(i, i + 2));
75
+ }
76
+ }
77
+ }
78
+ const shortTerms = [...new Set([...spaceSplit, ...cjkBigrams])];
69
79
  const patternHits = shortTerms.length > 0
70
80
  ? this.store.patternSearch(shortTerms, { limit: candidatePool })
71
81
  : [];
@@ -74,49 +84,41 @@ export class RecallEngine {
74
84
  score: 1 / (i + 1),
75
85
  }));
76
86
 
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.
87
+ // Step 1c: Hub memories — FTS + pattern + cached embeddings (same strategy as chunks/skills).
81
88
  let hubMemFtsRanked: Array<{ id: string; score: number }> = [];
82
89
  let hubMemVecRanked: Array<{ id: string; score: number }> = [];
83
90
  let hubMemPatternRanked: Array<{ id: string; score: number }> = [];
84
91
  if (query && this.ctx.config.sharing?.enabled && this.ctx.config.sharing.role === "hub") {
85
92
  try {
86
93
  const hubFtsHits = this.store.searchHubMemories(query, { maxResults: candidatePool });
87
- hubMemFtsRanked = hubFtsHits.map(({ hit }, i) => ({
88
- id: `hubmem:${hit.id}`, score: 1 / (i + 1),
89
- }));
94
+ hubMemFtsRanked = hubFtsHits.map(({ hit }, i) => ({ id: `hubmem:${hit.id}`, score: 1 / (i + 1) }));
90
95
  } catch { /* hub_memories table may not exist */ }
91
96
  if (shortTerms.length > 0) {
92
97
  try {
93
98
  const hubPatternHits = this.store.hubMemoryPatternSearch(shortTerms, { limit: candidatePool });
94
- hubMemPatternRanked = hubPatternHits.map((h, i) => ({
95
- id: `hubmem:${h.memoryId}`, score: 1 / (i + 1),
96
- }));
99
+ hubMemPatternRanked = hubPatternHits.map((h, i) => ({ id: `hubmem:${h.memoryId}`, score: 1 / (i + 1) }));
97
100
  } catch { /* best-effort */ }
98
101
  }
102
+
99
103
  try {
100
- const hubMemEmbs = this.store.getVisibleHubMemoryEmbeddings("");
101
- if (hubMemEmbs.length > 0) {
102
- const qv = await this.embedder.embedQuery(query).catch(() => null);
103
- if (qv) {
104
- const scored: Array<{ id: string; score: number }> = [];
105
- for (const e of hubMemEmbs) {
106
- 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];
109
- }
110
- const sim = nA > 0 && nB > 0 ? dot / (Math.sqrt(nA) * Math.sqrt(nB)) : 0;
111
- if (sim > 0.3) {
112
- scored.push({ id: `hubmem:${e.memoryId}`, score: sim });
113
- }
104
+ const qv = await this.embedder.embedQuery(query).catch(() => null);
105
+ if (qv) {
106
+ const memEmbs = this.store.getVisibleHubMemoryEmbeddings("__hub__");
107
+ const scored: Array<{ id: string; score: number }> = [];
108
+ for (const e of memEmbs) {
109
+ let dot = 0, nA = 0, nB = 0;
110
+ const len = Math.min(qv.length, e.vector.length);
111
+ for (let i = 0; i < len; i++) {
112
+ dot += qv[i] * e.vector[i]; nA += qv[i] * qv[i]; nB += e.vector[i] * e.vector[i];
114
113
  }
115
- scored.sort((a, b) => b.score - a.score);
116
- hubMemVecRanked = scored.slice(0, candidatePool);
114
+ const sim = nA > 0 && nB > 0 ? dot / (Math.sqrt(nA) * Math.sqrt(nB)) : 0;
115
+ if (sim > 0.3) scored.push({ id: `hubmem:${e.memoryId}`, score: sim });
117
116
  }
117
+ scored.sort((a, b) => b.score - a.score);
118
+ hubMemVecRanked = scored.slice(0, candidatePool);
118
119
  }
119
120
  } catch { /* best-effort */ }
121
+
120
122
  const hubTotal = hubMemFtsRanked.length + hubMemVecRanked.length + hubMemPatternRanked.length;
121
123
  if (hubTotal > 0) {
122
124
  this.ctx.log.debug(`recall: hub_memories candidates: fts=${hubMemFtsRanked.length}, vec=${hubMemVecRanked.length}, pattern=${hubMemPatternRanked.length}`);