recallnest 1.4.1 → 2.2.0

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 (166) hide show
  1. package/.dockerignore +11 -0
  2. package/CLAUDE.md +7 -3
  3. package/Dockerfile +20 -0
  4. package/README.md +202 -130
  5. package/README_CN.md +263 -190
  6. package/ROADMAP.md +128 -81
  7. package/SKILL.md +250 -0
  8. package/assets/dashboard.png +0 -0
  9. package/assets/knowledge-graph.png +0 -0
  10. package/assets/screenshots/ui-full.png +0 -0
  11. package/assets/screenshots/ui-overview.png +0 -0
  12. package/assets/screenshots/ui-search.png +0 -0
  13. package/assets/ui/app.js +227 -3
  14. package/assets/ui/index.html +81 -11
  15. package/assets/ui/styles.css +178 -2
  16. package/bin/recallnest.mjs +62 -0
  17. package/bun.lock +337 -0
  18. package/daily-recall.sh +23 -0
  19. package/daily-reflections/2026-04-02.md +35 -0
  20. package/daily-reflections/2026-04-03.md +37 -0
  21. package/daily-reflections/2026-04-04.md +37 -0
  22. package/daily-reflections/2026-04-05.md +35 -0
  23. package/daily-reflections/2026-04-06.md +36 -0
  24. package/daily-reflections/2026-04-07.md +35 -0
  25. package/daily-reflections/2026-04-08.md +35 -0
  26. package/daily-reflections/2026-04-12.md +35 -0
  27. package/data/alias-map.example.json +22 -0
  28. package/docs/plans/2026-04-01-recall-boost.md +100 -0
  29. package/docs/superpowers/plans/2026-04-11-constructive-retrieval-emotion.md +1195 -0
  30. package/docs/superpowers/specs/2026-04-11-constructive-retrieval-emotion-design.md +398 -0
  31. package/glama.json +6 -0
  32. package/integrations/setup-all.sh +31 -0
  33. package/package.json +10 -3
  34. package/plan-philosophy-upgrades.md +363 -0
  35. package/plan-product-upgrades.md +155 -0
  36. package/plan.md +140 -0
  37. package/research-memory-evolution.md +440 -0
  38. package/research-rl-agent-memory.md +330 -0
  39. package/scripts/backfill-anchors.ts +130 -0
  40. package/scripts/diagnose-vector-search.ts +132 -0
  41. package/scripts/migrate-language.ts +75 -0
  42. package/scripts/verify-emotion-decay.ts +148 -0
  43. package/src/__tests__/activity-counter.test.ts +96 -0
  44. package/src/__tests__/admission-control.test.ts +206 -0
  45. package/src/__tests__/anchor-generator.test.ts +67 -0
  46. package/src/__tests__/babel-integration.test.ts +139 -0
  47. package/src/__tests__/capture-engine.test.ts +5 -3
  48. package/src/__tests__/cascade-forget.test.ts +1 -1
  49. package/src/__tests__/cc8-cc1-essential-context.test.ts +298 -0
  50. package/src/__tests__/circuit-breaker.test.ts +99 -0
  51. package/src/__tests__/cluster-consolidation.test.ts +712 -0
  52. package/src/__tests__/confidence-metadata.test.ts +125 -0
  53. package/src/__tests__/confidence-tracker.test.ts +53 -0
  54. package/src/__tests__/consolidation-engine.test.ts +1 -1
  55. package/src/__tests__/constructive-retrieval.test.ts +509 -0
  56. package/src/__tests__/context-collapse-renderer.test.ts +214 -0
  57. package/src/__tests__/context-reconstructor.test.ts +65 -0
  58. package/src/__tests__/conversation-importer.test.ts +692 -0
  59. package/src/__tests__/data-checkup.test.ts +225 -0
  60. package/src/__tests__/decay-exemption.test.ts +84 -0
  61. package/src/__tests__/dedup-guard.test.ts +39 -0
  62. package/src/__tests__/distill-lock.test.ts +176 -0
  63. package/src/__tests__/dream-pipeline.test.ts +232 -0
  64. package/src/__tests__/emotion-decay.test.ts +46 -0
  65. package/src/__tests__/emotion-detector.test.ts +56 -0
  66. package/src/__tests__/emotion-phase1-integration.test.ts +153 -0
  67. package/src/__tests__/emotion-retrieval.test.ts +56 -0
  68. package/src/__tests__/ethics-layer.test.ts +476 -0
  69. package/src/__tests__/feature-flags-integration.test.ts +54 -0
  70. package/src/__tests__/frequency-tracker.test.ts +113 -0
  71. package/src/__tests__/graph-export.test.ts +489 -0
  72. package/src/__tests__/interference-detector.test.ts +203 -0
  73. package/src/__tests__/mcp-efficiency.test.ts +288 -0
  74. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +243 -0
  75. package/src/__tests__/memory-evolution.test.ts +271 -0
  76. package/src/__tests__/memory-lint.test.ts +271 -0
  77. package/src/__tests__/narrative-architecture.test.ts +456 -0
  78. package/src/__tests__/pending-review.test.ts +77 -0
  79. package/src/__tests__/post-retrieval-filter.test.ts +178 -0
  80. package/src/__tests__/prediction-engine.test.ts +349 -0
  81. package/src/__tests__/predictive-prospective-memory.test.ts +416 -0
  82. package/src/__tests__/preference-slots.test.ts +99 -0
  83. package/src/__tests__/query-expander-alias.test.ts +89 -0
  84. package/src/__tests__/recall-governor.test.ts +227 -0
  85. package/src/__tests__/resume-context.test.ts +156 -1
  86. package/src/__tests__/salience-scoring.test.ts +106 -0
  87. package/src/__tests__/session-distiller.test.ts +611 -0
  88. package/src/__tests__/skill-engine.test.ts +546 -0
  89. package/src/__tests__/skill-promotion.test.ts +349 -0
  90. package/src/__tests__/supersede-chain.test.ts +154 -0
  91. package/src/__tests__/temporal-validity.test.ts +160 -0
  92. package/src/__tests__/topic-tag.test.ts +113 -0
  93. package/src/__tests__/trusted-memory.test.ts +828 -0
  94. package/src/__tests__/write-verifier.test.ts +122 -0
  95. package/src/access-tracker.ts +3 -2
  96. package/src/activity-counter.ts +109 -0
  97. package/src/adaptive-retrieval.ts +6 -5
  98. package/src/admission-control.ts +163 -0
  99. package/src/anchor-generator.ts +80 -0
  100. package/src/api-server.ts +10 -0
  101. package/src/audit-log.ts +122 -0
  102. package/src/auto-gc.ts +98 -29
  103. package/src/auto-recall.ts +33 -4
  104. package/src/capture-engine.ts +372 -23
  105. package/src/cascade-forget.ts +3 -2
  106. package/src/cli.ts +107 -5
  107. package/src/compat.ts +22 -0
  108. package/src/confidence-tracker.ts +210 -47
  109. package/src/conflict-store.ts +12 -3
  110. package/src/consolidation-engine.ts +296 -8
  111. package/src/context-collapse-renderer.ts +222 -0
  112. package/src/context-composer-stable-selection.ts +5 -1
  113. package/src/context-composer.ts +239 -4
  114. package/src/context-reconstructor.ts +404 -0
  115. package/src/conversation-importer.ts +441 -0
  116. package/src/data-checkup.ts +232 -0
  117. package/src/decay-engine.ts +84 -0
  118. package/src/distill-lock.ts +122 -0
  119. package/src/doctor.ts +8 -10
  120. package/src/dream-pipeline.ts +227 -0
  121. package/src/emotion-detector.ts +90 -0
  122. package/src/eval.ts +3 -2
  123. package/src/forget-engine.ts +269 -0
  124. package/src/frequency-tracker.ts +175 -0
  125. package/src/graph-export.ts +644 -0
  126. package/src/ingest.ts +36 -6
  127. package/src/interference-detector.ts +187 -0
  128. package/src/kg-extractor.ts +15 -24
  129. package/src/kg-store.ts +2 -1
  130. package/src/language-hook.ts +67 -0
  131. package/src/llm-client.ts +289 -6
  132. package/src/mcp-server.ts +707 -80
  133. package/src/memory-assets.ts +22 -5
  134. package/src/memory-evolution.ts +365 -0
  135. package/src/memory-lint.ts +429 -0
  136. package/src/memory-output.ts +69 -0
  137. package/src/memory-schema.ts +74 -2
  138. package/src/narrative-schema.ts +79 -0
  139. package/src/narrative-tagger.ts +164 -0
  140. package/src/pii-detector.ts +98 -0
  141. package/src/post-retrieval-filter.ts +76 -0
  142. package/src/prediction-engine.ts +253 -0
  143. package/src/preference-slots.ts +65 -2
  144. package/src/prospective-memory.ts +215 -0
  145. package/src/query-expander.ts +67 -0
  146. package/src/recall-governor.ts +183 -0
  147. package/src/retention-policy.ts +90 -0
  148. package/src/retriever.ts +828 -55
  149. package/src/rif.ts +34 -8
  150. package/src/runtime-config.ts +23 -8
  151. package/src/scope-inventory.ts +4 -3
  152. package/src/scope-policy.ts +4 -0
  153. package/src/session-distiller.ts +307 -0
  154. package/src/session-output.ts +40 -0
  155. package/src/session-schema.ts +37 -0
  156. package/src/session-store.ts +7 -2
  157. package/src/skill-engine.ts +208 -0
  158. package/src/skill-promotion.ts +293 -0
  159. package/src/skill-schema.ts +39 -0
  160. package/src/store.ts +96 -10
  161. package/src/topic-tag.ts +66 -0
  162. package/src/tracker.ts +3 -1
  163. package/src/ui-server.ts +80 -8
  164. package/src/workflow-observation-scope-review.ts +2 -1
  165. package/src/workflow-observation-store.ts +2 -1
  166. package/src/write-verifier.ts +126 -0
