@memtensor/memos-local-openclaw-plugin 0.3.20 → 1.0.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 (106) hide show
  1. package/README.md +239 -22
  2. package/dist/capture/index.d.ts +1 -1
  3. package/dist/capture/index.d.ts.map +1 -1
  4. package/dist/capture/index.js +33 -8
  5. package/dist/capture/index.js.map +1 -1
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +2 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/ingest/providers/anthropic.d.ts.map +1 -1
  11. package/dist/ingest/providers/anthropic.js +22 -8
  12. package/dist/ingest/providers/anthropic.js.map +1 -1
  13. package/dist/ingest/providers/bedrock.d.ts.map +1 -1
  14. package/dist/ingest/providers/bedrock.js +22 -8
  15. package/dist/ingest/providers/bedrock.js.map +1 -1
  16. package/dist/ingest/providers/gemini.d.ts.map +1 -1
  17. package/dist/ingest/providers/gemini.js +22 -8
  18. package/dist/ingest/providers/gemini.js.map +1 -1
  19. package/dist/ingest/providers/index.d.ts +13 -18
  20. package/dist/ingest/providers/index.d.ts.map +1 -1
  21. package/dist/ingest/providers/index.js +213 -139
  22. package/dist/ingest/providers/index.js.map +1 -1
  23. package/dist/ingest/providers/openai.d.ts +1 -1
  24. package/dist/ingest/providers/openai.d.ts.map +1 -1
  25. package/dist/ingest/providers/openai.js +37 -17
  26. package/dist/ingest/providers/openai.js.map +1 -1
  27. package/dist/ingest/task-processor.d.ts +28 -3
  28. package/dist/ingest/task-processor.d.ts.map +1 -1
  29. package/dist/ingest/task-processor.js +166 -67
  30. package/dist/ingest/task-processor.js.map +1 -1
  31. package/dist/ingest/worker.d.ts.map +1 -1
  32. package/dist/ingest/worker.js +97 -75
  33. package/dist/ingest/worker.js.map +1 -1
  34. package/dist/shared/llm-call.d.ts +26 -0
  35. package/dist/shared/llm-call.d.ts.map +1 -0
  36. package/dist/shared/llm-call.js +163 -0
  37. package/dist/shared/llm-call.js.map +1 -0
  38. package/dist/skill/evaluator.d.ts +0 -3
  39. package/dist/skill/evaluator.d.ts.map +1 -1
  40. package/dist/skill/evaluator.js +34 -59
  41. package/dist/skill/evaluator.js.map +1 -1
  42. package/dist/skill/evolver.d.ts +22 -1
  43. package/dist/skill/evolver.d.ts.map +1 -1
  44. package/dist/skill/evolver.js +191 -32
  45. package/dist/skill/evolver.js.map +1 -1
  46. package/dist/skill/generator.d.ts +0 -3
  47. package/dist/skill/generator.d.ts.map +1 -1
  48. package/dist/skill/generator.js +15 -50
  49. package/dist/skill/generator.js.map +1 -1
  50. package/dist/skill/upgrader.d.ts +0 -2
  51. package/dist/skill/upgrader.d.ts.map +1 -1
  52. package/dist/skill/upgrader.js +4 -39
  53. package/dist/skill/upgrader.js.map +1 -1
  54. package/dist/skill/validator.d.ts +0 -2
  55. package/dist/skill/validator.d.ts.map +1 -1
  56. package/dist/skill/validator.js +14 -44
  57. package/dist/skill/validator.js.map +1 -1
  58. package/dist/storage/sqlite.d.ts +13 -2
  59. package/dist/storage/sqlite.d.ts.map +1 -1
  60. package/dist/storage/sqlite.js +92 -15
  61. package/dist/storage/sqlite.js.map +1 -1
  62. package/dist/tools/memory-get.d.ts.map +1 -1
  63. package/dist/tools/memory-get.js +5 -1
  64. package/dist/tools/memory-get.js.map +1 -1
  65. package/dist/tools/memory-search.d.ts.map +1 -1
  66. package/dist/tools/memory-search.js +5 -0
  67. package/dist/tools/memory-search.js.map +1 -1
  68. package/dist/tools/memory-timeline.d.ts.map +1 -1
  69. package/dist/tools/memory-timeline.js +11 -2
  70. package/dist/tools/memory-timeline.js.map +1 -1
  71. package/dist/types.d.ts +2 -1
  72. package/dist/types.d.ts.map +1 -1
  73. package/dist/types.js +1 -1
  74. package/dist/types.js.map +1 -1
  75. package/dist/viewer/html.d.ts +1 -1
  76. package/dist/viewer/html.d.ts.map +1 -1
  77. package/dist/viewer/html.js +380 -26
  78. package/dist/viewer/html.js.map +1 -1
  79. package/dist/viewer/server.d.ts +9 -0
  80. package/dist/viewer/server.d.ts.map +1 -1
  81. package/dist/viewer/server.js +549 -184
  82. package/dist/viewer/server.js.map +1 -1
  83. package/index.ts +9 -3
  84. package/package.json +2 -1
  85. package/src/capture/index.ts +39 -10
  86. package/src/index.ts +3 -2
  87. package/src/ingest/providers/anthropic.ts +22 -8
  88. package/src/ingest/providers/bedrock.ts +22 -8
  89. package/src/ingest/providers/gemini.ts +22 -8
  90. package/src/ingest/providers/index.ts +192 -142
  91. package/src/ingest/providers/openai.ts +37 -17
  92. package/src/ingest/task-processor.ts +183 -65
  93. package/src/ingest/worker.ts +98 -77
  94. package/src/shared/llm-call.ts +144 -0
  95. package/src/skill/evaluator.ts +35 -64
  96. package/src/skill/evolver.ts +201 -33
  97. package/src/skill/generator.ts +16 -59
  98. package/src/skill/upgrader.ts +5 -43
  99. package/src/skill/validator.ts +15 -47
  100. package/src/storage/sqlite.ts +107 -15
  101. package/src/tools/memory-get.ts +6 -1
  102. package/src/tools/memory-search.ts +6 -0
  103. package/src/tools/memory-timeline.ts +13 -1
  104. package/src/types.ts +2 -1
  105. package/src/viewer/html.ts +380 -26
  106. package/src/viewer/server.ts +535 -197
