recallnest 1.4.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 (261) hide show
  1. package/.claude-plugin/CLAUDE.md +29 -0
  2. package/.claude-plugin/marketplace.json +18 -0
  3. package/.env.example +3 -0
  4. package/.github/workflows/ci.yml +28 -0
  5. package/.mcp.json +9 -0
  6. package/CHANGELOG.md +51 -0
  7. package/CLAUDE.md +42 -0
  8. package/DEMO.md +94 -0
  9. package/EVAL.md +89 -0
  10. package/FAILURES.md +75 -0
  11. package/LICENSE +21 -0
  12. package/OPERATIONS.md +59 -0
  13. package/README.md +427 -0
  14. package/README_CN.md +425 -0
  15. package/ROADMAP.md +198 -0
  16. package/assets/recallnest-workbench.png +0 -0
  17. package/assets/ui/app.js +807 -0
  18. package/assets/ui/index.html +154 -0
  19. package/assets/ui/styles.css +605 -0
  20. package/assets/wechat-qr.jpg +0 -0
  21. package/config.json.example +38 -0
  22. package/daily-recall.sh +87 -0
  23. package/daily-reflections/2026-03-05.md +15 -0
  24. package/daily-reflections/2026-03-06.md +15 -0
  25. package/daily-reflections/2026-03-07.md +7 -0
  26. package/daily-reflections/2026-03-08.md +34 -0
  27. package/daily-reflections/2026-03-09.md +35 -0
  28. package/daily-reflections/2026-03-10.md +36 -0
  29. package/daily-reflections/2026-03-11.md +35 -0
  30. package/daily-reflections/2026-03-12.md +36 -0
  31. package/daily-reflections/2026-03-13.md +35 -0
  32. package/daily-reflections/2026-03-14.md +35 -0
  33. package/daily-reflections/2026-03-15.md +35 -0
  34. package/daily-reflections/2026-03-16.md +41 -0
  35. package/daily-reflections/2026-03-17.md +10 -0
  36. package/daily-reflections/2026-03-18.md +10 -0
  37. package/daily-reflections/2026-03-19.md +10 -0
  38. package/daily-reflections/2026-03-20.md +10 -0
  39. package/daily-reflections/2026-03-21.md +10 -0
  40. package/daily-reflections/2026-03-22.md +10 -0
  41. package/daily-reflections/2026-03-23.md +10 -0
  42. package/daily-reflections/2026-03-24.md +10 -0
  43. package/daily-reflections/2026-03-25.md +32 -0
  44. package/daily-reflections/2026-03-26.md +37 -0
  45. package/daily-reflections/2026-03-27.md +35 -0
  46. package/daily-reflections/2026-03-28.md +35 -0
  47. package/daily-reflections/2026-03-29.md +35 -0
  48. package/daily-reflections/2026-03-30.md +36 -0
  49. package/daily-reflections/2026-03-31.md +35 -0
  50. package/daily-reflections/2026-04-01.md +35 -0
  51. package/docs/api-reference.md +1291 -0
  52. package/docs/architecture.md +168 -0
  53. package/docs/memory-boundary-contract.md +226 -0
  54. package/docs/memory-categories.md +119 -0
  55. package/docs/pipeline-audit-ultramemory.md +177 -0
  56. package/docs/plans/2026-03-08-query-expansion.md +270 -0
  57. package/docs/plans/2026-03-16-continuity-layer.md +196 -0
  58. package/docs/plans/mcp-tiering-plan.md +126 -0
  59. package/docs/research/recall-quality-audit-2026-03-22.md +153 -0
  60. package/docs/self-evolution.md +198 -0
  61. package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +782 -0
  62. package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +77 -0
  63. package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +95 -0
  64. package/eval/cases.json +85 -0
  65. package/eval/continuity/case-seeds.json +90 -0
  66. package/eval/continuity/cases.json +302 -0
  67. package/eval/continuity/memory-seeds.json +26 -0
  68. package/eval/continuity/pattern-seeds.json +100 -0
  69. package/eval/continuity/results-2026-03-16.md +117 -0
  70. package/eval/continuity/results-2026-03-19.md +453 -0
  71. package/eval/continuity/runbook.md +110 -0
  72. package/eval/continuity/seed-cases.md +54 -0
  73. package/eval/reports/2026-03-06-abstract-query-baseline.md +96 -0
  74. package/eval/reports/2026-03-06-aws-ssh-baseline.md +82 -0
  75. package/eval/reports/2026-03-06-baseline.md +82 -0
  76. package/eval/reports/query-expansion.md +138 -0
  77. package/integrations/README.md +57 -0
  78. package/integrations/claude-code/README.md +133 -0
  79. package/integrations/claude-code/claude-md-snippet.md +31 -0
  80. package/integrations/claude-code/setup.sh +66 -0
  81. package/integrations/claude-code/smoke-continuity.sh +452 -0
  82. package/integrations/codex/README.md +49 -0
  83. package/integrations/codex/agents-md-snippet.md +31 -0
  84. package/integrations/codex/setup.sh +58 -0
  85. package/integrations/examples/claude-agent-sdk/memory-agent.ts +182 -0
  86. package/integrations/examples/langchain/memory-chain.py +111 -0
  87. package/integrations/examples/openai-agents-sdk/memory-agent.py +109 -0
  88. package/integrations/gemini-cli/README.md +57 -0
  89. package/integrations/gemini-cli/gemini-md-snippet.md +31 -0
  90. package/integrations/gemini-cli/setup.sh +64 -0
  91. package/integrations/shared/install-managed-block.sh +51 -0
  92. package/lm +19 -0
  93. package/package.json +50 -0
  94. package/plan-v1.2-distributable.md +105 -0
  95. package/scripts/archive-distilled-junk.ts +116 -0
  96. package/scripts/backfill-l0l1l2.ts +98 -0
  97. package/scripts/blind-scan.ts +588 -0
  98. package/scripts/cleanup-step1-exact-dups.ts +117 -0
  99. package/scripts/cleanup-step2-template-dups.ts +113 -0
  100. package/scripts/cleanup-step3-stale-content.ts +149 -0
  101. package/scripts/cleanup-step4-orphan-scopes.ts +131 -0
  102. package/scripts/delete-distilled-junk.ts +88 -0
  103. package/scripts/distill-facts.ts +305 -0
  104. package/scripts/health-check.ts +343 -0
  105. package/scripts/incremental-ingest.sh +71 -0
  106. package/scripts/mlp-health-check.ts +382 -0
  107. package/scripts/pull-claude-desktop.ts +515 -0
  108. package/scripts/re-ingest-cc.sh +66 -0
  109. package/scripts/rebalance-memory-health.ts +181 -0
  110. package/scripts/run-desktop-ingest.sh +11 -0
  111. package/scripts/start-server.sh +12 -0
  112. package/scripts/viz-dashboard.html +423 -0
  113. package/scripts/viz-export.ts +703 -0
  114. package/src/__tests__/access-tracker-novelty.test.ts +228 -0
  115. package/src/__tests__/auto-consolidation.test.ts +168 -0
  116. package/src/__tests__/auto-recall.test.ts +131 -0
  117. package/src/__tests__/batch-dedup.test.ts +65 -0
  118. package/src/__tests__/capture-engine.test.ts +1142 -0
  119. package/src/__tests__/capture-heuristic.test.ts +171 -0
  120. package/src/__tests__/cascade-forget.test.ts +210 -0
  121. package/src/__tests__/category-min-scores.test.ts +33 -0
  122. package/src/__tests__/confidence-tracker.test.ts +196 -0
  123. package/src/__tests__/conflict-advisor.test.ts +206 -0
  124. package/src/__tests__/conflict-escalation.test.ts +149 -0
  125. package/src/__tests__/conflict-lifecycle.test.ts +83 -0
  126. package/src/__tests__/conflict-output.test.ts +85 -0
  127. package/src/__tests__/conflict-store.test.ts +374 -0
  128. package/src/__tests__/consolidation-engine.test.ts +181 -0
  129. package/src/__tests__/context-composer-pins.test.ts +64 -0
  130. package/src/__tests__/context-composer-task-selection.test.ts +24 -0
  131. package/src/__tests__/context-renderer.test.ts +91 -0
  132. package/src/__tests__/continuity-eval.test.ts +275 -0
  133. package/src/__tests__/continuity-schema.test.ts +97 -0
  134. package/src/__tests__/core-recall-eval.test.ts +235 -0
  135. package/src/__tests__/core-summary.test.ts +109 -0
  136. package/src/__tests__/dedup-actions.test.ts +58 -0
  137. package/src/__tests__/deterministic-id.test.ts +39 -0
  138. package/src/__tests__/doctor.test.ts +303 -0
  139. package/src/__tests__/drill-down.test.ts +108 -0
  140. package/src/__tests__/embedder.test.ts +122 -0
  141. package/src/__tests__/entity-resolver.test.ts +58 -0
  142. package/src/__tests__/event-segmenter.test.ts +153 -0
  143. package/src/__tests__/hotness-blend.test.ts +76 -0
  144. package/src/__tests__/ingest.test.ts +360 -0
  145. package/src/__tests__/kg-extractor.test.ts +184 -0
  146. package/src/__tests__/kg-store.test.ts +198 -0
  147. package/src/__tests__/llm-consolidation.test.ts +226 -0
  148. package/src/__tests__/memory-boundaries.test.ts +237 -0
  149. package/src/__tests__/memory-health-rebalance.test.ts +144 -0
  150. package/src/__tests__/memory-output.test.ts +215 -0
  151. package/src/__tests__/multi-vector.test.ts +242 -0
  152. package/src/__tests__/ppr-traversal.test.ts +256 -0
  153. package/src/__tests__/preference-guard-smoke.test.ts +211 -0
  154. package/src/__tests__/preference-matcher.test.ts +230 -0
  155. package/src/__tests__/preference-slots.test.ts +106 -0
  156. package/src/__tests__/prospective-memory.test.ts +283 -0
  157. package/src/__tests__/query-entity-detector.test.ts +171 -0
  158. package/src/__tests__/query-expander.test.ts +34 -0
  159. package/src/__tests__/recall-mode.test.ts +64 -0
  160. package/src/__tests__/result-synthesizer.test.ts +198 -0
  161. package/src/__tests__/resume-context.test.ts +4743 -0
  162. package/src/__tests__/retrieval-trace.test.ts +213 -0
  163. package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
  164. package/src/__tests__/retroactive-boost.test.ts +223 -0
  165. package/src/__tests__/scope-inventory.test.ts +200 -0
  166. package/src/__tests__/scope-policy.test.ts +69 -0
  167. package/src/__tests__/session-checkpoint.test.ts +259 -0
  168. package/src/__tests__/session-gc.test.ts +215 -0
  169. package/src/__tests__/store-scope-audit.test.ts +81 -0
  170. package/src/__tests__/temporal-parser.test.ts +241 -0
  171. package/src/__tests__/term-registry.test.ts +121 -0
  172. package/src/__tests__/tool-output-compressor.test.ts +133 -0
  173. package/src/__tests__/version-manager.test.ts +213 -0
  174. package/src/__tests__/workflow-observation.test.ts +400 -0
  175. package/src/access-tracker.ts +316 -0
  176. package/src/adaptive-retrieval.ts +64 -0
  177. package/src/api-server.ts +667 -0
  178. package/src/asset-sync.ts +79 -0
  179. package/src/auto-consolidation.ts +133 -0
  180. package/src/auto-gc.ts +124 -0
  181. package/src/auto-recall.ts +95 -0
  182. package/src/capture-engine.ts +931 -0
  183. package/src/capture-heuristic.ts +177 -0
  184. package/src/cascade-forget.ts +135 -0
  185. package/src/chunker.ts +253 -0
  186. package/src/cli.ts +1917 -0
  187. package/src/confidence-tracker.ts +166 -0
  188. package/src/conflict-advisor.ts +433 -0
  189. package/src/conflict-engine.ts +181 -0
  190. package/src/conflict-escalation.ts +138 -0
  191. package/src/conflict-lifecycle.ts +67 -0
  192. package/src/conflict-output.ts +218 -0
  193. package/src/conflict-schema.ts +130 -0
  194. package/src/conflict-store.ts +127 -0
  195. package/src/consolidation-engine.ts +256 -0
  196. package/src/context-composer-pins.ts +183 -0
  197. package/src/context-composer-queries.ts +121 -0
  198. package/src/context-composer-scope.ts +56 -0
  199. package/src/context-composer-stable-selection.ts +200 -0
  200. package/src/context-composer-stable.ts +217 -0
  201. package/src/context-composer-task-fallbacks.ts +36 -0
  202. package/src/context-composer-task-ranking.ts +197 -0
  203. package/src/context-composer-task-results.ts +137 -0
  204. package/src/context-composer-task-selection.ts +342 -0
  205. package/src/context-composer-text.ts +57 -0
  206. package/src/context-composer.ts +300 -0
  207. package/src/context-renderer.ts +179 -0
  208. package/src/decay-engine.ts +189 -0
  209. package/src/doctor.ts +543 -0
  210. package/src/embedder.ts +557 -0
  211. package/src/entity-resolver.ts +131 -0
  212. package/src/eval.ts +632 -0
  213. package/src/event-segmenter.ts +216 -0
  214. package/src/ingest.ts +1822 -0
  215. package/src/kg-extractor.ts +217 -0
  216. package/src/kg-store.ts +353 -0
  217. package/src/legacy-scope-review.ts +53 -0
  218. package/src/llm-client.ts +629 -0
  219. package/src/llm-consolidation.ts +168 -0
  220. package/src/mcp-server.ts +1337 -0
  221. package/src/memory-assets.ts +475 -0
  222. package/src/memory-boundaries.ts +354 -0
  223. package/src/memory-health-rebalance.ts +220 -0
  224. package/src/memory-output.ts +474 -0
  225. package/src/memory-schema.ts +184 -0
  226. package/src/multi-vector.ts +234 -0
  227. package/src/noise-filter.ts +155 -0
  228. package/src/ppr-traversal.ts +279 -0
  229. package/src/preference-matcher.ts +174 -0
  230. package/src/preference-slots.ts +259 -0
  231. package/src/prospective-memory.ts +198 -0
  232. package/src/query-entity-detector.ts +207 -0
  233. package/src/query-expander.ts +98 -0
  234. package/src/result-synthesizer.ts +114 -0
  235. package/src/retrieval-profiles.ts +101 -0
  236. package/src/retrieval-trace.ts +94 -0
  237. package/src/retriever.ts +1338 -0
  238. package/src/retroactive-boost.ts +154 -0
  239. package/src/rif.ts +73 -0
  240. package/src/runtime-config.ts +172 -0
  241. package/src/schema-utils.ts +56 -0
  242. package/src/scope-inventory.ts +310 -0
  243. package/src/scope-policy.ts +142 -0
  244. package/src/session-engine.ts +168 -0
  245. package/src/session-output.ts +96 -0
  246. package/src/session-schema.ts +60 -0
  247. package/src/session-store.ts +293 -0
  248. package/src/stderr-log.ts +16 -0
  249. package/src/store.ts +903 -0
  250. package/src/temporal-parser.ts +318 -0
  251. package/src/term-registry.ts +791 -0
  252. package/src/tool-output-compressor.ts +115 -0
  253. package/src/tracker.ts +57 -0
  254. package/src/ui-server.ts +430 -0
  255. package/src/version-manager.ts +172 -0
  256. package/src/workflow-observation-engine.ts +316 -0
  257. package/src/workflow-observation-managed.ts +83 -0
  258. package/src/workflow-observation-output.ts +86 -0
  259. package/src/workflow-observation-schema.ts +87 -0
  260. package/src/workflow-observation-scope-review.ts +409 -0
  261. package/src/workflow-observation-store.ts +69 -0