package/src/llm-client.ts CHANGED
@@ -14,6 +14,89 @@
14
14
  */
15
15
 
16
16
  import OpenAI from "openai";
17
+ import { logInfo } from "./stderr-log.js";
18
+ import { isEmotionScoringEnabled } from "./memory-schema.js";
19
+
20
+ // ============================================================================
21
+ // LME-9: Circuit Breaker — LLM 降级策略
22
+ // ============================================================================
23
+
24
+ type CircuitState = "closed" | "open" | "half-open";
25
+
26
+ export interface CircuitBreakerConfig {
27
+ /** Consecutive failures before opening the circuit (default: 3) */
28
+ failureThreshold: number;
29
+ /** Cooldown in ms before trying again (default: 30_000 = 30s) */
30
+ cooldownMs: number;
31
+ }
32
+
33
+ const DEFAULT_BREAKER_CONFIG: CircuitBreakerConfig = {
34
+ failureThreshold: 3,
35
+ cooldownMs: 30_000,
36
+ };
37
+
38
+ export class CircuitBreaker {
39
+ private state: CircuitState = "closed";
40
+ private consecutiveFailures = 0;
41
+ private openedAt = 0;
42
+ private readonly config: CircuitBreakerConfig;
43
+
44
+ constructor(config?: Partial<CircuitBreakerConfig>) {
45
+ this.config = { ...DEFAULT_BREAKER_CONFIG, ...config };
46
+ }
47
+
48
+ /** Check if a call should be allowed through. */
49
+ canAttempt(): boolean {
50
+ if (this.state === "closed") return true;
51
+ if (this.state === "open") {
52
+ // Check if cooldown has elapsed → transition to half-open
53
+ if (Date.now() - this.openedAt >= this.config.cooldownMs) {
54
+ this.state = "half-open";
55
+ logInfo("[circuit-breaker] transitioning to half-open, allowing probe");
56
+ return true;
57
+ }
58
+ return false;
59
+ }
60
+ // half-open: allow one probe
61
+ return true;
62
+ }
63
+
64
+ /** Record a successful call. */
65
+ recordSuccess(): void {
66
+ if (this.state !== "closed") {
67
+ logInfo("[circuit-breaker] probe succeeded, circuit closed");
68
+ }
69
+ this.consecutiveFailures = 0;
70
+ this.state = "closed";
71
+ }
72
+
73
+ /** Record a failed call. */
74
+ recordFailure(): void {
75
+ this.consecutiveFailures++;
76
+ if (this.state === "half-open") {
77
+ // Probe failed → reopen
78
+ this.state = "open";
79
+ this.openedAt = Date.now();
80
+ logInfo("[circuit-breaker] probe failed, circuit re-opened");
81
+ return;
82
+ }
83
+ if (this.consecutiveFailures >= this.config.failureThreshold) {
84
+ this.state = "open";
85
+ this.openedAt = Date.now();
86
+ logInfo(`[circuit-breaker] ${this.consecutiveFailures} consecutive failures, circuit opened`);
87
+ }
88
+ }
89
+
90
+ /** Current circuit state (for testing/diagnostics). */
91
+ getState(): CircuitState {
92
+ return this.state;
93
+ }
94
+
95
+ /** Current failure count (for testing/diagnostics). */
96
+ getFailureCount(): number {
97
+ return this.consecutiveFailures;
98
+ }
99
+ }
17
100
 