@@ -76,16 +76,30 @@ export async function summarizeTaskBedrock(
76
76
  return json.output?.message?.content?.[0]?.text?.trim() ?? "";
77
77
  }
78
78
 
79
- const 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.
79
+ const TOPIC_JUDGE_PROMPT = `You are a conversation topic boundary detector. Given the CURRENT task context (may include opening topic + recent exchanges) and a single NEW user message, decide if the new message belongs to the SAME task or starts a NEW one.
80
80
 
81
81
  Answer ONLY "NEW" or "SAME".
82
82
 
83
- Rules:
84
- - "NEW" = the new message is about a completely different subject, project, or task
85
- - "SAME" = the new message continues, follows up on, or is closely related to the current topic
86
- - Follow-up questions, clarifications, refinements, bug fixes, or next steps on the same task = SAME
87
- - Greetings or meta-questions like "你好" or "谢谢" without new substance = SAME
88
- - A clearly unrelated request (e.g., current topic is deployment, new message asks about cooking) = NEW
83
+ SAME — the new message:
84
+ - Continues, follows up on, refines, or corrects the same subject/project/task
85
+ - Asks a clarification or next-step question about what was just discussed
86
+ - Reports a result, error, or feedback about the current task
87
+ - Discusses different tools, methods, or approaches for the SAME goal (e.g., learning English via BBC → via ChatGPT → via AI tools = all SAME "learning English" task)
88
+ - Mentions a related technology or platform in the context of the current goal
89
+ - Is a short acknowledgment (ok, thanks, 好的, 嗯) in direct response to the current flow
90
+
91
+ NEW — the new message:
92
+ - Introduces a clearly UNRELATED subject with NO logical connection to the current task
93
+ - The topic has ZERO overlap with any aspect of the current conversation (e.g., from "learning English" to "what's the weather tomorrow")
94
+ - Starts a request about a completely different domain or life area
95
+ - Begins with a new greeting/reset followed by a different topic
96
+
97
+ Key principles:
98
+ - STRONGLY lean toward SAME — only mark NEW for obvious, unambiguous topic shifts
99
+ - Different aspects, tools, or methods related to the same overall goal are SAME
100
+ - If the new message could reasonably be interpreted as part of the ongoing discussion, choose SAME
101
+ - Only choose NEW when there is absolutely no thematic connection to the current task
102
+ - Examples: "学英语" → "用AI工具学英语" = SAME; "学英语" → "明天天气" = NEW
89
103
 
90
104
  Output exactly one word: NEW or SAME`;
91
105
 
@@ -107,7 +121,7 @@ export async function judgeNewTopicBedrock(
107
121
  ...cfg.headers,
108
122
  };
109
123
 