@@ -0,0 +1,629 @@
1
+ /**
2
+ * LLM Client — OpenAI-compatible wrapper for Qwen / other providers.
3
+ *
4
+ * Used for:
5
+ * - Smart 6-category extraction (profile/preferences/entities/events/cases/patterns)
6
+ * - L0/L1 summary generation (L0 = one-liner, L1 = structured markdown)
7
+ * - Semantic dedup decisions (CREATE / MERGE / SKIP)
8
+ *
9
+ * Design references:
10
+ * - 6-category system: ByteDance OpenViking memory architecture
11
+ * - Weibull decay + tier: hippocampal memory consolidation model
12
+ *
13
+ * Zero new dependencies: reuses the OpenAI SDK already in tree.
14
+ */
15
+
16
+ import OpenAI from "openai";
17
+
18
+ // ============================================================================
19
+ // Types
20
+ // ============================================================================
21
+
22
+ export interface LLMConfig {
23
+ apiKey: string;
24
+ model: string;
25
+ baseURL: string;
26
+ /** Request timeout in ms (default: 15000) */
27
+ timeoutMs?: number;
28
+ /** Temperature (default: 0.1 for consistency) */
29
+ temperature?: number;
30
+ }
31
+
32
+ export interface DedupDecision {
33
+ action: "CREATE" | "MERGE" | "SKIP";
34
+ reason: string;
35
+ }
36
+
37
+ /** Extended dedup decision with optional secondary actions on other existing memories. */
38
+ export interface DedupDecisionMulti extends DedupDecision {
39
+ actions?: Array<{ match_index: number; action: "delete"; reason: string }>;
40
+ }
41
+
42
+ /** Six memory categories (OpenViking-inspired) */
43
+ export type SmartCategory =
44
+ | "profile" | "preferences" | "entities" | "events" | "cases" | "patterns";
45
+
46
+ /** Result of LLM smart extraction */
47
+ export interface SmartExtraction {
48
+ /** One of 6 categories */
49
+ category: SmartCategory;
50
+ /** L0: one-line index summary (≤80 chars) */
51
+ l0: string;
52
+ /** L1: structured markdown overview (2-5 lines) */
53
+ l1: string;
54
+ /** Importance score 0-1 (LLM's estimate) */
55
+ importance: number;
56
+ }
57
+
58
+ /** Category-specific merge strategies */
59
+ export const CATEGORY_MERGE_STRATEGY: Record<SmartCategory, "merge" | "append"> = {
60
+ profile: "merge", // 身份信息:永远合并
61
+ preferences: "merge", // 偏好:合并更新
62
+ entities: "merge", // 实体:合并补充
63
+ events: "append", // 事件:追加,不覆盖
64
+ cases: "append", // 案例:追加,不覆盖
65
+ patterns: "merge", // 模式:合并优化
66
+ };
67
+
68
+ /** Default importance by category */
69
+ export const CATEGORY_DEFAULT_IMPORTANCE: Record<SmartCategory, number> = {
70
+ profile: 0.85, // 身份信息很重要
71
+ preferences: 0.7, // 偏好中等
72
+ entities: 0.65, // 实体中等
73
+ events: 0.5, // 事件一般
74
+ cases: 0.75, // 问题解决方案较重要
75
+ patterns: 0.8, // 可复用模式很重要
76
+ };
77
+
78
+ // ============================================================================
79
+ // Default Config
80
+ // ============================================================================
81
+
82
+ export const DEFAULT_LLM_CONFIG: LLMConfig = {
83
+ apiKey: "${QWEN_API_KEY}",
84
+ model: "qwen-turbo",
85
+ baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
86
+ timeoutMs: 15000,
87
+ temperature: 0.1,
88
+ };
89
+
90
+ // ============================================================================
91
+ // LLM Client
92
+ // ============================================================================
93
+
94
+ export class LLMClient {
95
+ private client: OpenAI;
96
+ private model: string;
97
+ private temperature: number;
98
+ private timeoutMs: number;
99
+
100
+ constructor(config: LLMConfig) {
101
+ const apiKey = resolveEnvVars(config.apiKey);
102
+ this.client = new OpenAI({
103
+ apiKey,
104
+ baseURL: config.baseURL,
105
+ });
106
+ this.model = config.model;
107
+ this.temperature = config.temperature ?? 0.1;
108
+ this.timeoutMs = config.timeoutMs ?? 15000;
109
+ }
110
+
111
+ /**
112
+ * Generate a one-line L0 summary for a memory chunk.
113
+ * Returns null on failure (caller should use extractive fallback).
114
+ */
115
+ async generateL0(text: string): Promise<string | null> {
116
+ try {
117
+ const response = await this.chat(
118
+ "你是记忆索引助手。给以下对话片段写一句话摘要(不超过80字)," +
119
+ "用于快速检索。只输出摘要本身,不加任何前缀。",
120
+ text.slice(0, 2000), // Cap input to avoid token overflow
121
+ );
122
+ if (!response || response.length < 5) return null;
123
+ return response.slice(0, 150);
124
+ } catch {
125
+ return null;
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Smart extraction: classify a chunk into 6 categories and generate L0/L1.
131
+ * This is the core of the OpenViking-inspired memory architecture.
132
+ *
133
+ * Returns null on failure (caller should use fallback classification).
134
+ */
135
+ async smartExtract(text: string): Promise<SmartExtraction | null> {
136
+ try {
137
+ const response = await this.chat(SMART_EXTRACT_SYSTEM_PROMPT, text.slice(0, 2000));
138
+ if (!response) return null;
139
+
140
+ const parsed = parseJSON<SmartExtraction>(response);
141
+ if (!parsed) return null;
142
+
143
+ // Validate category
144
+ const validCategories: SmartCategory[] = ["profile", "preferences", "entities", "events", "cases", "patterns"];
145
+ if (!validCategories.includes(parsed.category)) return null;
146
+
147
+ return {
148
+ category: parsed.category,
149
+ l0: (parsed.l0 || "").slice(0, 150),
150
+ l1: (parsed.l1 || "").slice(0, 500),
151
+ importance: typeof parsed.importance === "number"
152
+ ? Math.max(0, Math.min(1, parsed.importance))
153
+ : CATEGORY_DEFAULT_IMPORTANCE[parsed.category],
154
+ };
155
+ } catch {
156
+ return null;
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Batch smart extraction: process multiple chunks efficiently.
162
+ * Packs up to 3 chunks per request to reduce API calls.
163
+ */
164
+ async smartExtractBatch(texts: string[]): Promise<(SmartExtraction | null)[]> {
165
+ if (texts.length === 0) return [];
166
+ if (texts.length === 1) return [await this.smartExtract(texts[0])];
167
+
168
+ const batchSize = 3; // Smaller batches for structured output reliability
169
+ const results: (SmartExtraction | null)[] = new Array(texts.length).fill(null);
170
+
171
+ for (let i = 0; i < texts.length; i += batchSize) {
172
+ const batch = texts.slice(i, i + batchSize);
173
+ const numbered = batch
174
+ .map((t, idx) => `[${idx + 1}]\n${t.slice(0, 800)}`)
175
+ .join("\n===\n");
176
+
177
+ try {
178
+ const response = await this.chat(
179
+ SMART_EXTRACT_BATCH_PROMPT,
180
+ numbered,
181
+ );
182
+
183
+ if (response) {
184
+ // Parse array of extractions
185
+ const parsed = parseJSON<SmartExtraction[]>(response);
186
+ if (Array.isArray(parsed)) {
187
+ for (let j = 0; j < parsed.length && j < batch.length; j++) {
188
+ const item = parsed[j];
189
+ if (item && item.category && item.l0) {
190
+ results[i + j] = {
191
+ category: item.category,
192
+ l0: (item.l0 || "").slice(0, 150),
193
+ l1: (item.l1 || "").slice(0, 500),
194
+ importance: typeof item.importance === "number"
195
+ ? Math.max(0, Math.min(1, item.importance))
196
+ : CATEGORY_DEFAULT_IMPORTANCE[item.category] ?? 0.6,
197
+ };
198
+ }
199
+ }
200
+ }
201
+ }
202
+ } catch {
203
+ // Fall through — individual nulls remain
204
+ }
205
+ }
206
+
207
+ return results;
208
+ }
209
+
210
+ /**
211
+ * Batch generate L0 summaries for multiple chunks.
212
+ * More efficient than individual calls — packs up to 5 chunks per request.
213
+ */
214
+ async generateL0Batch(texts: string[]): Promise<(string | null)[]> {
215
+ if (texts.length === 0) return [];
216
+ if (texts.length === 1) {
217
+ const result = await this.generateL0(texts[0]);
218
+ return [result];
219
+ }
220
+
221
+ // Pack multiple chunks into one request (up to 5)
222
+ const batchSize = 5;
223
+ const results: (string | null)[] = new Array(texts.length).fill(null);
224
+
225
+ for (let i = 0; i < texts.length; i += batchSize) {
226
+ const batch = texts.slice(i, i + batchSize);
227
+ const numbered = batch
228
+ .map((t, idx) => `[${idx + 1}] ${t.slice(0, 800)}`)
229
+ .join("\n---\n");
230
+
231
+ try {
232
+ const response = await this.chat(
233
+ "你是记忆索引助手。给以下每段对话写一句话摘要(不超过80字)。\n" +
234
+ "输出格式:每行一条,以 [序号] 开头。只输出摘要,不加多余文字。",
235
+ numbered,
236
+ );
237
+
238
+ if (response) {
239
+ const lines = response.split("\n").filter(l => l.trim());
240
+ for (const line of lines) {
241
+ const match = line.match(/^\[(\d+)\]\s*(.+)/);
242
+ if (match) {
243
+ const idx = parseInt(match[1]) - 1;
244
+ if (idx >= 0 && idx < batch.length) {
245
+ results[i + idx] = match[2].trim().slice(0, 150);
246
+ }
247
+ }
248
+ }
249
+ }
250
+ } catch {
251
+ // Fall through — individual nulls remain
252
+ }
253
+ }
254
+
255
+ return results;
256
+ }
257
+
258
+ /**
259
+ * Semantic dedup decision: given a new chunk and an existing similar chunk,
260
+ * decide whether to CREATE (store new), MERGE (combine), or SKIP (discard).
261
+ */
262
+ async dedupDecision(
263
+ newText: string,
264
+ existingText: string,
265
+ ): Promise<DedupDecision> {
266
+ try {
267
+ const response = await this.chat(
268
+ "你是记忆去重助手。比较以下两段记忆,决定新记忆应该如何处理。\n\n" +
269
+ "规则:\n" +
270
+ "- SKIP:新记忆跟已有记忆说的是同一件事,没有新信息\n" +
271
+ "- MERGE:新记忆有补充信息,应该合并到已有记忆\n" +
272
+ "- CREATE:新记忆是不同的事,应该独立存储\n\n" +
273
+ "额外规则:如果两条都是偏好陈述,并且是同品牌/同主题下的不同具体对象或条目(例如不同食物、商品、菜单项),必须返回 CREATE。聚合摘要也不能吞掉新的原子偏好。\n\n" +
274
+ "只输出一行 JSON:{\"action\":\"CREATE|MERGE|SKIP\",\"reason\":\"简短原因\"}",
275
+ `[已有记忆]\n${existingText.slice(0, 1000)}\n\n[新记忆]\n${newText.slice(0, 1000)}`,
276
+ );
277
+
278
+ if (!response) return { action: "CREATE", reason: "LLM 无响应" };
279
+
280
+ const parsed = parseJSON<DedupDecision>(response);
281
+ if (parsed && (parsed.action === "CREATE" || parsed.action === "MERGE" || parsed.action === "SKIP")) {
282
+ return parsed;
283
+ }
284
+
285
+ return { action: "CREATE", reason: "JSON 解析失败" };
286
+ } catch {
287
+ return { action: "CREATE", reason: "LLM 调用失败" };
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Multi-candidate dedup decision: compare a new chunk against multiple existing
293
+ * memories and optionally return secondary delete actions on outdated entries.
294
+ */
295
+ async dedupDecisionMulti(
296
+ newText: string,
297
+ existingEntries: Array<{ id: string; text: string }>,
298
+ ): Promise<DedupDecisionMulti> {
299
+ if (existingEntries.length === 0) {
300
+ return { action: "CREATE", reason: "无候选" };
301
+ }
302
+ // Fallback to simple 1:1 dedup for single candidate
303
+ if (existingEntries.length === 1) {
304
+ return this.dedupDecision(newText, existingEntries[0].text);
305
+ }
306
+
307
+ try {
308
+ const existingBlock = existingEntries
309
+ .map((e, i) => `[已有记忆 ${i + 1}] (${e.id.slice(0, 8)})\n${e.text.slice(0, 500)}`)
310
+ .join("\n\n");
311
+
312
+ const response = await this.chat(
313
+ "你是记忆去重助手。比较新记忆和多条已有记忆,决定新记忆应该如何处理。\n\n" +
314
+ "规则:\n" +
315
+ "- SKIP:新记忆跟某条已有记忆说的是同一件事,没有新信息\n" +
316
+ "- MERGE:新记忆有补充信息,应该合并到已有记忆\n" +
317
+ "- CREATE:新记忆是不同的事,应该独立存储\n" +
318
+ "- 如果两条都是偏好陈述,同品牌/同主题下的不同条目,必须返回 CREATE\n\n" +
319
+ "额外能力:如果发现某些已有记忆已经过时(被新记忆或其他已有记忆完全取代),可以在 actions 里标记删除。\n\n" +
320
+ "只输出一行 JSON:\n" +
321
+ '{"action":"CREATE|MERGE|SKIP","match_index":1,"reason":"简短原因","actions":[{"match_index":2,"action":"delete","reason":"被新记忆取代"}]}' + "\n\n" +
322
+ "- match_index 是 1-based 索引,指向你要操作的已有记忆\n" +
323
+ "- actions 是可选的,只在确实有过时记忆需要清理时才填\n" +
324
+ "- actions 里只支持 delete",
325
+ `${existingBlock}\n\n[新记忆]\n${newText.slice(0, 1000)}`,
326
+ );
327
+
328
+ if (!response) return { action: "CREATE", reason: "LLM 无响应" };
329
+
330
+ const parsed = parseJSON<DedupDecisionMulti>(response);
331
+ if (parsed && (parsed.action === "CREATE" || parsed.action === "MERGE" || parsed.action === "SKIP")) {
332
+ // Validate secondary actions
333
+ const validActions = Array.isArray(parsed.actions)
334
+ ? parsed.actions.filter(
335
+ (a) =>
336
+ typeof a === "object" && a !== null &&
337
+ typeof a.match_index === "number" &&
338
+ a.match_index >= 1 && a.match_index <= existingEntries.length &&
339
+ a.action === "delete",
340
+ )
341
+ : [];
342
+ return {
343
+ action: parsed.action,
344
+ reason: parsed.reason ?? "",
345
+ actions: validActions.length > 0 ? validActions : undefined,
346
+ };
347
+ }
348
+
349
+ return { action: "CREATE", reason: "JSON 解析失败" };
350
+ } catch {
351
+ return { action: "CREATE", reason: "LLM 调用失败" };
352
+ }
353
+ }
354
+
355
+ /**
356
+ * Generate a core summary (≤200 chars) for a memory chunk.
357
+ * More detailed than L0 (~80 chars), more concise than L1 (~500 chars).
358
+ * Used by Tier 3.1 to produce token-efficient context output.
359
+ *
360
+ * @returns Core summary string, or null on failure
361
+ */
362
+ async generateCoreSummary(text: string): Promise<string | null> {
363
+ try {
364
+ return await this.chat(
365
+ "你是记忆精炼助手。把以下内容提炼为一段核心摘要(不超过200字)。\n" +
366
+ "要求:保留关键事实、人名、数字、时间等;去除冗余修辞;直接输出摘要,不加前缀。",
367
+ text.slice(0, 2000),
368
+ );
369
+ } catch {
370
+ return null;
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Batch generate core summaries for multiple texts.
376
+ * Packs up to 3 texts per request for efficiency.
377
+ */
378
+ async generateCoreSummaryBatch(texts: string[]): Promise<(string | null)[]> {
379
+ if (texts.length === 0) return [];
380
+ if (texts.length === 1) return [await this.generateCoreSummary(texts[0])];
381
+
382
+ const results: (string | null)[] = new Array(texts.length).fill(null);
383
+ const batchSize = 3;
384
+
385
+ for (let i = 0; i < texts.length; i += batchSize) {
386
+ const batch = texts.slice(i, i + batchSize);
387
+ const numbered = batch.map((t, idx) => `[${idx + 1}] ${t.slice(0, 800)}`).join("\n---\n");
388
+
389
+ try {
390
+ const raw = await this.chat(
391
+ "你是记忆精炼助手。为每段内容生成核心摘要(每条不超过200字)。\n" +
392
+ "输出 JSON 数组:[\"摘要1\", \"摘要2\", ...]",
393
+ numbered,
394
+ );
395
+ if (raw) {
396
+ const parsed = JSON.parse(
397
+ raw.replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/, ""),
398
+ );
399
+ if (Array.isArray(parsed)) {
400
+ for (let j = 0; j < Math.min(parsed.length, batch.length); j++) {
401
+ results[i + j] = typeof parsed[j] === "string" ? parsed[j] : null;
402
+ }
403
+ }
404
+ }
405
+ } catch {
406
+ // Individual fallback
407
+ for (let j = 0; j < batch.length; j++) {
408
+ results[i + j] = await this.generateCoreSummary(batch[j]);
409
+ }
410
+ }
411
+ }
412
+
413
+ return results;
414
+ }
415
+
416
+ /**
417
+ * Synthesize multiple memory fragments into a coherent narrative.
418
+ * Used by Tier 3.5 result-synthesizer to produce readable context.
419
+ *
420
+ * @param fragments Array of memory text snippets
421
+ * @param query The original query for context
422
+ * @param maxLen Max output length in chars (default: 500)
423
+ * @returns Synthesized text, or null on failure
424
+ */
425
+ async synthesizeFragments(
426
+ fragments: string[],
427
+ query: string,
428
+ maxLen = 500,
429
+ ): Promise<string | null> {
430
+ if (fragments.length === 0) return null;
431
+ if (fragments.length === 1) return fragments[0];
432
+
433
+ const numbered = fragments.map((f, i) => `[${i + 1}] ${f}`).join("\n");
434
+ return this.chat(
435
+ `你是记忆整合助手。把多条碎片记忆整合成一段连贯的叙述。\n` +
436
+ `要求:\n` +
437
+ `- 不超过${maxLen}字\n` +
438
+ `- 去除重复信息,保留所有不同的事实\n` +
439
+ `- 保持客观,不添加原文没有的信息\n` +
440
+ `- 直接输出整合后的文本,不加前缀或解释`,
441
+ `查询:${query}\n\n记忆碎片:\n${numbered}`,
442
+ );
443
+ }
444
+
445
+ /**
446
+ * Test LLM connectivity.
447
+ */
448
+ async test(): Promise<{ success: boolean; error?: string }> {
449
+ try {
450
+ const response = await this.chat("回复 OK", "测试");
451
+ return { success: !!response };
452
+ } catch (err: any) {
453
+ return { success: false, error: err.message };
454
+ }
455
+ }
456
+
457
+ /**
458
+ * Chat + JSON parse helper for structured extraction.
459
+ * Returns null on failure (LLM error or invalid JSON).
460
+ */
461
+ async chatJson<T>(system: string, user: string): Promise<T | null> {
462
+ const raw = await this.chat(system, user);
463
+ if (!raw) return null;
464
+ try {
465
+ // Extract JSON from markdown fences if present
466
+ const fenceMatch = raw.match(/```(?:json)?\s*\n?([\s\S]*?)```/);
467
+ const jsonStr = fenceMatch ? fenceMatch[1].trim() : raw;
468
+ return JSON.parse(jsonStr) as T;
469
+ } catch {
470
+ // Try to find JSON object in the response
471
+ const start = raw.indexOf("{");
472
+ const end = raw.lastIndexOf("}");
473
+ if (start >= 0 && end > start) {
474
+ try {
475
+ return JSON.parse(raw.slice(start, end + 1)) as T;
476
+ } catch {
477
+ return null;
478
+ }
479
+ }
480
+ return null;
481
+ }
482
+ }
483
+
484
+ // --------------------------------------------------------------------------
485
+ // Internals
486
+ // --------------------------------------------------------------------------
487
+
488
+ private async chat(system: string, user: string): Promise<string | null> {
489
+ const controller = new AbortController();
490
+ const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
491
+
492
+ try {
493
+ const response = await this.client.chat.completions.create(
494
+ {
495
+ model: this.model,
496
+ messages: [
497
+ { role: "system", content: system },
498
+ { role: "user", content: user },
499
+ ],
500
+ temperature: this.temperature,
501
+ max_tokens: 500,
502
+ },
503
+ { signal: controller.signal },
504
+ );
505
+
506
+ return response.choices[0]?.message?.content?.trim() || null;
507
+ } finally {
508
+ clearTimeout(timeout);
509
+ }
510
+ }
511
+ }
512
+
513
+ // ============================================================================
514
+ // Smart Extraction Prompts (OpenViking-inspired 6-category system)
515
+ // ============================================================================
516
+
517
+ const SMART_EXTRACT_SYSTEM_PROMPT = `你是 AI 记忆分类助手。把对话片段分类到 6 种记忆类型之一,并生成摘要。
518
+
519
+ ## 6 种记忆类型
520
+
521
+ 用户记忆(4 类):
522
+ - profile:用户身份、背景、职业、技能(长期稳定)
523
+ - preferences:偏好、习惯、喜好(会变但较稳定)
524
+ - entities:项目、工具、人物等持续存在的名词
525
+ - events:发生过的具体事件(一次性)
526
+
527
+ 智能体记忆(2 类):
528
+ - cases:具体的问题→解决方案对(踩坑记录、bug 修复)
529
+ - patterns:可复用的流程、规律、最佳实践
530
+
531
+ ## 分类决策表
532
+ | 关键词线索 | 类别 |
533
+ |-----------|------|
534
+ | "我是…"、身份、背景 | profile |
535
+ | "我喜欢…"、"我习惯…"、偏好 | preferences |
536
+ | 项目名、工具名、人名 | entities |
537
+ | "今天…"、"刚才…"、具体操作 | events |
538
+ | 报错→修复、问题→方案 | cases |
539
+ | "每次…"、"一般…"、规律总结 | patterns |
540
+
541
+ ## 输出格式
542
+ 只输出一个 JSON 对象:
543
+ {"category":"类别","l0":"一句话摘要(≤80字)","l1":"结构化概述(2-5行markdown)","importance":0.7}
544
+
545
+ importance 评分标准:身份/模式 0.8+、偏好/案例 0.7、实体 0.65、普通事件 0.5`;
546
+
547
+ const SMART_EXTRACT_BATCH_PROMPT = `你是 AI 记忆分类助手。把每段对话分类到 6 种记忆类型之一。
548
+
549
+ 类型:profile(身份) | preferences(偏好) | entities(实体) | events(事件) | cases(问题方案) | patterns(模式规律)
550
+
551
+ 对每段输出 JSON 对象,最终输出一个 JSON 数组:
552
+ [{"category":"类别","l0":"一句话摘要","l1":"结构化概述","importance":0.7}, ...]
553
+
554
+ importance:身份/模式 0.8+、偏好/案例 0.7、实体 0.65、事件 0.5`;
555
+
556
+ // ============================================================================
557
+ // Helpers
558
+ // ============================================================================
559
+
560
+ function resolveEnvVars(value: string): string {
561
+ return value.replace(/\$\{([^}]+)\}/g, (_, envVar) => {
562
+ const envValue = process.env[envVar];
563
+ if (!envValue) {
564
+ throw new Error(`Environment variable ${envVar} is not set`);
565
+ }
566
+ return envValue;
567
+ });
568
+ }
569
+
570
+ /**
571
+ * Robust JSON parsing: handles markdown code blocks, unbalanced braces/brackets.
572
+ * Supports both objects and arrays.
573
+ */
574
+ function parseJSON<T>(text: string): T | null {
575
+ // Strip markdown code block wrapping
576
+ const cleaned = text.replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/, "");
577
+
578
+ // Try direct parse
579
+ try {
580
+ return JSON.parse(cleaned) as T;
581
+ } catch {
582
+ // Try extracting JSON object or array from balanced delimiters
583
+ // Check for array first (batch results)
584
+ const arrStart = cleaned.indexOf("[");
585
+ const arrEnd = cleaned.lastIndexOf("]");
586
+ if (arrStart >= 0 && arrEnd > arrStart) {
587
+ try {
588
+ return JSON.parse(cleaned.slice(arrStart, arrEnd + 1)) as T;
589
+ } catch { /* fall through to object attempt */ }
590
+ }
591
+
592
+ // Try object
593
+ const objStart = cleaned.indexOf("{");
594
+ const objEnd = cleaned.lastIndexOf("}");
595
+ if (objStart >= 0 && objEnd > objStart) {
596
+ try {
597
+ return JSON.parse(cleaned.slice(objStart, objEnd + 1)) as T;
598
+ } catch {
599
+ return null;
600
+ }
601
+ }
602
+ return null;
603
+ }
604
+ }
605
+
606
+ // ============================================================================
607
+ // Factory
608
+ // ============================================================================
609
+
610
+ /**
611
+ * Create an LLM client from config. Returns null if API key is not available.
612
+ */
613
+ export function createLLMClient(config?: Partial<LLMConfig>): LLMClient | null {
614
+ const merged = { ...DEFAULT_LLM_CONFIG, ...config };
615
+
616
+ // Check if API key is available
617
+ try {
618
+ const resolved = resolveEnvVars(merged.apiKey);
619
+ if (!resolved || resolved.startsWith("$")) return null;
620
+ } catch {
621
+ return null;
622
+ }
623
+
624
+ try {
625
+ return new LLMClient(merged);
626
+ } catch {
627
+ return null;
628
+ }
629
+ }