18
101
  // ============================================================================
19
102
  // Types
@@ -53,6 +136,12 @@ export interface SmartExtraction {
53
136
  l1: string;
54
137
  /** Importance score 0-1 (LLM's estimate) */
55
138
  importance: number;
139
+ /** Optional emotional tone (only populated when feature flag is on) */
140
+ emotion?: {
141
+ valence: number;
142
+ arousal: number;
143
+ label?: string;
144
+ };
56
145
  }
57
146
 
58
147
  /** Category-specific merge strategies */
@@ -96,8 +185,10 @@ export class LLMClient {
96
185
  private model: string;
97
186
  private temperature: number;
98
187
  private timeoutMs: number;
188
+ /** LME-9: Circuit breaker for graceful LLM degradation */
189
+ readonly breaker: CircuitBreaker;
99
190
 
100
- constructor(config: LLMConfig) {
191
+ constructor(config: LLMConfig, breakerConfig?: Partial<CircuitBreakerConfig>) {
101
192
  const apiKey = resolveEnvVars(config.apiKey);
102
193
  this.client = new OpenAI({
103
194
  apiKey,
@@ -106,6 +197,7 @@ export class LLMClient {
106
197
  this.model = config.model;
107
198
  this.temperature = config.temperature ?? 0.1;
108
199
  this.timeoutMs = config.timeoutMs ?? 15000;
200
+ this.breaker = new CircuitBreaker(breakerConfig);
109
201
  }
110
202
 
111
203
  /**
@@ -116,7 +208,9 @@ export class LLMClient {
116
208
  try {
117
209
  const response = await this.chat(
118
210
  "你是记忆索引助手。给以下对话片段写一句话摘要(不超过80字)," +
119
- "用于快速检索。只输出摘要本身,不加任何前缀。",
211
+ "用于快速检索。只输出摘要本身,不加任何前缀。\n" +
212
+ "保真规则:端口号/IP/URL/文件路径/API名称 → 原样保留;" +
213
+ "函数名/事件名/配置项 → 逐项保留不概括。",
120
214
  text.slice(0, 2000), // Cap input to avoid token overflow
121
215
  );
122
216
  if (!response || response.length < 5) return null;
@@ -144,7 +238,7 @@ export class LLMClient {
144
238
  const validCategories: SmartCategory[] = ["profile", "preferences", "entities", "events", "cases", "patterns"];
145
239
  if (!validCategories.includes(parsed.category)) return null;
146
240
 
147
- return {
241
+ const result: SmartExtraction = {
148
242
  category: parsed.category,
149
243
  l0: (parsed.l0 || "").slice(0, 150),
150
244
  l1: (parsed.l1 || "").slice(0, 500),
@@ -152,6 +246,16 @@ export class LLMClient {
152
246
  ? Math.max(0, Math.min(1, parsed.importance))
153
247
  : CATEGORY_DEFAULT_IMPORTANCE[parsed.category],
154
248
  };
249
+
250
+ if (isEmotionScoringEnabled() && parsed.emotion) {
251
+ result.emotion = {
252
+ valence: Math.max(-1, Math.min(1, parsed.emotion.valence ?? 0)),
253
+ arousal: Math.max(0, Math.min(1, parsed.emotion.arousal ?? 0)),
254
+ label: parsed.emotion.label,
255
+ };
256
+ }
257
+
258
+ return result;
155
259
  } catch {
156
260
  return null;
157
261
  }
@@ -255,6 +359,87 @@ export class LLMClient {
255
359
  return results;
256
360
  }
257
361
 
362
+ /**
363
+ * B-3: Assess the long-term importance of a memory (0–1).
364
+ * Called when store_memory importance is at the default value, to let LLM refine it.
365
+ * Returns null on failure (caller should keep the default).
366
+ */
367
+ async assessImportance(text: string, category: string): Promise<number | null> {
368
+ try {
369
+ const response = await this.chat(
370
+ "你是记忆重要性评估助手。评估以下记忆的长期重要性(0.0-1.0)。\n\n" +
371
+ "锚定标准(每级附具体示例,严格对标):\n" +
372
+ "- 0.85-1.0:用户明确纠正行为(如'不要这样做')、身份声明(职业/角色)、" +
373
+ "情感/行为变化、核心偏好(写作风格/沟通方式)、反复强调的规则\n" +
374
+ "- 0.70-0.84:关键决策(架构选型/技术路线)、项目承诺(deadline/里程碑)、" +
375
+ "可复用案例(踩坑→修复→教训)、持久偏好(喜欢X不喜欢Y)\n" +
376
+ "- 0.55-0.69:实体信息(项目名/工具名/人名)、新学到的背景知识、" +
377
+ "配置参数(API key位置/环境变量)\n" +
378
+ "- 0.40-0.54:普通事件(今天部署了X)、一次性操作步骤、" +
379
+ "已解决的 bug 细节(修完即过时)\n" +
380
+ "- 0.10-0.30:日常寒暄、重复信息(已有更完整版本)、" +
381
+ "临时调试输出、短期上下文(如'刚才那个文件')\n\n" +
382
+ "常见误判提醒:\n" +
383
+ "- 用户说'记住'/'永远不要' → 至少 0.85\n" +
384
+ "- 看起来是小事但反复出现 → 至少 0.70(重复=重要)\n" +
385
+ "- 纯技术细节但没有复用价值 → 不超过 0.50\n\n" +
386
+ "只输出一行 JSON:{\"importance\":0.7,\"reason\":\"简短原因\"}",
387
+ `[类别] ${category}\n[内容] ${text.slice(0, 1000)}`,
388
+ );
389
+ if (!response) return null;
390
+ const parsed = parseJSON<{ importance: number; reason?: string }>(response);
391
+ if (parsed && typeof parsed.importance === "number") {
392
+ return Math.max(0, Math.min(1, parsed.importance));
393
+ }
394
+ return null;
395
+ } catch {
396
+ return null;
397
+ }
398
+ }
399
+
400
+ /**
401
+ * HP-4: Batch re-assess importance for existing memories during distill.
402
+ * Returns a map of id → new importance (only for entries that changed).
403
+ */
404
+ async reassessImportanceBatch(
405
+ entries: Array<{ id: string; text: string; category: string; currentImportance: number }>,
406
+ ): Promise<Map<string, number>> {
407
+ const results = new Map<string, number>();
408
+ if (entries.length === 0) return results;
409
+
410
+ const batch = entries
411
+ .map((e, i) => `[${i}] category=${e.category} importance=${e.currentImportance}\n${e.text.slice(0, 300)}`)
412
+ .join("\n---\n");
413
+
414
+ try {
415
+ const response = await this.chat(
416
+ "批量重评记忆重要性。使用同一套锚定标准(0.85+ 身份/纠正/核心偏好," +
417
+ "0.70-0.84 决策/案例/偏好,0.55-0.69 实体/背景,0.40-0.54 事件/操作," +
418
+ "0.10-0.30 寒暄/重复/临时)。\n" +
419
+ "只输出 JSON 数组,每项 {\"idx\":0,\"importance\":0.7}," +
420
+ "只包含需要调整的项(与当前分数差 ≥ 0.1 才输出)。",
421
+ batch,
422
+ );
423
+ if (!response) return results;
424
+ const parsed = parseJSON<Array<{ idx: number; importance: number }>>(response);
425
+ if (Array.isArray(parsed)) {
426
+ for (const item of parsed) {
427
+ if (
428
+ typeof item.idx === "number" &&
429
+ typeof item.importance === "number" &&
430
+ item.idx >= 0 &&
431
+ item.idx < entries.length
432
+ ) {
433
+ results.set(entries[item.idx].id, Math.max(0, Math.min(1, item.importance)));
434
+ }
435
+ }
436
+ }
437
+ } catch {
438
+ // Batch re-assessment failure is non-fatal
439
+ }
440
+ return results;
441
+ }
442
+
258
443
  /**
259
444
  * Semantic dedup decision: given a new chunk and an existing similar chunk,
260
445
  * decide whether to CREATE (store new), MERGE (combine), or SKIP (discard).
@@ -363,7 +548,10 @@ export class LLMClient {
363
548
  try {
364
549
  return await this.chat(
365
550
  "你是记忆精炼助手。把以下内容提炼为一段核心摘要(不超过200字)。\n" +
366
- "要求:保留关键事实、人名、数字、时间等;去除冗余修辞;直接输出摘要,不加前缀。",
551
+ "要求:保留关键事实、人名、数字、时间等;去除冗余修辞;直接输出摘要,不加前缀。\n" +
552
+ "保真规则:端口号/IP/URL/文件路径 → 原样保留;" +
553
+ "命名列表(函数名/事件名/配置项)→ 逐项保留不概括;" +
554
+ "规则表/状态转移 → 完整保留;对比/排序 → 保留顺序和理由。",
367
555
  text.slice(0, 2000),
368
556
  );
369
557
  } catch {
@@ -454,6 +642,33 @@ export class LLMClient {
454
642
  }
455
643
  }
456
644
 
645
+ /**
646
+ * HP-5: Extract cross-memory pattern from a cluster of related memories.
647
+ * Asks LLM whether the cluster reveals an implicit recurring theme,
648
+ * preference, or behavior not stated in any single memory.
649
+ *
650
+ * @returns Pattern text if found, null if no pattern or LLM failure.
651
+ */
652
+ async extractPattern(clusterTexts: string[]): Promise<string | null> {
653
+ if (clusterTexts.length < 3) return null;
654
+
655
+ const numbered = clusterTexts.map((t, i) => `[${i + 1}] ${t}`).join("\n");
656
+ const result = await this.chatJson<{ hasPattern: boolean; pattern?: string }>(
657
+ "你是记忆模式发现助手。分析以下一组相关记忆,判断它们是否暗示了某个跨条目的隐含模式——" +
658
+ "例如反复出现的偏好、行为倾向、价值观或循环主题。\n\n" +
659
+ "规则:\n" +
660
+ "- 只发现真正跨条目的模式,而非单条记忆已经明确表达的内容\n" +
661
+ "- 模式必须有至少 2 条记忆作为证据支撑\n" +
662
+ "- 如果没有发现有意义的模式,返回 hasPattern: false\n" +
663
+ "- 模式描述不超过 150 字,简洁且可操作\n" +
664
+ '- 输出 JSON:{"hasPattern": true, "pattern": "描述..."} 或 {"hasPattern": false}',
665
+ `记忆条目(共${clusterTexts.length}条):\n${numbered}`,
666
+ );
667
+
668
+ if (!result || !result.hasPattern || !result.pattern) return null;
669
+ return result.pattern;
670
+ }
671
+
457
672
  /**
458
673
  * Chat + JSON parse helper for structured extraction.
459
674
  * Returns null on failure (LLM error or invalid JSON).
@@ -481,11 +696,63 @@ export class LLMClient {
481
696
  }
482
697
  }
483
698
 
699
+ async chatLong(system: string, user: string, maxTokens = 2000): Promise<string | null> {
700
+ if (!this.breaker.canAttempt()) {
701
+ return null;
702
+ }
703
+
704
+ const controller = new AbortController();
705
+ const timeout = setTimeout(() => controller.abort(), this.timeoutMs * 2);
706
+
707
+ try {
708
+ const response = await this.client.chat.completions.create(
709
+ {
710
+ model: this.model,
711
+ messages: [
712
+ { role: "system", content: system },
713
+ { role: "user", content: user },
714
+ ],
715
+ temperature: this.temperature,
716
+ max_tokens: maxTokens,
717
+ },
718
+ { signal: controller.signal },
719
+ );
720
+
721
+ const result = response.choices[0]?.message?.content?.trim() || null;
722
+ this.breaker.recordSuccess();
723
+ return result;
724
+ } catch (err) {
725
+ this.breaker.recordFailure();
726
+ throw err;
727
+ } finally {
728
+ clearTimeout(timeout);
729
+ }
730
+ }
731
+
732
+ // --------------------------------------------------------------------------
733
+ // Constructive Retrieval — public wrappers for reconstruction pipeline
734
+ // --------------------------------------------------------------------------
735
+
736
+ /** Public wrapper for reconstruction pipeline */
737
+ async generateReconstruction(system: string, user: string): Promise<string | null> {
738
+ return this.chat(system, user);
739
+ }
740
+
741
+ /** Check if LLM circuit breaker allows requests */
742
+ isAvailable(): boolean {
743
+ return this.breaker.canAttempt();
744
+ }
745
+
484
746
  // --------------------------------------------------------------------------
485
747
  // Internals
486
748
  // --------------------------------------------------------------------------
487
749
 
488
750
  private async chat(system: string, user: string): Promise<string | null> {
751
+ // LME-9: Circuit breaker — short-circuit when LLM is down
752
+ if (!this.breaker.canAttempt()) {
753
+ return null;
754
+ }
755
+
489
756
  const controller = new AbortController();
490
757
  const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
491
758
 
@@ -503,7 +770,12 @@ export class LLMClient {
503
770
  { signal: controller.signal },
504
771
  );
505
772
 
506
- return response.choices[0]?.message?.content?.trim() || null;
773
+ const result = response.choices[0]?.message?.content?.trim() || null;
774
+ this.breaker.recordSuccess();
775
+ return result;
776
+ } catch (err) {
777
+ this.breaker.recordFailure();
778
+ throw err;
507
779
  } finally {
508
780
  clearTimeout(timeout);
509
781
  }
@@ -538,11 +810,22 @@ const SMART_EXTRACT_SYSTEM_PROMPT = `你是 AI 记忆分类助手。把对话片
538
810
  | 报错→修复、问题→方案 | cases |
539
811
  | "每次…"、"一般…"、规律总结 | patterns |
540
812
 
813
+ ## 保真规则(l0/l1 必须遵守)
814
+ - 端口号/IP/URL/文件路径/API endpoint → 原样保留
815
+ - 命名列表(函数名/事件名/配置项)→ 逐项保留,不概括为"等"
816
+ - 规则表/状态转移 → 完整保留
817
+ - 对比/排序 → 保留顺序和理由
818
+
541
819
  ## 输出格式
542
820
  只输出一个 JSON 对象:
543
821
  {"category":"类别","l0":"一句话摘要(≤80字)","l1":"结构化概述(2-5行markdown)","importance":0.7}
544
822
 
545
- importance 评分标准:身份/模式 0.8+、偏好/案例 0.7、实体 0.65、普通事件 0.5`;
823
+ importance 评分标准:身份/模式 0.8+、偏好/案例 0.7、实体 0.65、普通事件 0.5
824
+
825
+ Also rate emotional tone:
826
+ - emotion.valence: [-1, 1] (negative to positive)
827
+ - emotion.arousal: [0, 1] (calm to excited)
828
+ - emotion.label: one word (e.g., "frustration", "neutral")`;
546
829
 
547
830
  const SMART_EXTRACT_BATCH_PROMPT = `你是 AI 记忆分类助手。把每段对话分类到 6 种记忆类型之一。
548
831