110
- const userContent = `CURRENT CONVERSATION SUMMARY:\n${currentContext}\n\nNEW USER MESSAGE:\n${newMessage}`;
124
+ const userContent = `CURRENT TASK CONTEXT:\n${currentContext}\n\n---\n\nNEW USER MESSAGE:\n${newMessage}`;
111
125
 
112
126
  const resp = await fetch(url, {
113
127
  method: "POST",
@@ -75,16 +75,30 @@ export async function summarizeTaskGemini(
75
75
  return json.candidates?.[0]?.content?.parts?.[0]?.text?.trim() ?? "";
76
76
  }
77
77
 
78
- const 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.
78
+ const TOPIC_JUDGE_PROMPT = `You are a conversation topic boundary detector. Given the CURRENT task context (may include opening topic + recent exchanges) and a single NEW user message, decide if the new message belongs to the SAME task or starts a NEW one.
79
79
 
80
80
  Answer ONLY "NEW" or "SAME".
81
81
 
82
- Rules:
83
- - "NEW" = the new message is about a completely different subject, project, or task
84
- - "SAME" = the new message continues, follows up on, or is closely related to the current topic
85
- - Follow-up questions, clarifications, refinements, bug fixes, or next steps on the same task = SAME
86
- - Greetings or meta-questions like "你好" or "谢谢" without new substance = SAME
87
- - A clearly unrelated request (e.g., current topic is deployment, new message asks about cooking) = NEW
82
+ SAME — the new message:
83
+ - Continues, follows up on, refines, or corrects the same subject/project/task
84
+ - Asks a clarification or next-step question about what was just discussed
85
+ - Reports a result, error, or feedback about the current task
86
+ - Discusses different tools, methods, or approaches for the SAME goal (e.g., learning English via BBC → via ChatGPT → via AI tools = all SAME "learning English" task)
87
+ - Mentions a related technology or platform in the context of the current goal
88
+ - Is a short acknowledgment (ok, thanks, 好的, 嗯) in direct response to the current flow
89
+
90
+ NEW — the new message:
91
+ - Introduces a clearly UNRELATED subject with NO logical connection to the current task
92
+ - The topic has ZERO overlap with any aspect of the current conversation (e.g., from "learning English" to "what's the weather tomorrow")
93
+ - Starts a request about a completely different domain or life area
94
+ - Begins with a new greeting/reset followed by a different topic
95
+
96
+ Key principles:
97
+ - STRONGLY lean toward SAME — only mark NEW for obvious, unambiguous topic shifts
98
+ - Different aspects, tools, or methods related to the same overall goal are SAME
99
+ - If the new message could reasonably be interpreted as part of the ongoing discussion, choose SAME
100
+ - Only choose NEW when there is absolutely no thematic connection to the current task
101
+ - Examples: "学英语" → "用AI工具学英语" = SAME; "学英语" → "明天天气" = NEW
88
102
 
89
103
  Output exactly one word: NEW or SAME`;
90
104
 
@@ -105,7 +119,7 @@ export async function judgeNewTopicGemini(
105
119
  ...cfg.headers,
106
120
  };
107
121
 
108
- const userContent = `CURRENT CONVERSATION SUMMARY:\n${currentContext}\n\nNEW USER MESSAGE:\n${newMessage}`;
122
+ const userContent = `CURRENT TASK CONTEXT:\n${currentContext}\n\n---\n\nNEW USER MESSAGE:\n${newMessage}`;
109
123
 
110
124
  const resp = await fetch(url, {
111
125
  method: "POST",
@@ -1,3 +1,5 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
1
3
  import type { SummarizerConfig, Logger } from "../../types";
2
4
  import { summarizeOpenAI, summarizeTaskOpenAI, judgeNewTopicOpenAI, filterRelevantOpenAI, judgeDedupOpenAI } from "./openai";
3
5
  import type { FilterResult, DedupResult } from "./openai";
@@ -6,190 +8,238 @@ import { summarizeAnthropic, summarizeTaskAnthropic, judgeNewTopicAnthropic, fil
6
8
  import { summarizeGemini, summarizeTaskGemini, judgeNewTopicGemini, filterRelevantGemini, judgeDedupGemini } from "./gemini";
7
9
  import { summarizeBedrock, summarizeTaskBedrock, judgeNewTopicBedrock, filterRelevantBedrock, judgeDedupBedrock } from "./bedrock";
8
10
 
11
+ /**
12
+ * Build a SummarizerConfig from OpenClaw's native model configuration (openclaw.json).
13
+ * This serves as the final fallback when both strongCfg and plugin summarizer fail or are absent.
14
+ */
15
+ function loadOpenClawFallbackConfig(log: Logger): SummarizerConfig | undefined {
16
+ try {
17
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
18
+ const cfgPath = path.join(home, ".openclaw", "openclaw.json");
19
+ if (!fs.existsSync(cfgPath)) return undefined;
20
+
21
+ const raw = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
22
+
23
+ const agentModel: string | undefined = raw?.agents?.defaults?.model?.primary;
24
+ if (!agentModel) return undefined;
25
+
26
+ const [providerKey, modelId] = agentModel.includes("/")
27
+ ? agentModel.split("/", 2)
28
+ : [undefined, agentModel];
29
+
30
+ const providerCfg = providerKey
31
+ ? raw?.models?.providers?.[providerKey]
32
+ : Object.values(raw?.models?.providers ?? {})[0] as any;
33
+ if (!providerCfg) return undefined;
34
+
35
+ const baseUrl: string | undefined = providerCfg.baseUrl;
36
+ const apiKey: string | undefined = providerCfg.apiKey;
37
+ if (!baseUrl || !apiKey) return undefined;
38
+
39
+ const endpoint = baseUrl.endsWith("/chat/completions")
40
+ ? baseUrl
41
+ : baseUrl.replace(/\/+$/, "") + "/chat/completions";
42
+
43
+ log.debug(`OpenClaw fallback model: ${modelId} via ${baseUrl}`);
44
+ return {
45
+ provider: "openai_compatible",
46
+ endpoint,
47
+ apiKey,
48
+ model: modelId,
49
+ };
50
+ } catch (err) {
51
+ log.debug(`Failed to load OpenClaw fallback config: ${err}`);
52
+ return undefined;
53
+ }
54
+ }
55
+
9
56
  export class Summarizer {
57
+ private strongCfg: SummarizerConfig | undefined;
58
+ private fallbackCfg: SummarizerConfig | undefined;
59
+
10
60
  constructor(
11
61
  private cfg: SummarizerConfig | undefined,
12
62
  private log: Logger,
13
- ) {}
63
+ strongCfg?: SummarizerConfig,
64
+ ) {
65
+ this.strongCfg = strongCfg;
66
+ this.fallbackCfg = loadOpenClawFallbackConfig(log);
67
+ }
14
68
 
15
- async summarize(text: string): Promise<string> {
16
- if (!this.cfg) {
17
- return ruleFallback(text);
69
+ /**
70
+ * Ordered config chain: strongCfg → cfg → fallbackCfg (OpenClaw native model).
71
+ * Returns configs that are defined, in priority order.
72
+ */
73
+ private getConfigChain(): SummarizerConfig[] {
74
+ const chain: SummarizerConfig[] = [];
75
+ if (this.strongCfg) chain.push(this.strongCfg);
76
+ if (this.cfg) chain.push(this.cfg);
77
+ if (this.fallbackCfg) chain.push(this.fallbackCfg);
78
+ return chain;
79
+ }
80
+
81
+ /**
82
+ * Try calling fn with each config in the chain until one succeeds.
83
+ * Returns undefined if all fail.
84
+ */
85
+ private async tryChain<T>(
86
+ label: string,
87
+ fn: (cfg: SummarizerConfig) => Promise<T>,
88
+ ): Promise<T | undefined> {
89
+ const chain = this.getConfigChain();
90
+ for (let i = 0; i < chain.length; i++) {
91
+ try {
92
+ return await fn(chain[i]);
93
+ } catch (err) {
94
+ const level = i < chain.length - 1 ? "warn" : "error";
95
+ const modelInfo = `${chain[i].provider}/${chain[i].model ?? "?"}`;
96
+ this.log[level](`${label} failed (${modelInfo}), ${i < chain.length - 1 ? "trying next" : "no more fallbacks"}: ${err}`);
97
+ }
18
98
  }
99
+ return undefined;
100
+ }
19
101
 
20
- try {
21
- return await this.callProvider(text);
22
- } catch (err) {
23
- this.log.warn(`Summarizer provider failed, using rule fallback: ${err}`);
102
+ async summarize(text: string): Promise<string> {
103
+ if (!this.cfg && !this.fallbackCfg) {
24
104
  return ruleFallback(text);
25
105
  }
106
+
107
+ const result = await this.tryChain("summarize", (cfg) => callSummarize(cfg, text, this.log));
108
+ return result ?? ruleFallback(text);
26
109
  }
27
110
 
28
111
  async summarizeTask(text: string): Promise<string> {
29
- if (!this.cfg) {
30
- return taskFallback(text);
31
- }
32
-
33
- try {
34
- return await this.callTaskProvider(text);
35
- } catch (err) {
36
- this.log.warn(`Task summarizer failed, using fallback: ${err}`);
112
+ if (!this.cfg && !this.fallbackCfg) {
37
113
  return taskFallback(text);
38
114
  }
39
- }
40
115
 
41
- private async callProvider(text: string): Promise<string> {
42
- const cfg = this.cfg!;
43
- switch (cfg.provider) {
44
- case "openai":
45
- case "openai_compatible":
46
- return summarizeOpenAI(text, cfg, this.log);
47
- case "anthropic":
48
- return summarizeAnthropic(text, cfg, this.log);
49
- case "gemini":
50
- return summarizeGemini(text, cfg, this.log);
51
- case "azure_openai":
52
- return summarizeOpenAI(text, cfg, this.log);
53
- case "bedrock":
54
- return summarizeBedrock(text, cfg, this.log);
55
- default:
56
- throw new Error(`Unknown summarizer provider: ${cfg.provider}`);
57
- }
116
+ const result = await this.tryChain("summarizeTask", (cfg) => callSummarizeTask(cfg, text, this.log));
117
+ return result ?? taskFallback(text);
58
118
  }
59
119
 
60
- /**
61
- * Ask the LLM whether the new message starts a different topic from the current conversation.
62
- * Returns true if it's a new topic, false if it continues the current one.
63
- * Returns null if no summarizer is configured (caller should fall back to heuristic).
64
- */
65
120
  async judgeNewTopic(currentContext: string, newMessage: string): Promise<boolean | null> {
66
- if (!this.cfg) return null;
67
-
68
- try {
69
- return await this.callTopicJudge(currentContext, newMessage);
70
- } catch (err) {
71
- this.log.warn(`Topic judge failed: ${err}`);
72
- return null;
73
- }
74
- }
121
+ if (!this.cfg && !this.fallbackCfg) return null;
75
122
 
76
- private async callTopicJudge(currentContext: string, newMessage: string): Promise<boolean> {
77
- const cfg = this.cfg!;
78
- switch (cfg.provider) {
79
- case "openai":
80
- case "openai_compatible":
81
- case "azure_openai":
82
- return judgeNewTopicOpenAI(currentContext, newMessage, cfg, this.log);
83
- case "anthropic":
84
- return judgeNewTopicAnthropic(currentContext, newMessage, cfg, this.log);
85
- case "gemini":
86
- return judgeNewTopicGemini(currentContext, newMessage, cfg, this.log);
87
- case "bedrock":
88
- return judgeNewTopicBedrock(currentContext, newMessage, cfg, this.log);
89
- default:
90
- throw new Error(`Unknown summarizer provider: ${cfg.provider}`);
91
- }
123
+ const result = await this.tryChain("judgeNewTopic", (cfg) => callTopicJudge(cfg, currentContext, newMessage, this.log));
124
+ return result ?? null;
92
125
  }
93
126
 
94
- /**
95
- * Filter search results by LLM relevance judgment.
96
- * Returns { relevant: number[], sufficient: boolean } or null if no summarizer configured.
97
- */
98
127
  async filterRelevant(
99
128
  query: string,
100
129
  candidates: Array<{ index: number; summary: string; role: string }>,
101
130
  ): Promise<FilterResult | null> {
102
- if (!this.cfg) return null;
131
+ if (!this.cfg && !this.fallbackCfg) return null;
103
132
  if (candidates.length === 0) return { relevant: [], sufficient: true };
104
133
 
105
- try {
106
- return await this.callFilterRelevant(query, candidates);
107
- } catch (err) {
108
- this.log.warn(`filterRelevant failed, returning all candidates: ${err}`);
109
- return null;
110
- }
111
- }
112
-
113
- private async callFilterRelevant(
114
- query: string,
115
- candidates: Array<{ index: number; summary: string; role: string }>,
116
- ): Promise<FilterResult> {
117
- const cfg = this.cfg!;
118
- switch (cfg.provider) {
119
- case "openai":
120
- case "openai_compatible":
121
- case "azure_openai":
122
- return filterRelevantOpenAI(query, candidates, cfg, this.log);
123
- case "anthropic":
124
- return filterRelevantAnthropic(query, candidates, cfg, this.log);
125
- case "gemini":
126
- return filterRelevantGemini(query, candidates, cfg, this.log);
127
- case "bedrock":
128
- return filterRelevantBedrock(query, candidates, cfg, this.log);
129
- default:
130
- throw new Error(`Unknown summarizer provider: ${cfg.provider}`);
131
- }
134
+ const result = await this.tryChain("filterRelevant", (cfg) => callFilterRelevant(cfg, query, candidates, this.log));
135
+ return result ?? null;
132
136
  }
133
137
 
134
- /**
135
- * Judge whether a new memory is DUPLICATE / UPDATE / NEW relative to similar existing memories.
136
- * Returns null if no summarizer configured (caller should treat as NEW).
137
- */
138
138
  async judgeDedup(
139
139
  newSummary: string,
140
140
  candidates: Array<{ index: number; summary: string; chunkId: string }>,
141
141
  ): Promise<DedupResult | null> {
142
- if (!this.cfg) return null;
142
+ if (!this.cfg && !this.fallbackCfg) return null;
143
143
  if (candidates.length === 0) return null;
144
144
 
145
- try {
146
- return await this.callJudgeDedup(newSummary, candidates);
147
- } catch (err) {
148
- this.log.warn(`judgeDedup failed, treating as NEW: ${err}`);
149
- return { action: "NEW", reason: "llm_error" };
150
- }
145
+ const result = await this.tryChain("judgeDedup", (cfg) => callJudgeDedup(cfg, newSummary, candidates, this.log));
146
+ return result ?? { action: "NEW", reason: "all_models_failed" };
151
147
  }
152
148
 
153
- private async callJudgeDedup(
154
- newSummary: string,
155
- candidates: Array<{ index: number; summary: string; chunkId: string }>,
156
- ): Promise<DedupResult> {
157
- const cfg = this.cfg!;
158
- switch (cfg.provider) {
159
- case "openai":
160
- case "openai_compatible":
161
- case "azure_openai":
162
- return judgeDedupOpenAI(newSummary, candidates, cfg, this.log);
163
- case "anthropic":
164
- return judgeDedupAnthropic(newSummary, candidates, cfg, this.log);
165
- case "gemini":
166
- return judgeDedupGemini(newSummary, candidates, cfg, this.log);
167
- case "bedrock":
168
- return judgeDedupBedrock(newSummary, candidates, cfg, this.log);
169
- default:
170
- throw new Error(`Unknown summarizer provider: ${cfg.provider}`);
171
- }
149
+ getStrongConfig(): SummarizerConfig | undefined {
150
+ return this.strongCfg;
172
151
  }
152
+ }
173
153
 
174
- private async callTaskProvider(text: string): Promise<string> {
175
- const cfg = this.cfg!;
176
- switch (cfg.provider) {
177
- case "openai":
178
- case "openai_compatible":
179
- case "azure_openai":
180
- return summarizeTaskOpenAI(text, cfg, this.log);
181
- case "anthropic":
182
- return summarizeTaskAnthropic(text, cfg, this.log);
183
- case "gemini":
184
- return summarizeTaskGemini(text, cfg, this.log);
185
- case "bedrock":
186
- return summarizeTaskBedrock(text, cfg, this.log);
187
- default:
188
- throw new Error(`Unknown summarizer provider: ${cfg.provider}`);
189
- }
154
+ // ─── Dispatch helpers ───
155
+
156
+ function callSummarize(cfg: SummarizerConfig, text: string, log: Logger): Promise<string> {
157
+ switch (cfg.provider) {
158
+ case "openai":
159
+ case "openai_compatible":
160
+ case "azure_openai":
161
+ return summarizeOpenAI(text, cfg, log);
162
+ case "anthropic":
163
+ return summarizeAnthropic(text, cfg, log);
164
+ case "gemini":
165
+ return summarizeGemini(text, cfg, log);
166
+ case "bedrock":
167
+ return summarizeBedrock(text, cfg, log);
168
+ default:
169
+ throw new Error(`Unknown summarizer provider: ${cfg.provider}`);
170
+ }
171
+ }
172
+
173
+ function callSummarizeTask(cfg: SummarizerConfig, text: string, log: Logger): Promise<string> {
174
+ switch (cfg.provider) {
175
+ case "openai":
176
+ case "openai_compatible":
177
+ case "azure_openai":
178
+ return summarizeTaskOpenAI(text, cfg, log);
179
+ case "anthropic":
180
+ return summarizeTaskAnthropic(text, cfg, log);
181
+ case "gemini":
182
+ return summarizeTaskGemini(text, cfg, log);
183
+ case "bedrock":
184
+ return summarizeTaskBedrock(text, cfg, log);
185
+ default:
186
+ throw new Error(`Unknown summarizer provider: ${cfg.provider}`);
187
+ }
188
+ }
189
+
190
+ function callTopicJudge(cfg: SummarizerConfig, currentContext: string, newMessage: string, log: Logger): Promise<boolean> {
191
+ switch (cfg.provider) {
192
+ case "openai":
193
+ case "openai_compatible":
194
+ case "azure_openai":
195
+ return judgeNewTopicOpenAI(currentContext, newMessage, cfg, log);
196
+ case "anthropic":
197
+ return judgeNewTopicAnthropic(currentContext, newMessage, cfg, log);
198
+ case "gemini":
199
+ return judgeNewTopicGemini(currentContext, newMessage, cfg, log);
200
+ case "bedrock":
201
+ return judgeNewTopicBedrock(currentContext, newMessage, cfg, log);
202
+ default:
203
+ throw new Error(`Unknown summarizer provider: ${cfg.provider}`);
204
+ }
205
+ }
206
+
207
+ function callFilterRelevant(cfg: SummarizerConfig, query: string, candidates: Array<{ index: number; summary: string; role: string }>, log: Logger): Promise<FilterResult> {
208
+ switch (cfg.provider) {
209
+ case "openai":
210
+ case "openai_compatible":
211
+ case "azure_openai":
212
+ return filterRelevantOpenAI(query, candidates, cfg, log);
213
+ case "anthropic":
214
+ return filterRelevantAnthropic(query, candidates, cfg, log);
215
+ case "gemini":
216
+ return filterRelevantGemini(query, candidates, cfg, log);
217
+ case "bedrock":
218
+ return filterRelevantBedrock(query, candidates, cfg, log);
219
+ default:
220
+ throw new Error(`Unknown summarizer provider: ${cfg.provider}`);
190
221
  }
191
222
  }
192
223
 
224
+ function callJudgeDedup(cfg: SummarizerConfig, newSummary: string, candidates: Array<{ index: number; summary: string; chunkId: string }>, log: Logger): Promise<DedupResult> {
225
+ switch (cfg.provider) {
226
+ case "openai":
227
+ case "openai_compatible":
228
+ case "azure_openai":
229
+ return judgeDedupOpenAI(newSummary, candidates, cfg, log);
230
+ case "anthropic":
231
+ return judgeDedupAnthropic(newSummary, candidates, cfg, log);
232
+ case "gemini":
233
+ return judgeDedupGemini(newSummary, candidates, cfg, log);
234
+ case "bedrock":
235
+ return judgeDedupBedrock(newSummary, candidates, cfg, log);
236
+ default:
237
+ throw new Error(`Unknown summarizer provider: ${cfg.provider}`);
238
+ }
239
+ }
240
+
241
+ // ─── Fallbacks ───
242
+
193
243
  function taskFallback(text: string): string {
194
244
  const lines = text.split("\n").filter((l) => l.trim().length > 10);
195
245
  return lines.slice(0, 30).join("\n").slice(0, 2000);
@@ -114,16 +114,30 @@ export async function summarizeOpenAI(
114
114
  return json.choices[0]?.message?.content?.trim() ?? "";
115
115
  }
116
116
 
117
- const 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.
117
+ const TOPIC_JUDGE_PROMPT = `You are a conversation topic boundary detector. Given the CURRENT task context (may include opening topic + recent exchanges) and a single NEW user message, decide if the new message belongs to the SAME task or starts a NEW one.
118
118
 
119
119
  Answer ONLY "NEW" or "SAME".
120
120
 
121
- Rules:
122
- - "NEW" = the new message is about a completely different subject, project, or task
123
- - "SAME" = the new message continues, follows up on, or is closely related to the current topic
124
- - Follow-up questions, clarifications, refinements, bug fixes, or next steps on the same task = SAME
125
- - Greetings or meta-questions like "你好" or "谢谢" without new substance = SAME
126
- - A clearly unrelated request (e.g., current topic is deployment, new message asks about cooking) = NEW
121
+ SAME — the new message:
122
+ - Continues, follows up on, refines, or corrects the same subject/project/task
123
+ - Asks a clarification or next-step question about what was just discussed
124
+ - Reports a result, error, or feedback about the current task
125
+ - Discusses different tools, methods, or approaches for the SAME goal (e.g., learning English via BBC → via ChatGPT → via AI tools = all SAME "learning English" task)
126
+ - Mentions a related technology or platform in the context of the current goal
127
+ - Is a short acknowledgment (ok, thanks, 好的, 嗯) in direct response to the current flow
128
+
129
+ NEW — the new message:
130
+ - Introduces a clearly UNRELATED subject with NO logical connection to the current task
131
+ - The topic has ZERO overlap with any aspect of the current conversation (e.g., from "learning English" to "what's the weather tomorrow")
132
+ - Starts a request about a completely different domain or life area
133
+ - Begins with a new greeting/reset followed by a different topic
134
+
135
+ Key principles:
136
+ - STRONGLY lean toward SAME — only mark NEW for obvious, unambiguous topic shifts
137
+ - Different aspects, tools, or methods related to the same overall goal are SAME
138
+ - If the new message could reasonably be interpreted as part of the ongoing discussion, choose SAME
139
+ - Only choose NEW when there is absolutely no thematic connection to the current task
140
+ - Examples: "学英语" → "用AI工具学英语" = SAME; "学英语" → "明天天气" = NEW
127
141
 
128
142
  Output exactly one word: NEW or SAME`;
129
143
 
@@ -141,7 +155,7 @@ export async function judgeNewTopicOpenAI(
141
155
  ...cfg.headers,
142
156
  };
143
157
 
144
- const userContent = `CURRENT CONVERSATION SUMMARY:\n${currentContext}\n\nNEW USER MESSAGE:\n${newMessage}`;
158
+ const userContent = `CURRENT TASK CONTEXT:\n${currentContext}\n\n---\n\nNEW USER MESSAGE:\n${newMessage}`;
145
159
 
146
160
  const resp = await fetch(endpoint, {
147
161
  method: "POST",
@@ -258,21 +272,27 @@ function parseFilterResult(raw: string, log: Logger): FilterResult {
258
272
 
259
273
  // ─── Smart Dedup: judge whether new memory is DUPLICATE / UPDATE / NEW ───
260
274
 
261
- export const DEDUP_JUDGE_PROMPT = `You are a memory deduplication system. Given a NEW memory summary and several EXISTING memory summaries, determine the relationship.
275
+ export const DEDUP_JUDGE_PROMPT = `You are a memory deduplication system.
276
+
277
+ LANGUAGE RULE (MUST FOLLOW): You MUST reply in the SAME language as the input memories. 如果输入是中文,reason 和 mergedSummary 必须用中文。If input is English, reply in English. This applies to ALL text fields in your JSON output.
278
+
279
+ Given a NEW memory summary and several EXISTING memory summaries, determine the relationship.
262
280
 
263
281
  For each EXISTING memory, the NEW memory is either:
264
- - "DUPLICATE": NEW is fully covered by an EXISTING memory no new information at all
265
- - "UPDATE": NEW contains information that supplements or updates an EXISTING memory (new data, status change, additional detail)
266
- - "NEW": NEW is a different topic/event despite surface similarity
282
+ - "DUPLICATE": NEW conveys the same intent/meaning as an EXISTING memory, even if worded differently. Examples: "请告诉我你的名字" vs "你希望我怎么称呼你"; "新会话已开始" vs "New session started"; greetings with minor variations. If the core information/intent is the same, it IS a duplicate.
283
+ - "UPDATE": NEW contains meaningful additional information that supplements an EXISTING memory (new data, status change, concrete detail not present before)
284
+ - "NEW": NEW covers a genuinely different topic/event with no semantic overlap
285
+
286
+ IMPORTANT: Lean toward DUPLICATE when memories share the same intent, topic, or factual content. Only choose NEW when the topics are truly unrelated. Repetitive conversational patterns (greetings, session starts, identity questions, capability descriptions) across different sessions should be treated as DUPLICATE.
267
287
 
268
288
  Pick the BEST match among all candidates. If none match well, choose "NEW".
269
289
 
270
- Output a single JSON object:
271
- - If DUPLICATE: {"action":"DUPLICATE","targetIndex":2,"reason":"..."}
272
- - If UPDATE: {"action":"UPDATE","targetIndex":3,"reason":"...","mergedSummary":"a combined summary preserving all info from both old and new, same language as input"}
273
- - If NEW: {"action":"NEW","reason":"..."}
290
+ Output a single JSON object (reason and mergedSummary MUST match input language):
291
+ - If DUPLICATE: {"action":"DUPLICATE","targetIndex":2,"reason":"与已有记忆意图相同"}
292
+ - If UPDATE: {"action":"UPDATE","targetIndex":3,"reason":"新记忆补充了额外细节","mergedSummary":"合并后的完整摘要,保留新旧所有信息"}
293
+ - If NEW: {"action":"NEW","reason":"不同主题,无关联"}
274
294
 
275
- CRITICAL: mergedSummary must use the SAME language as the input. Output ONLY the JSON object.`;
295
+ Output ONLY the JSON object, no other text.`;
276
296
 
277
297
  export interface DedupResult {
278
298
  action: "DUPLICATE" | "UPDATE" | "NEW";