@memtensor/memos-local-openclaw-plugin 1.0.2-beta.5 → 1.0.2-beta.7
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.js +52 -8
- package/dist/capture/index.js.map +1 -1
- package/dist/embedding/index.d.ts.map +1 -1
- package/dist/embedding/index.js +4 -3
- package/dist/embedding/index.js.map +1 -1
- package/dist/ingest/chunker.d.ts +3 -4
- package/dist/ingest/chunker.d.ts.map +1 -1
- package/dist/ingest/chunker.js +19 -24
- package/dist/ingest/chunker.js.map +1 -1
- package/dist/ingest/providers/anthropic.d.ts +3 -1
- package/dist/ingest/providers/anthropic.d.ts.map +1 -1
- package/dist/ingest/providers/anthropic.js +79 -39
- package/dist/ingest/providers/anthropic.js.map +1 -1
- package/dist/ingest/providers/bedrock.d.ts +3 -1
- package/dist/ingest/providers/bedrock.d.ts.map +1 -1
- package/dist/ingest/providers/bedrock.js +79 -39
- package/dist/ingest/providers/bedrock.js.map +1 -1
- package/dist/ingest/providers/gemini.d.ts +3 -1
- package/dist/ingest/providers/gemini.d.ts.map +1 -1
- package/dist/ingest/providers/gemini.js +77 -39
- package/dist/ingest/providers/gemini.js.map +1 -1
- package/dist/ingest/providers/index.d.ts +3 -1
- package/dist/ingest/providers/index.d.ts.map +1 -1
- package/dist/ingest/providers/index.js +107 -30
- package/dist/ingest/providers/index.js.map +1 -1
- package/dist/ingest/providers/openai.d.ts +3 -1
- package/dist/ingest/providers/openai.d.ts.map +1 -1
- package/dist/ingest/providers/openai.js +80 -39
- package/dist/ingest/providers/openai.js.map +1 -1
- package/dist/ingest/task-processor.d.ts +1 -0
- package/dist/ingest/task-processor.d.ts.map +1 -1
- package/dist/ingest/task-processor.js +33 -9
- package/dist/ingest/task-processor.js.map +1 -1
- package/dist/ingest/worker.d.ts.map +1 -1
- package/dist/ingest/worker.js +29 -13
- package/dist/ingest/worker.js.map +1 -1
- package/dist/recall/engine.d.ts.map +1 -1
- package/dist/recall/engine.js +19 -14
- package/dist/recall/engine.js.map +1 -1
- package/dist/skill/bundled-memory-guide.d.ts +1 -5
- package/dist/skill/bundled-memory-guide.d.ts.map +1 -1
- package/dist/skill/bundled-memory-guide.js +38 -97
- package/dist/skill/bundled-memory-guide.js.map +1 -1
- package/dist/skill/evaluator.js +1 -1
- package/dist/storage/sqlite.d.ts +1 -2
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +90 -17
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/tools/memory-get.d.ts.map +1 -1
- package/dist/tools/memory-get.js +1 -3
- package/dist/tools/memory-get.js.map +1 -1
- package/dist/types.d.ts +3 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/dist/update-check.d.ts +21 -0
- package/dist/update-check.d.ts.map +1 -0
- package/dist/update-check.js +110 -0
- package/dist/update-check.js.map +1 -0
- package/dist/viewer/html.d.ts.map +1 -1
- package/dist/viewer/html.js +487 -189
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +1 -1
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +240 -78
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +205 -197
- package/openclaw.plugin.json +3 -0
- package/package.json +8 -3
- package/scripts/postinstall.cjs +69 -2
- package/skill/memos-memory-guide/SKILL.md +73 -36
- package/src/capture/index.ts +52 -8
- package/src/embedding/index.ts +4 -2
- package/src/ingest/chunker.ts +22 -30
- package/src/ingest/providers/anthropic.ts +89 -41
- package/src/ingest/providers/bedrock.ts +90 -41
- package/src/ingest/providers/gemini.ts +89 -41
- package/src/ingest/providers/index.ts +118 -35
- package/src/ingest/providers/openai.ts +90 -41
- package/src/ingest/task-processor.ts +29 -8
- package/src/ingest/worker.ts +31 -13
- package/src/recall/engine.ts +20 -13
- package/src/skill/bundled-memory-guide.ts +5 -96
- package/src/skill/evaluator.ts +1 -1
- package/src/storage/sqlite.ts +93 -21
- package/src/tools/memory-get.ts +1 -4
- package/src/types.ts +9 -10
- package/src/update-check.ts +95 -0
- package/src/viewer/html.ts +487 -189
- package/src/viewer/server.ts +187 -66
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import type { SummarizerConfig, Logger } from "../../types";
|
|
4
|
-
import { summarizeOpenAI, summarizeTaskOpenAI, judgeNewTopicOpenAI, filterRelevantOpenAI, judgeDedupOpenAI } from "./openai";
|
|
4
|
+
import { summarizeOpenAI, summarizeTaskOpenAI, generateTaskTitleOpenAI, judgeNewTopicOpenAI, filterRelevantOpenAI, judgeDedupOpenAI } from "./openai";
|
|
5
5
|
import type { FilterResult, DedupResult } from "./openai";
|
|
6
6
|
export type { FilterResult, DedupResult } from "./openai";
|
|
7
|
-
import { summarizeAnthropic, summarizeTaskAnthropic, judgeNewTopicAnthropic, filterRelevantAnthropic, judgeDedupAnthropic } from "./anthropic";
|
|
8
|
-
import { summarizeGemini, summarizeTaskGemini, judgeNewTopicGemini, filterRelevantGemini, judgeDedupGemini } from "./gemini";
|
|
9
|
-
import { summarizeBedrock, summarizeTaskBedrock, judgeNewTopicBedrock, filterRelevantBedrock, judgeDedupBedrock } from "./bedrock";
|
|
7
|
+
import { summarizeAnthropic, summarizeTaskAnthropic, generateTaskTitleAnthropic, judgeNewTopicAnthropic, filterRelevantAnthropic, judgeDedupAnthropic } from "./anthropic";
|
|
8
|
+
import { summarizeGemini, summarizeTaskGemini, generateTaskTitleGemini, judgeNewTopicGemini, filterRelevantGemini, judgeDedupGemini } from "./gemini";
|
|
9
|
+
import { summarizeBedrock, summarizeTaskBedrock, generateTaskTitleBedrock, judgeNewTopicBedrock, filterRelevantBedrock, judgeDedupBedrock } from "./bedrock";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Build a SummarizerConfig from OpenClaw's native model configuration (openclaw.json).
|
|
@@ -163,34 +163,53 @@ export class Summarizer {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
async summarize(text: string): Promise<string> {
|
|
166
|
+
const cleaned = stripMarkdown(text).trim();
|
|
167
|
+
|
|
168
|
+
if (wordCount(cleaned) <= 10) {
|
|
169
|
+
return cleaned;
|
|
170
|
+
}
|
|
171
|
+
|
|
166
172
|
if (!this.cfg && !this.fallbackCfg) {
|
|
167
|
-
return ruleFallback(
|
|
173
|
+
return ruleFallback(cleaned);
|
|
168
174
|
}
|
|
169
175
|
|
|
170
|
-
const
|
|
176
|
+
const accept = (s: string | undefined): s is string =>
|
|
177
|
+
!!s && s.length > 0 && s.length < cleaned.length;
|
|
171
178
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
179
|
+
let llmCalled = false;
|
|
180
|
+
try {
|
|
181
|
+
const result = await this.tryChain("summarize", (cfg) => callSummarize(cfg, text, this.log));
|
|
182
|
+
llmCalled = true;
|
|
183
|
+
const resultCleaned = result ? stripMarkdown(result).trim() : undefined;
|
|
175
184
|
|
|
176
|
-
|
|
177
|
-
|
|
185
|
+
if (accept(resultCleaned)) {
|
|
186
|
+
return resultCleaned;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (resultCleaned !== undefined && resultCleaned !== null) {
|
|
190
|
+
const len: number = (resultCleaned as string).length;
|
|
191
|
+
this.log.warn(`summarize: result (${len}) >= input (${cleaned.length}), retrying`);
|
|
192
|
+
}
|
|
193
|
+
} catch (err) {
|
|
194
|
+
this.log.warn(`summarize primary failed: ${err}`);
|
|
178
195
|
}
|
|
179
196
|
|
|
180
197
|
const fallback = this.fallbackCfg ?? this.cfg;
|
|
181
198
|
if (fallback) {
|
|
182
199
|
try {
|
|
183
200
|
const retry = await callSummarize(fallback, text, this.log);
|
|
184
|
-
|
|
201
|
+
llmCalled = true;
|
|
202
|
+
const retryCleaned = retry ? stripMarkdown(retry).trim() : undefined;
|
|
203
|
+
if (accept(retryCleaned)) {
|
|
185
204
|
modelHealth.recordSuccess("summarize", `${fallback.provider}/${fallback.model ?? "?"}`);
|
|
186
|
-
return
|
|
205
|
+
return retryCleaned;
|
|
187
206
|
}
|
|
188
207
|
} catch (err) {
|
|
189
208
|
this.log.warn(`summarize fallback retry failed: ${err}`);
|
|
190
209
|
}
|
|
191
210
|
}
|
|
192
211
|
|
|
193
|
-
return ruleFallback(
|
|
212
|
+
return llmCalled ? cleaned : ruleFallback(cleaned);
|
|
194
213
|
}
|
|
195
214
|
|
|
196
215
|
async summarizeTask(text: string): Promise<string> {
|
|
@@ -202,6 +221,12 @@ export class Summarizer {
|
|
|
202
221
|
return result ?? taskFallback(text);
|
|
203
222
|
}
|
|
204
223
|
|
|
224
|
+
async generateTaskTitle(text: string): Promise<string> {
|
|
225
|
+
if (!this.cfg && !this.fallbackCfg) return "";
|
|
226
|
+
const result = await this.tryChain("generateTaskTitle", (cfg) => callGenerateTaskTitle(cfg, text, this.log));
|
|
227
|
+
return result ?? "";
|
|
228
|
+
}
|
|
229
|
+
|
|
205
230
|
async judgeNewTopic(currentContext: string, newMessage: string): Promise<boolean | null> {
|
|
206
231
|
const chain: SummarizerConfig[] = [];
|
|
207
232
|
if (this.strongCfg) chain.push(this.strongCfg);
|
|
@@ -226,7 +251,7 @@ export class Summarizer {
|
|
|
226
251
|
|
|
227
252
|
async filterRelevant(
|
|
228
253
|
query: string,
|
|
229
|
-
candidates: Array<{ index: number;
|
|
254
|
+
candidates: Array<{ index: number; role: string; content: string; time?: string }>,
|
|
230
255
|
): Promise<FilterResult | null> {
|
|
231
256
|
if (!this.cfg && !this.fallbackCfg) return null;
|
|
232
257
|
if (candidates.length === 0) return { relevant: [], sufficient: true };
|
|
@@ -258,6 +283,12 @@ function callSummarize(cfg: SummarizerConfig, text: string, log: Logger): Promis
|
|
|
258
283
|
case "openai":
|
|
259
284
|
case "openai_compatible":
|
|
260
285
|
case "azure_openai":
|
|
286
|
+
case "zhipu":
|
|
287
|
+
case "siliconflow":
|
|
288
|
+
case "bailian":
|
|
289
|
+
case "cohere":
|
|
290
|
+
case "mistral":
|
|
291
|
+
case "voyage":
|
|
261
292
|
return summarizeOpenAI(text, cfg, log);
|
|
262
293
|
case "anthropic":
|
|
263
294
|
return summarizeAnthropic(text, cfg, log);
|
|
@@ -275,6 +306,12 @@ function callSummarizeTask(cfg: SummarizerConfig, text: string, log: Logger): Pr
|
|
|
275
306
|
case "openai":
|
|
276
307
|
case "openai_compatible":
|
|
277
308
|
case "azure_openai":
|
|
309
|
+
case "zhipu":
|
|
310
|
+
case "siliconflow":
|
|
311
|
+
case "bailian":
|
|
312
|
+
case "cohere":
|
|
313
|
+
case "mistral":
|
|
314
|
+
case "voyage":
|
|
278
315
|
return summarizeTaskOpenAI(text, cfg, log);
|
|
279
316
|
case "anthropic":
|
|
280
317
|
return summarizeTaskAnthropic(text, cfg, log);
|
|
@@ -287,11 +324,40 @@ function callSummarizeTask(cfg: SummarizerConfig, text: string, log: Logger): Pr
|
|
|
287
324
|
}
|
|
288
325
|
}
|
|
289
326
|
|
|
327
|
+
function callGenerateTaskTitle(cfg: SummarizerConfig, text: string, log: Logger): Promise<string> {
|
|
328
|
+
switch (cfg.provider) {
|
|
329
|
+
case "openai":
|
|
330
|
+
case "openai_compatible":
|
|
331
|
+
case "azure_openai":
|
|
332
|
+
case "zhipu":
|
|
333
|
+
case "siliconflow":
|
|
334
|
+
case "bailian":
|
|
335
|
+
case "cohere":
|
|
336
|
+
case "mistral":
|
|
337
|
+
case "voyage":
|
|
338
|
+
return generateTaskTitleOpenAI(text, cfg, log);
|
|
339
|
+
case "anthropic":
|
|
340
|
+
return generateTaskTitleAnthropic(text, cfg, log);
|
|
341
|
+
case "gemini":
|
|
342
|
+
return generateTaskTitleGemini(text, cfg, log);
|
|
343
|
+
case "bedrock":
|
|
344
|
+
return generateTaskTitleBedrock(text, cfg, log);
|
|
345
|
+
default:
|
|
346
|
+
throw new Error(`Unknown summarizer provider: ${cfg.provider}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
290
350
|
function callTopicJudge(cfg: SummarizerConfig, currentContext: string, newMessage: string, log: Logger): Promise<boolean> {
|
|
291
351
|
switch (cfg.provider) {
|
|
292
352
|
case "openai":
|
|
293
353
|
case "openai_compatible":
|
|
294
354
|
case "azure_openai":
|
|
355
|
+
case "zhipu":
|
|
356
|
+
case "siliconflow":
|
|
357
|
+
case "bailian":
|
|
358
|
+
case "cohere":
|
|
359
|
+
case "mistral":
|
|
360
|
+
case "voyage":
|
|
295
361
|
return judgeNewTopicOpenAI(currentContext, newMessage, cfg, log);
|
|
296
362
|
case "anthropic":
|
|
297
363
|
return judgeNewTopicAnthropic(currentContext, newMessage, cfg, log);
|
|
@@ -304,11 +370,17 @@ function callTopicJudge(cfg: SummarizerConfig, currentContext: string, newMessag
|
|
|
304
370
|
}
|
|
305
371
|
}
|
|
306
372
|
|
|
307
|
-
function callFilterRelevant(cfg: SummarizerConfig, query: string, candidates: Array<{ index: number;
|
|
373
|
+
function callFilterRelevant(cfg: SummarizerConfig, query: string, candidates: Array<{ index: number; role: string; content: string; time?: string }>, log: Logger): Promise<FilterResult> {
|
|
308
374
|
switch (cfg.provider) {
|
|
309
375
|
case "openai":
|
|
310
376
|
case "openai_compatible":
|
|
311
377
|
case "azure_openai":
|
|
378
|
+
case "zhipu":
|
|
379
|
+
case "siliconflow":
|
|
380
|
+
case "bailian":
|
|
381
|
+
case "cohere":
|
|
382
|
+
case "mistral":
|
|
383
|
+
case "voyage":
|
|
312
384
|
return filterRelevantOpenAI(query, candidates, cfg, log);
|
|
313
385
|
case "anthropic":
|
|
314
386
|
return filterRelevantAnthropic(query, candidates, cfg, log);
|
|
@@ -326,6 +398,12 @@ function callJudgeDedup(cfg: SummarizerConfig, newSummary: string, candidates: A
|
|
|
326
398
|
case "openai":
|
|
327
399
|
case "openai_compatible":
|
|
328
400
|
case "azure_openai":
|
|
401
|
+
case "zhipu":
|
|
402
|
+
case "siliconflow":
|
|
403
|
+
case "bailian":
|
|
404
|
+
case "cohere":
|
|
405
|
+
case "mistral":
|
|
406
|
+
case "voyage":
|
|
329
407
|
return judgeDedupOpenAI(newSummary, candidates, cfg, log);
|
|
330
408
|
case "anthropic":
|
|
331
409
|
return judgeDedupAnthropic(newSummary, candidates, cfg, log);
|
|
@@ -340,29 +418,34 @@ function callJudgeDedup(cfg: SummarizerConfig, newSummary: string, candidates: A
|
|
|
340
418
|
|
|
341
419
|
// ─── Fallbacks ───
|
|
342
420
|
|
|
421
|
+
function ruleFallback(text: string): string {
|
|
422
|
+
const lines = text.split("\n").filter((l) => l.trim().length > 5);
|
|
423
|
+
return (lines[0] ?? text).trim();
|
|
424
|
+
}
|
|
425
|
+
|
|
343
426
|
function taskFallback(text: string): string {
|
|
344
427
|
const lines = text.split("\n").filter((l) => l.trim().length > 10);
|
|
345
428
|
return lines.slice(0, 30).join("\n").slice(0, 2000);
|
|
346
429
|
}
|
|
347
430
|
|
|
348
|
-
function
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
}
|
|
358
|
-
}
|
|
431
|
+
function stripMarkdown(text: string): string {
|
|
432
|
+
return text
|
|
433
|
+
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
434
|
+
.replace(/\*([^*]+)\*/g, "$1")
|
|
435
|
+
.replace(/^#{1,6}\s+/gm, "")
|
|
436
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
437
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
438
|
+
.trim();
|
|
439
|
+
}
|
|
359
440
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
let
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
441
|
+
/** Count "words": CJK characters count as 1 word each, latin words separated by spaces. */
|
|
442
|
+
function wordCount(text: string): number {
|
|
443
|
+
let count = 0;
|
|
444
|
+
const cjk = /[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff]/g;
|
|
445
|
+
const cjkMatches = text.match(cjk);
|
|
446
|
+
if (cjkMatches) count += cjkMatches.length;
|
|
447
|
+
const noCjk = text.replace(cjk, " ").trim();
|
|
448
|
+
if (noCjk) count += noCjk.split(/\s+/).filter(Boolean).length;
|
|
449
|
+
return count;
|
|
368
450
|
}
|
|
451
|
+
|
|
@@ -1,29 +1,35 @@
|
|
|
1
1
|
import type { SummarizerConfig, Logger } from "../../types";
|
|
2
2
|
|
|
3
|
-
const SYSTEM_PROMPT = `You
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
3
|
+
const SYSTEM_PROMPT = `You generate a retrieval-friendly title.
|
|
4
|
+
|
|
5
|
+
Return exactly one noun phrase that names the topic AND its key details.
|
|
6
|
+
|
|
7
|
+
Requirements:
|
|
8
|
+
- Same language as input
|
|
9
|
+
- Keep proper nouns, API/function names, specific parameters, versions, error codes
|
|
10
|
+
- Include WHO/WHAT/WHERE details when present (e.g. person name + event, tool name + what it does)
|
|
11
|
+
- Prefer concrete topic words over generic words
|
|
12
|
+
- No verbs unless unavoidable
|
|
13
|
+
- No generic endings like:
|
|
14
|
+
功能说明、使用说明、简介、介绍、用途、summary、overview、basics
|
|
15
|
+
- Chinese: 10-50 characters (aim for 15-30)
|
|
16
|
+
- Non-Chinese: 5-15 words (aim for 8-12)
|
|
17
|
+
- Output title only`;
|
|
13
18
|
|
|
14
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.
|
|
15
20
|
|
|
16
|
-
|
|
21
|
+
## LANGUAGE RULE (HIGHEST PRIORITY)
|
|
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.
|
|
17
23
|
|
|
18
24
|
Output EXACTLY this structure:
|
|
19
25
|
|
|
20
|
-
📌 Title
|
|
21
|
-
A short, descriptive title (10-30 characters).
|
|
26
|
+
📌 Title / 标题
|
|
27
|
+
A short, descriptive title (10-30 characters). Same language as user messages.
|
|
22
28
|
|
|
23
|
-
🎯 Goal
|
|
29
|
+
🎯 Goal / 目标
|
|
24
30
|
One sentence: what the user wanted to accomplish.
|
|
25
31
|
|
|
26
|
-
📋 Key Steps
|
|
32
|
+
📋 Key Steps / 关键步骤
|
|
27
33
|
- Describe each meaningful step in detail
|
|
28
34
|
- Include the ACTUAL content produced: code snippets, commands, config blocks, formulas, key paragraphs
|
|
29
35
|
- For code: include the function signature and core logic (up to ~30 lines per block), use fenced code blocks
|
|
@@ -32,10 +38,10 @@ One sentence: what the user wanted to accomplish.
|
|
|
32
38
|
- Merge only truly trivial back-and-forth (like "ok" / "sure")
|
|
33
39
|
- Do NOT over-summarize: "provided a function" is BAD; show the actual function
|
|
34
40
|
|
|
35
|
-
✅ Result
|
|
41
|
+
✅ Result / 结果
|
|
36
42
|
What was the final outcome? Include the final version of any code/config/content produced.
|
|
37
43
|
|
|
38
|
-
💡 Key Details
|
|
44
|
+
💡 Key Details / 关键细节
|
|
39
45
|
- Decisions made, trade-offs discussed, caveats noted, alternative approaches mentioned
|
|
40
46
|
- Specific values: numbers, versions, thresholds, URLs, file paths, model names
|
|
41
47
|
- Omit this section only if there truly are no noteworthy details
|
|
@@ -85,6 +91,55 @@ export async function summarizeTaskOpenAI(
|
|
|
85
91
|
return json.choices[0]?.message?.content?.trim() ?? "";
|
|
86
92
|
}
|
|
87
93
|
|
|
94
|
+
const TASK_TITLE_PROMPT = `Generate a short title for a conversation task.
|
|
95
|
+
|
|
96
|
+
Input: the first few user messages from a conversation.
|
|
97
|
+
Output: a concise title (5-20 characters for Chinese, 3-8 words for English).
|
|
98
|
+
|
|
99
|
+
Rules:
|
|
100
|
+
- Same language as user messages
|
|
101
|
+
- Describe WHAT the user wanted to do, not system/technical details
|
|
102
|
+
- Ignore system prompts, session startup messages, or boilerplate instructions — focus on the user's actual intent
|
|
103
|
+
- If the user only asked one question, use that question as the title (shortened if needed)
|
|
104
|
+
- Output the title only, no quotes, no prefix, no explanation`;
|
|
105
|
+
|
|
106
|
+
export async function generateTaskTitleOpenAI(
|
|
107
|
+
text: string,
|
|
108
|
+
cfg: SummarizerConfig,
|
|
109
|
+
log: Logger,
|
|
110
|
+
): Promise<string> {
|
|
111
|
+
const endpoint = normalizeChatEndpoint(cfg.endpoint ?? "https://api.openai.com/v1/chat/completions");
|
|
112
|
+
const model = cfg.model ?? "gpt-4o-mini";
|
|
113
|
+
const headers: Record<string, string> = {
|
|
114
|
+
"Content-Type": "application/json",
|
|
115
|
+
Authorization: `Bearer ${cfg.apiKey}`,
|
|
116
|
+
...cfg.headers,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const resp = await fetch(endpoint, {
|
|
120
|
+
method: "POST",
|
|
121
|
+
headers,
|
|
122
|
+
body: JSON.stringify({
|
|
123
|
+
model,
|
|
124
|
+
temperature: 0,
|
|
125
|
+
max_tokens: 100,
|
|
126
|
+
messages: [
|
|
127
|
+
{ role: "system", content: TASK_TITLE_PROMPT },
|
|
128
|
+
{ role: "user", content: text },
|
|
129
|
+
],
|
|
130
|
+
}),
|
|
131
|
+
signal: AbortSignal.timeout(cfg.timeoutMs ?? 15_000),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (!resp.ok) {
|
|
135
|
+
const body = await resp.text();
|
|
136
|
+
throw new Error(`OpenAI task-title failed (${resp.status}): ${body}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const json = (await resp.json()) as { choices: Array<{ message: { content: string } }> };
|
|
140
|
+
return json.choices[0]?.message?.content?.trim() ?? "";
|
|
141
|
+
}
|
|
142
|
+
|
|
88
143
|
export async function summarizeOpenAI(
|
|
89
144
|
text: string,
|
|
90
145
|
cfg: SummarizerConfig,
|
|
@@ -191,32 +246,23 @@ export async function judgeNewTopicOpenAI(
|
|
|
191
246
|
return answer.startsWith("NEW");
|
|
192
247
|
}
|
|
193
248
|
|
|
194
|
-
const FILTER_RELEVANT_PROMPT = `You are a
|
|
195
|
-
|
|
196
|
-
1. Select ONLY candidates that are DIRECTLY relevant to the query's topic.
|
|
197
|
-
- A candidate is relevant ONLY if it shares the same subject/topic as the query.
|
|
198
|
-
- EXCLUDE candidates about unrelated topics, even if they are from the same user.
|
|
199
|
-
- For list/history questions (e.g. "which companies did I work at"), include all MATCHING items.
|
|
200
|
-
- For factual lookups, a single direct answer is enough.
|
|
201
|
-
- When in doubt, EXCLUDE the candidate. Precision is more important than recall.
|
|
202
|
-
2. Judge whether the selected memories are SUFFICIENT to fully answer the query.
|
|
249
|
+
const FILTER_RELEVANT_PROMPT = `You are a memory relevance judge.
|
|
203
250
|
|
|
204
|
-
|
|
205
|
-
- Query: "recipe for braised beef" → ONLY include candidates about cooking/recipes/beef. EXCLUDE candidates about weather, deployment, identity, etc.
|
|
206
|
-
- Query: "我是谁" → ONLY include candidates about user identity/name/profile. EXCLUDE candidates about cooking, news, technical issues, etc.
|
|
207
|
-
- Query: "SSH port" → ONLY include candidates mentioning SSH or port configuration.
|
|
251
|
+
Given a QUERY and CANDIDATE memories, decide: does each candidate's content contain information that would HELP ANSWER the query?
|
|
208
252
|
|
|
209
|
-
|
|
210
|
-
-
|
|
211
|
-
-
|
|
253
|
+
CORE QUESTION: "If I include this memory, will it help produce a better answer?"
|
|
254
|
+
- YES → include
|
|
255
|
+
- NO → exclude
|
|
212
256
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
- "sufficient": true ONLY if the memories contain a direct answer; false otherwise.
|
|
257
|
+
RULES:
|
|
258
|
+
1. A candidate is relevant if its content provides facts, context, or data that directly supports answering the query.
|
|
259
|
+
2. A candidate that merely shares the same broad topic/domain but contains NO useful information for answering is NOT relevant.
|
|
260
|
+
3. If NO candidate can help answer the query, return {"relevant":[],"sufficient":false} — do NOT force-pick the "least irrelevant" one.
|
|
218
261
|
|
|
219
|
-
|
|
262
|
+
OUTPUT — JSON only:
|
|
263
|
+
{"relevant":[1,3],"sufficient":true}
|
|
264
|
+
- "relevant": candidate numbers whose content helps answer the query. [] if none can help.
|
|
265
|
+
- "sufficient": true only if the selected memories fully answer the query.`;
|
|
220
266
|
|
|
221
267
|
export interface FilterResult {
|
|
222
268
|
relevant: number[];
|
|
@@ -225,7 +271,7 @@ export interface FilterResult {
|
|
|
225
271
|
|
|
226
272
|
export async function filterRelevantOpenAI(
|
|
227
273
|
query: string,
|
|
228
|
-
candidates: Array<{ index: number;
|
|
274
|
+
candidates: Array<{ index: number; role: string; content: string; time?: string }>,
|
|
229
275
|
cfg: SummarizerConfig,
|
|
230
276
|
log: Logger,
|
|
231
277
|
): Promise<FilterResult> {
|
|
@@ -238,7 +284,10 @@ export async function filterRelevantOpenAI(
|
|
|
238
284
|
};
|
|
239
285
|
|
|
240
286
|
const candidateText = candidates
|
|
241
|
-
.map((c) =>
|
|
287
|
+
.map((c) => {
|
|
288
|
+
const timeTag = c.time ? ` (${c.time})` : "";
|
|
289
|
+
return `${c.index}. [${c.role}]${timeTag}\n ${c.content}`;
|
|
290
|
+
})
|
|
242
291
|
.join("\n");
|
|
243
292
|
|
|
244
293
|
const resp = await fetch(endpoint, {
|
|
@@ -310,9 +310,10 @@ export class TaskProcessor {
|
|
|
310
310
|
const skipReason = this.shouldSkipSummary(chunks);
|
|
311
311
|
|
|
312
312
|
if (skipReason) {
|
|
313
|
-
|
|
313
|
+
const skipTitle = await this.generateTitle(chunks, fallbackTitle);
|
|
314
|
+
this.ctx.log.info(`Task ${task.id} skipped: ${skipReason} (chunks=${chunks.length}, title="${skipTitle}")`);
|
|
314
315
|
const reason = this.humanReadableSkipReason(skipReason, chunks);
|
|
315
|
-
this.store.updateTask(task.id, { title:
|
|
316
|
+
this.store.updateTask(task.id, { title: skipTitle, summary: reason, status: "skipped", endedAt: Date.now() });
|
|
316
317
|
return;
|
|
317
318
|
}
|
|
318
319
|
|
|
@@ -326,7 +327,7 @@ export class TaskProcessor {
|
|
|
326
327
|
}
|
|
327
328
|
|
|
328
329
|
const { title: llmTitle, body } = this.parseTitleFromSummary(summary);
|
|
329
|
-
const title = llmTitle || fallbackTitle;
|
|
330
|
+
const title = llmTitle || await this.generateTitle(chunks, fallbackTitle);
|
|
330
331
|
|
|
331
332
|
this.store.updateTask(task.id, {
|
|
332
333
|
title,
|
|
@@ -455,19 +456,39 @@ export class TaskProcessor {
|
|
|
455
456
|
private parseTitleFromSummary(summary: string): { title: string; body: string } {
|
|
456
457
|
const titleMatch = summary.match(/📌\s*(?:Title|标题)\s*\n(.+)/);
|
|
457
458
|
if (titleMatch) {
|
|
458
|
-
const title = titleMatch[1].trim()
|
|
459
|
+
const title = titleMatch[1].trim();
|
|
459
460
|
const body = summary.replace(/📌\s*(?:Title|标题)\s*\n.+\n?/, "").trim();
|
|
460
461
|
return { title, body };
|
|
461
462
|
}
|
|
462
463
|
return { title: "", body: summary };
|
|
463
464
|
}
|
|
464
465
|
|
|
466
|
+
private async generateTitle(chunks: Chunk[], fallback: string): Promise<string> {
|
|
467
|
+
try {
|
|
468
|
+
const userChunks = chunks.filter((c) => c.role === "user");
|
|
469
|
+
const titleInput = userChunks
|
|
470
|
+
.slice(0, 3)
|
|
471
|
+
.map((c) => c.content.trim())
|
|
472
|
+
.join("\n\n");
|
|
473
|
+
if (!titleInput) return fallback || "Untitled Task";
|
|
474
|
+
const title = await this.summarizer.generateTaskTitle(titleInput);
|
|
475
|
+
return title || fallback || "Untitled Task";
|
|
476
|
+
} catch (err) {
|
|
477
|
+
this.ctx.log.warn(`generateTitle failed: ${err}`);
|
|
478
|
+
return fallback || "Untitled Task";
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
465
482
|
private extractTitle(chunks: Chunk[]): string {
|
|
466
|
-
const firstUser = chunks.find((c) =>
|
|
483
|
+
const firstUser = chunks.find((c) => {
|
|
484
|
+
if (c.role !== "user") return false;
|
|
485
|
+
const t = c.content.trim();
|
|
486
|
+
if (t.length > 200) return false;
|
|
487
|
+
if (/session.startup|Session Startup|\/new|\/reset/i.test(t)) return false;
|
|
488
|
+
return true;
|
|
489
|
+
});
|
|
467
490
|
if (!firstUser) return "Untitled Task";
|
|
468
|
-
|
|
469
|
-
if (text.length <= 60) return text;
|
|
470
|
-
return text.slice(0, 57) + "...";
|
|
491
|
+
return firstUser.content.trim().slice(0, 80);
|
|
471
492
|
}
|
|
472
493
|
|
|
473
494
|
private humanReadableSkipReason(reason: string, chunks: Chunk[]): string {
|
package/src/ingest/worker.ts
CHANGED
|
@@ -59,32 +59,32 @@ export class IngestWorker {
|
|
|
59
59
|
let duplicated = 0;
|
|
60
60
|
let errors = 0;
|
|
61
61
|
const resultLines: string[] = [];
|
|
62
|
+
const inputDetails: Array<{ role: string; content: string }> = [];
|
|
62
63
|
|
|
63
64
|
while (this.queue.length > 0) {
|
|
64
65
|
const msg = this.queue.shift()!;
|
|
66
|
+
inputDetails.push({ role: msg.role, content: msg.content });
|
|
65
67
|
try {
|
|
66
68
|
const result = await this.ingestMessage(msg);
|
|
67
69
|
lastSessionKey = msg.sessionKey;
|
|
68
70
|
lastOwner = msg.owner ?? "agent:main";
|
|
69
71
|
lastTimestamp = Math.max(lastTimestamp, msg.timestamp);
|
|
70
|
-
const brief = (s: string) => s.length > 80 ? s.slice(0, 80) + "…" : s;
|
|
71
72
|
if (result === "skipped") {
|
|
72
73
|
skipped++;
|
|
73
|
-
resultLines.push(
|
|
74
|
+
resultLines.push(JSON.stringify({ role: msg.role, action: "exact-dup", summary: "", content: msg.content }));
|
|
74
75
|
} else if (result.action === "stored") {
|
|
75
76
|
stored++;
|
|
76
|
-
resultLines.push(
|
|
77
|
+
resultLines.push(JSON.stringify({ role: msg.role, action: "stored", summary: result.summary ?? "", content: msg.content }));
|
|
77
78
|
} else if (result.action === "duplicate") {
|
|
78
79
|
duplicated++;
|
|
79
|
-
resultLines.push(
|
|
80
|
+
resultLines.push(JSON.stringify({ role: msg.role, action: "dedup", reason: result.reason ?? "similar", summary: result.summary ?? "", content: msg.content }));
|
|
80
81
|
} else if (result.action === "merged") {
|
|
81
82
|
merged++;
|
|
82
|
-
resultLines.push(
|
|
83
|
+
resultLines.push(JSON.stringify({ role: msg.role, action: "merged", summary: result.summary ?? "", content: msg.content }));
|
|
83
84
|
}
|
|
84
85
|
} catch (err) {
|
|
85
86
|
errors++;
|
|
86
|
-
|
|
87
|
-
resultLines.push(`[${msg.role}] ❌ error → ${brief(msg.content)}`);
|
|
87
|
+
resultLines.push(JSON.stringify({ role: msg.role, action: "error", summary: "", content: msg.content }));
|
|
88
88
|
this.ctx.log.error(`Failed to ingest message turn=${msg.turnId}: ${err}`);
|
|
89
89
|
}
|
|
90
90
|
}
|
|
@@ -97,6 +97,7 @@ export class IngestWorker {
|
|
|
97
97
|
const inputInfo = {
|
|
98
98
|
session: lastSessionKey,
|
|
99
99
|
messages: batchSize,
|
|
100
|
+
details: inputDetails,
|
|
100
101
|
};
|
|
101
102
|
const stats = [`stored=${stored}`, skipped > 0 ? `skipped=${skipped}` : null, duplicated > 0 ? `dedup=${duplicated}` : null, merged > 0 ? `merged=${merged}` : null, errors > 0 ? `errors=${errors}` : null].filter(Boolean).join(", ");
|
|
102
103
|
this.store.recordApiLog("memory_add", inputInfo, `${stats}\n${resultLines.join("\n")}`, dur, errors === 0);
|
|
@@ -122,8 +123,7 @@ export class IngestWorker {
|
|
|
122
123
|
private async ingestMessage(msg: ConversationMessage): Promise<
|
|
123
124
|
"skipped" | { action: "stored" | "duplicate" | "merged"; summary?: string; reason?: string }
|
|
124
125
|
> {
|
|
125
|
-
|
|
126
|
-
return await this.storeChunk(msg, msg.content, kind, 0);
|
|
126
|
+
return await this.storeChunk(msg, msg.content, "paragraph", 0);
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
private async storeChunk(
|
|
@@ -146,6 +146,8 @@ export class IngestWorker {
|
|
|
146
146
|
let dedupTarget: string | null = null;
|
|
147
147
|
let dedupReason: string | null = null;
|
|
148
148
|
let mergedFromOld: string | null = null;
|
|
149
|
+
let mergeCount = 0;
|
|
150
|
+
let mergeHistory = "[]";
|
|
149
151
|
|
|
150
152
|
// Fast path: exact content_hash match within same owner (agent dimension)
|
|
151
153
|
const chunkOwner = msg.owner ?? "agent:main";
|
|
@@ -160,7 +162,7 @@ export class IngestWorker {
|
|
|
160
162
|
|
|
161
163
|
// Smart dedup: find Top-5 similar chunks, then ask LLM to judge
|
|
162
164
|
if (dedupStatus === "active" && embedding) {
|
|
163
|
-
const similarThreshold = this.ctx.config.dedup?.similarityThreshold ?? 0.
|
|
165
|
+
const similarThreshold = this.ctx.config.dedup?.similarityThreshold ?? 0.80;
|
|
164
166
|
const dedupOwnerFilter = msg.owner ? [msg.owner] : undefined;
|
|
165
167
|
const topSimilar = findTopSimilar(this.store, embedding, similarThreshold, 5, this.ctx.log, dedupOwnerFilter);
|
|
166
168
|
|
|
@@ -208,7 +210,23 @@ export class IngestWorker {
|
|
|
208
210
|
|
|
209
211
|
mergedFromOld = targetChunkId;
|
|
210
212
|
dedupReason = dedupResult.reason;
|
|
211
|
-
|
|
213
|
+
|
|
214
|
+
// Inherit merge history from the old chunk
|
|
215
|
+
if (oldChunk) {
|
|
216
|
+
const oldHistory = JSON.parse(oldChunk.mergeHistory || "[]");
|
|
217
|
+
oldHistory.push({
|
|
218
|
+
action: "merge",
|
|
219
|
+
at: Date.now(),
|
|
220
|
+
reason: dedupResult.reason,
|
|
221
|
+
from: oldSummary,
|
|
222
|
+
to: dedupResult.mergedSummary,
|
|
223
|
+
sourceChunkId: targetChunkId,
|
|
224
|
+
});
|
|
225
|
+
mergeHistory = JSON.stringify(oldHistory);
|
|
226
|
+
mergeCount = (oldChunk.mergeCount || 0) + 1;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
this.ctx.log.debug(`Smart dedup: UPDATE → old chunk=${targetChunkId} retired, new chunk=${chunkId} gets merged summary (mergeCount=${mergeCount}), reason: ${dedupResult.reason}`);
|
|
212
230
|
}
|
|
213
231
|
}
|
|
214
232
|
|
|
@@ -235,9 +253,9 @@ export class IngestWorker {
|
|
|
235
253
|
dedupStatus,
|
|
236
254
|
dedupTarget,
|
|
237
255
|
dedupReason,
|
|
238
|
-
mergeCount:
|
|
256
|
+
mergeCount: mergeCount,
|
|
239
257
|
lastHitAt: null,
|
|
240
|
-
mergeHistory:
|
|
258
|
+
mergeHistory: mergeHistory,
|
|
241
259
|
createdAt: msg.timestamp,
|
|
242
260
|
updatedAt: msg.timestamp,
|
|
243
261
|
};
|