@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.
- package/dist/capture/index.d.ts.map +1 -1
- package/dist/capture/index.js +24 -0
- package/dist/capture/index.js.map +1 -1
- package/dist/client/connector.d.ts.map +1 -1
- package/dist/client/connector.js +33 -5
- package/dist/client/connector.js.map +1 -1
- package/dist/client/hub.d.ts.map +1 -1
- package/dist/client/hub.js +4 -0
- package/dist/client/hub.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 +116 -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 +32 -86
- package/dist/ingest/providers/index.js.map +1 -1
- package/dist/ingest/providers/openai.d.ts.map +1 -1
- package/dist/ingest/providers/openai.js +29 -13
- package/dist/ingest/providers/openai.js.map +1 -1
- package/dist/recall/engine.d.ts.map +1 -1
- package/dist/recall/engine.js +33 -32
- package/dist/recall/engine.js.map +1 -1
- package/dist/storage/sqlite.d.ts +43 -7
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +179 -58
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/tools/memory-get.d.ts.map +1 -1
- package/dist/tools/memory-get.js +4 -1
- package/dist/tools/memory-get.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/update-check.d.ts.map +1 -1
- package/dist/update-check.js +2 -7
- package/dist/update-check.js.map +1 -1
- package/dist/viewer/html.d.ts.map +1 -1
- package/dist/viewer/html.js +115 -27
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +25 -0
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +503 -206
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +273 -282
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/scripts/native-binding.cjs +32 -0
- package/scripts/postinstall.cjs +24 -11
- package/src/capture/index.ts +36 -0
- package/src/client/connector.ts +32 -5
- package/src/client/hub.ts +4 -0
- package/src/hub/server.ts +110 -50
- package/src/ingest/providers/index.ts +37 -92
- package/src/ingest/providers/openai.ts +31 -13
- package/src/recall/engine.ts +32 -30
- package/src/storage/sqlite.ts +196 -63
- package/src/tools/memory-get.ts +4 -1
- package/src/types.ts +2 -0
- package/src/update-check.ts +2 -7
- package/src/viewer/html.ts +115 -27
- package/src/viewer/server.ts +483 -172
- package/prebuilds/darwin-arm64/better_sqlite3.node +0 -0
- package/prebuilds/darwin-x64/better_sqlite3.node +0 -0
- package/prebuilds/linux-x64/better_sqlite3.node +0 -0
- package/prebuilds/win32-x64/better_sqlite3.node +0 -0
- 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
|
+
}
|
package/src/recall/engine.ts
CHANGED
|
@@ -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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
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
|
|
101
|
-
if (
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
116
|
-
|
|
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}`);
|