recallnest 1.4.1 → 2.2.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 (233) hide show
  1. package/.env +4 -0
  2. package/CLAUDE.md +7 -3
  3. package/README.md +202 -130
  4. package/README_CN.md +263 -190
  5. package/bin/recallnest.mjs +62 -0
  6. package/config.json +55 -0
  7. package/daily-recall.sh +23 -0
  8. package/integrations/setup-all.sh +31 -0
  9. package/logs/daily-recall.error.log +0 -0
  10. package/logs/daily-recall.log +30 -0
  11. package/logs/ingest-2026-04-11.log +110 -0
  12. package/logs/ingest-2026-04-12.log +90 -0
  13. package/logs/launchagent-stderr.log +0 -0
  14. package/logs/launchagent-stdout.log +0 -0
  15. package/package.json +10 -3
  16. package/src/access-tracker.ts +3 -2
  17. package/src/activity-counter.ts +109 -0
  18. package/src/adaptive-retrieval.ts +6 -5
  19. package/src/admission-control.ts +163 -0
  20. package/src/anchor-generator.ts +80 -0
  21. package/src/api-server.ts +10 -0
  22. package/src/audit-log.ts +122 -0
  23. package/src/auto-gc.ts +98 -29
  24. package/src/auto-recall.ts +33 -4
  25. package/src/capture-engine.ts +372 -23
  26. package/src/cascade-forget.ts +3 -2
  27. package/src/cli.ts +107 -5
  28. package/src/compat.ts +22 -0
  29. package/src/confidence-tracker.ts +210 -47
  30. package/src/conflict-store.ts +12 -3
  31. package/src/consolidation-engine.ts +296 -8
  32. package/src/context-collapse-renderer.ts +222 -0
  33. package/src/context-composer-stable-selection.ts +5 -1
  34. package/src/context-composer.ts +239 -4
  35. package/src/context-reconstructor.ts +404 -0
  36. package/src/conversation-importer.ts +441 -0
  37. package/src/data-checkup.ts +232 -0
  38. package/src/decay-engine.ts +84 -0
  39. package/src/distill-lock.ts +122 -0
  40. package/src/doctor.ts +8 -10
  41. package/src/dream-pipeline.ts +227 -0
  42. package/src/emotion-detector.ts +90 -0
  43. package/src/eval.ts +3 -2
  44. package/src/forget-engine.ts +269 -0
  45. package/src/frequency-tracker.ts +175 -0
  46. package/src/graph-export.ts +644 -0
  47. package/src/ingest.ts +36 -6
  48. package/src/interference-detector.ts +187 -0
  49. package/src/kg-extractor.ts +15 -24
  50. package/src/kg-store.ts +2 -1
  51. package/src/language-hook.ts +67 -0
  52. package/src/llm-client.ts +289 -6
  53. package/src/mcp-server.ts +707 -80
  54. package/src/memory-assets.ts +22 -5
  55. package/src/memory-evolution.ts +365 -0
  56. package/src/memory-lint.ts +429 -0
  57. package/src/memory-output.ts +69 -0
  58. package/src/memory-schema.ts +74 -2
  59. package/src/narrative-schema.ts +79 -0
  60. package/src/narrative-tagger.ts +164 -0
  61. package/src/pii-detector.ts +98 -0
  62. package/src/post-retrieval-filter.ts +76 -0
  63. package/src/prediction-engine.ts +253 -0
  64. package/src/preference-slots.ts +65 -2
  65. package/src/prospective-memory.ts +215 -0
  66. package/src/query-expander.ts +67 -0
  67. package/src/recall-governor.ts +183 -0
  68. package/src/retention-policy.ts +90 -0
  69. package/src/retriever.ts +828 -55
  70. package/src/rif.ts +34 -8
  71. package/src/runtime-config.ts +23 -8
  72. package/src/scope-inventory.ts +4 -3
  73. package/src/scope-policy.ts +4 -0
  74. package/src/session-distiller.ts +307 -0
  75. package/src/session-output.ts +40 -0
  76. package/src/session-schema.ts +37 -0
  77. package/src/session-store.ts +7 -2
  78. package/src/skill-engine.ts +208 -0
  79. package/src/skill-promotion.ts +293 -0
  80. package/src/skill-schema.ts +39 -0
  81. package/src/store.ts +96 -10
  82. package/src/topic-tag.ts +66 -0
  83. package/src/tracker.ts +3 -1
  84. package/src/ui-server.ts +80 -8
  85. package/src/workflow-observation-scope-review.ts +2 -1
  86. package/src/workflow-observation-store.ts +2 -1
  87. package/src/write-verifier.ts +126 -0
  88. package/.claude-plugin/CLAUDE.md +0 -29
  89. package/.claude-plugin/marketplace.json +0 -18
  90. package/.github/workflows/ci.yml +0 -28
  91. package/.mcp.json +0 -9
  92. package/CHANGELOG.md +0 -51
  93. package/DEMO.md +0 -94
  94. package/EVAL.md +0 -89
  95. package/FAILURES.md +0 -75
  96. package/OPERATIONS.md +0 -59
  97. package/ROADMAP.md +0 -198
  98. package/assets/recallnest-workbench.png +0 -0
  99. package/assets/ui/app.js +0 -807
  100. package/assets/ui/index.html +0 -154
  101. package/assets/ui/styles.css +0 -605
  102. package/assets/wechat-qr.jpg +0 -0
  103. package/daily-reflections/2026-03-05.md +0 -15
  104. package/daily-reflections/2026-03-06.md +0 -15
  105. package/daily-reflections/2026-03-07.md +0 -7
  106. package/daily-reflections/2026-03-08.md +0 -34
  107. package/daily-reflections/2026-03-09.md +0 -35
  108. package/daily-reflections/2026-03-10.md +0 -36
  109. package/daily-reflections/2026-03-11.md +0 -35
  110. package/daily-reflections/2026-03-12.md +0 -36
  111. package/daily-reflections/2026-03-13.md +0 -35
  112. package/daily-reflections/2026-03-14.md +0 -35
  113. package/daily-reflections/2026-03-15.md +0 -35
  114. package/daily-reflections/2026-03-16.md +0 -41
  115. package/daily-reflections/2026-03-17.md +0 -10
  116. package/daily-reflections/2026-03-18.md +0 -10
  117. package/daily-reflections/2026-03-19.md +0 -10
  118. package/daily-reflections/2026-03-20.md +0 -10
  119. package/daily-reflections/2026-03-21.md +0 -10
  120. package/daily-reflections/2026-03-22.md +0 -10
  121. package/daily-reflections/2026-03-23.md +0 -10
  122. package/daily-reflections/2026-03-24.md +0 -10
  123. package/daily-reflections/2026-03-25.md +0 -32
  124. package/daily-reflections/2026-03-26.md +0 -37
  125. package/daily-reflections/2026-03-27.md +0 -35
  126. package/daily-reflections/2026-03-28.md +0 -35
  127. package/daily-reflections/2026-03-29.md +0 -35
  128. package/daily-reflections/2026-03-30.md +0 -36
  129. package/daily-reflections/2026-03-31.md +0 -35
  130. package/daily-reflections/2026-04-01.md +0 -35
  131. package/docs/api-reference.md +0 -1291
  132. package/docs/architecture.md +0 -168
  133. package/docs/memory-boundary-contract.md +0 -226
  134. package/docs/memory-categories.md +0 -119
  135. package/docs/pipeline-audit-ultramemory.md +0 -177
  136. package/docs/plans/2026-03-08-query-expansion.md +0 -270
  137. package/docs/plans/2026-03-16-continuity-layer.md +0 -196
  138. package/docs/plans/mcp-tiering-plan.md +0 -126
  139. package/docs/research/recall-quality-audit-2026-03-22.md +0 -153
  140. package/docs/self-evolution.md +0 -198
  141. package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +0 -782
  142. package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +0 -77
  143. package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +0 -95
  144. package/eval/continuity/results-2026-03-16.md +0 -117
  145. package/eval/continuity/results-2026-03-19.md +0 -453
  146. package/eval/continuity/runbook.md +0 -110
  147. package/eval/continuity/seed-cases.md +0 -54
  148. package/eval/reports/2026-03-06-abstract-query-baseline.md +0 -96
  149. package/eval/reports/2026-03-06-aws-ssh-baseline.md +0 -82
  150. package/eval/reports/2026-03-06-baseline.md +0 -82
  151. package/eval/reports/query-expansion.md +0 -138
  152. package/lm +0 -19
  153. package/plan-v1.2-distributable.md +0 -105
  154. package/scripts/archive-distilled-junk.ts +0 -116
  155. package/scripts/backfill-l0l1l2.ts +0 -98
  156. package/scripts/blind-scan.ts +0 -588
  157. package/scripts/cleanup-step1-exact-dups.ts +0 -117
  158. package/scripts/cleanup-step2-template-dups.ts +0 -113
  159. package/scripts/cleanup-step3-stale-content.ts +0 -149
  160. package/scripts/cleanup-step4-orphan-scopes.ts +0 -131
  161. package/scripts/delete-distilled-junk.ts +0 -88
  162. package/scripts/distill-facts.ts +0 -305
  163. package/scripts/health-check.ts +0 -343
  164. package/scripts/incremental-ingest.sh +0 -71
  165. package/scripts/mlp-health-check.ts +0 -382
  166. package/scripts/pull-claude-desktop.ts +0 -515
  167. package/scripts/re-ingest-cc.sh +0 -66
  168. package/scripts/rebalance-memory-health.ts +0 -181
  169. package/scripts/run-desktop-ingest.sh +0 -11
  170. package/scripts/start-server.sh +0 -12
  171. package/scripts/viz-dashboard.html +0 -423
  172. package/scripts/viz-export.ts +0 -703
  173. package/src/__tests__/access-tracker-novelty.test.ts +0 -228
  174. package/src/__tests__/auto-consolidation.test.ts +0 -168
  175. package/src/__tests__/auto-recall.test.ts +0 -131
  176. package/src/__tests__/batch-dedup.test.ts +0 -65
  177. package/src/__tests__/capture-engine.test.ts +0 -1142
  178. package/src/__tests__/capture-heuristic.test.ts +0 -171
  179. package/src/__tests__/cascade-forget.test.ts +0 -210
  180. package/src/__tests__/category-min-scores.test.ts +0 -33
  181. package/src/__tests__/confidence-tracker.test.ts +0 -196
  182. package/src/__tests__/conflict-advisor.test.ts +0 -206
  183. package/src/__tests__/conflict-escalation.test.ts +0 -149
  184. package/src/__tests__/conflict-lifecycle.test.ts +0 -83
  185. package/src/__tests__/conflict-output.test.ts +0 -85
  186. package/src/__tests__/conflict-store.test.ts +0 -374
  187. package/src/__tests__/consolidation-engine.test.ts +0 -181
  188. package/src/__tests__/context-composer-pins.test.ts +0 -64
  189. package/src/__tests__/context-composer-task-selection.test.ts +0 -24
  190. package/src/__tests__/context-renderer.test.ts +0 -91
  191. package/src/__tests__/continuity-eval.test.ts +0 -275
  192. package/src/__tests__/continuity-schema.test.ts +0 -97
  193. package/src/__tests__/core-recall-eval.test.ts +0 -235
  194. package/src/__tests__/core-summary.test.ts +0 -109
  195. package/src/__tests__/dedup-actions.test.ts +0 -58
  196. package/src/__tests__/deterministic-id.test.ts +0 -39
  197. package/src/__tests__/doctor.test.ts +0 -303
  198. package/src/__tests__/drill-down.test.ts +0 -108
  199. package/src/__tests__/embedder.test.ts +0 -122
  200. package/src/__tests__/entity-resolver.test.ts +0 -58
  201. package/src/__tests__/event-segmenter.test.ts +0 -153
  202. package/src/__tests__/hotness-blend.test.ts +0 -76
  203. package/src/__tests__/ingest.test.ts +0 -360
  204. package/src/__tests__/kg-extractor.test.ts +0 -184
  205. package/src/__tests__/kg-store.test.ts +0 -198
  206. package/src/__tests__/llm-consolidation.test.ts +0 -226
  207. package/src/__tests__/memory-boundaries.test.ts +0 -237
  208. package/src/__tests__/memory-health-rebalance.test.ts +0 -144
  209. package/src/__tests__/memory-output.test.ts +0 -215
  210. package/src/__tests__/multi-vector.test.ts +0 -242
  211. package/src/__tests__/ppr-traversal.test.ts +0 -256
  212. package/src/__tests__/preference-guard-smoke.test.ts +0 -211
  213. package/src/__tests__/preference-matcher.test.ts +0 -230
  214. package/src/__tests__/preference-slots.test.ts +0 -106
  215. package/src/__tests__/prospective-memory.test.ts +0 -283
  216. package/src/__tests__/query-entity-detector.test.ts +0 -171
  217. package/src/__tests__/query-expander.test.ts +0 -34
  218. package/src/__tests__/recall-mode.test.ts +0 -64
  219. package/src/__tests__/result-synthesizer.test.ts +0 -198
  220. package/src/__tests__/resume-context.test.ts +0 -4743
  221. package/src/__tests__/retrieval-trace.test.ts +0 -213
  222. package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
  223. package/src/__tests__/retroactive-boost.test.ts +0 -223
  224. package/src/__tests__/scope-inventory.test.ts +0 -200
  225. package/src/__tests__/scope-policy.test.ts +0 -69
  226. package/src/__tests__/session-checkpoint.test.ts +0 -259
  227. package/src/__tests__/session-gc.test.ts +0 -215
  228. package/src/__tests__/store-scope-audit.test.ts +0 -81
  229. package/src/__tests__/temporal-parser.test.ts +0 -241
  230. package/src/__tests__/term-registry.test.ts +0 -121
  231. package/src/__tests__/tool-output-compressor.test.ts +0 -133
  232. package/src/__tests__/version-manager.test.ts +0 -213
  233. package/src/__tests__/workflow-observation.test.ts +0 -400
@@ -1,360 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
-
3
- import { dedupCheck } from "../ingest.js";
4
-
5
- function buildSearchResult(text: string, score: number) {
6
- return {
7
- score,
8
- entry: {
9
- id: `memory-${score}`,
10
- text,
11
- vector: [score],
12
- category: "events",
13
- scope: "cc:test-session",
14
- importance: 0.6,
15
- timestamp: 1_700_000_000_000,
16
- metadata: "{}",
17
- },
18
- };
19
- }
20
-
21
- describe("dedupCheck", () => {
22
- it("stores a new same-brand item preference instead of treating it as a duplicate topic", async () => {
23
- let llmCalls = 0;
24
- const store = {
25
- async vectorSearch() {
26
- return [
27
- buildSearchResult("喜欢吃麦当劳的麦旋风", 0.92),
28
- buildSearchResult("喜欢吃麦当劳的板烧鸡腿堡", 0.89),
29
- ];
30
- },
31
- };
32
- const llm = {
33
- async dedupDecision() {
34
- llmCalls += 1;
35
- return { action: "SKIP" as const, reason: "same topic" };
36
- },
37
- };
38
-
39
- const result = await dedupCheck(
40
- store as any,
41
- [1, 0, 0],
42
- "我喜欢吃麦当劳的麦辣鸡翅",
43
- llm as any,
44
- );
45
-
46
- expect(result.action).toBe("store");
47
- expect(result.reason).toBe("unique");
48
- expect(result.existingText).toBe("喜欢吃麦当劳的板烧鸡腿堡");
49
- expect(llmCalls).toBe(0);
50
- });
51
-
52
- it("stores a new atomic preference even when the closest match is an aggregate summary", async () => {
53
- const store = {
54
- async vectorSearch() {
55
- return [
56
- buildSearchResult("喜欢吃麦当劳的麦旋风、板烧鸡腿堡和藤椒鸡派", 0.94),
57
- ];
58
- },
59
- };
60
-
61
- const result = await dedupCheck(
62
- store as any,
63
- [1, 0, 0],
64
- "我喜欢吃麦当劳的麦辣鸡翅",
65
- );
66
-
67
- expect(result.action).toBe("store");
68
- expect(result.reason).toBe("unique");
69
- expect(result.existingText).toContain("麦旋风");
70
- });
71
-
72
- it("still skips when an exact atomic preference already exists among the candidates", async () => {
73
- const store = {
74
- async vectorSearch() {
75
- return [
76
- buildSearchResult("喜欢吃麦当劳的麦旋风、板烧鸡腿堡和藤椒鸡派", 0.94),
77
- buildSearchResult("我喜欢吃麦当劳的麦辣鸡翅", 0.82),
78
- ];
79
- },
80
- };
81
-
82
- const result = await dedupCheck(
83
- store as any,
84
- [1, 0, 0],
85
- "我喜欢吃麦当劳的麦辣鸡翅",
86
- );
87
-
88
- expect(result.action).toBe("skip");
89
- expect(result.reason).toBe("exact");
90
- expect(result.existingText).toBe("我喜欢吃麦当劳的麦辣鸡翅");
91
- });
92
-
93
- it("does not force-create when the same brand-item preference is only rephrased", async () => {
94
- let llmCalls = 0;
95
- const store = {
96
- async vectorSearch() {
97
- return [
98
- buildSearchResult("我喜欢吃麦当劳的麦辣鸡翅", 0.75),
99
- ];
100
- },
101
- };
102
- const llm = {
103
- async dedupDecision() {
104
- llmCalls += 1;
105
- return { action: "SKIP" as const, reason: "same item, rephrased" };
106
- },
107
- };
108
-
109
- const result = await dedupCheck(
110
- store as any,
111
- [1, 0, 0],
112
- "用户喜欢麦当劳的麦辣鸡翅",
113
- llm as any,
114
- );
115
-
116
- expect(result.action).toBe("skip");
117
- expect(result.reason).toBe("llm-skip");
118
- expect(result.existingText).toBe("我喜欢吃麦当劳的麦辣鸡翅");
119
- expect(llmCalls).toBe(1);
120
- });
121
-
122
- it("does not trigger the brand-item guard for non-preference text that mentions the same brand and item", async () => {
123
- let llmCalls = 0;
124
- const store = {
125
- async vectorSearch() {
126
- return [
127
- buildSearchResult("我喜欢喝星巴克的抹茶拿铁", 0.75),
128
- ];
129
- },
130
- };
131
- const llm = {
132
- async dedupDecision() {
133
- llmCalls += 1;
134
- return { action: "SKIP" as const, reason: "same context, not a new preference" };
135
- },
136
- };
137
-
138
- const result = await dedupCheck(
139
- store as any,
140
- [1, 0, 0],
141
- "我们刚讨论过星巴克的抹茶拿铁做法",
142
- llm as any,
143
- );
144
-
145
- expect(result.action).toBe("skip");
146
- expect(result.reason).toBe("llm-skip");
147
- expect(result.existingText).toBe("我喜欢喝星巴克的抹茶拿铁");
148
- expect(llmCalls).toBe(1);
149
- });
150
-
151
- it("stores a new reply-style preference instead of collapsing different style traits into one topic", async () => {
152
- let llmCalls = 0;
153
- const store = {
154
- async vectorSearch() {
155
- return [
156
- buildSearchResult("User prefers concise, direct replies.", 0.91),
157
- ];
158
- },
159
- };
160
- const llm = {
161
- async dedupDecision() {
162
- llmCalls += 1;
163
- return { action: "SKIP" as const, reason: "same topic" };
164
- },
165
- };
166
-
167
- const result = await dedupCheck(
168
- store as any,
169
- [1, 0, 0],
170
- "User prefers colloquial, grounded replies.",
171
- llm as any,
172
- );
173
-
174
- expect(result.action).toBe("store");
175
- expect(result.reason).toBe("unique");
176
- expect(result.existingText).toBe("User prefers concise, direct replies.");
177
- expect(llmCalls).toBe(0);
178
- });
179
-
180
- it("does not trigger the reply-style guard for descriptive text about a draft", async () => {
181
- let llmCalls = 0;
182
- const store = {
183
- async vectorSearch() {
184
- return [
185
- buildSearchResult("User prefers colloquial, grounded replies.", 0.75),
186
- ];
187
- },
188
- };
189
- const llm = {
190
- async dedupDecision() {
191
- llmCalls += 1;
192
- return { action: "SKIP" as const, reason: "draft note, not a user preference" };
193
- },
194
- };
195
-
196
- const result = await dedupCheck(
197
- store as any,
198
- [1, 0, 0],
199
- "这段文案简洁直接,先别改。",
200
- llm as any,
201
- );
202
-
203
- expect(result.action).toBe("skip");
204
- expect(result.reason).toBe("llm-skip");
205
- expect(result.existingText).toBe("User prefers colloquial, grounded replies.");
206
- expect(llmCalls).toBe(1);
207
- });
208
-
209
- it("stores a new tool-choice preference instead of collapsing different tool choices into one topic", async () => {
210
- let llmCalls = 0;
211
- const store = {
212
- async vectorSearch() {
213
- return [
214
- buildSearchResult("Uses Bun over Node.", 0.91),
215
- ];
216
- },
217
- };
218
- const llm = {
219
- async dedupDecision() {
220
- llmCalls += 1;
221
- return { action: "SKIP" as const, reason: "same topic" };
222
- },
223
- };
224
-
225
- const result = await dedupCheck(
226
- store as any,
227
- [1, 0, 0],
228
- "Prefers rg over grep.",
229
- llm as any,
230
- );
231
-
232
- expect(result.action).toBe("store");
233
- expect(result.reason).toBe("unique");
234
- expect(result.existingText).toBe("Uses Bun over Node.");
235
- expect(llmCalls).toBe(0);
236
- });
237
-
238
- it("does not trigger the tool-choice guard for narrative migration text", async () => {
239
- let llmCalls = 0;
240
- const store = {
241
- async vectorSearch() {
242
- return [
243
- buildSearchResult("Prefers rg over grep.", 0.75),
244
- ];
245
- },
246
- };
247
- const llm = {
248
- async dedupDecision() {
249
- llmCalls += 1;
250
- return { action: "SKIP" as const, reason: "migration note, not a tool preference" };
251
- },
252
- };
253
-
254
- const result = await dedupCheck(
255
- store as any,
256
- [1, 0, 0],
257
- "文档里写了 uses Bun over Node 的迁移说明。",
258
- llm as any,
259
- );
260
-
261
- expect(result.action).toBe("skip");
262
- expect(result.reason).toBe("llm-skip");
263
- expect(result.existingText).toBe("Prefers rg over grep.");
264
- expect(llmCalls).toBe(1);
265
- });
266
-
267
- it("stores a borderline chunk when LLM says it should CREATE", async () => {
268
- let llmCalls = 0;
269
- const store = {
270
- async vectorSearch() {
271
- return [
272
- buildSearchResult(
273
- "A2A gateway 之前是先本地 smoke,再补 Claude SDK 配置。",
274
- 0.74,
275
- ),
276
- ];
277
- },
278
- };
279
- const llm = {
280
- async dedupDecision() {
281
- llmCalls += 1;
282
- return { action: "CREATE" as const, reason: "new implementation branch" };
283
- },
284
- };
285
-
286
- const result = await dedupCheck(
287
- store as any,
288
- [1, 0, 0],
289
- "A2A gateway 升级后要补 MCP transport 配置和 LaunchAgent 环境变量,不是原来的 smoke 流程。",
290
- llm as any,
291
- );
292
-
293
- expect(result.action).toBe("store");
294
- expect(result.reason).toBe("unique");
295
- expect(llmCalls).toBe(1);
296
- });
297
-
298
- it("stores a borderline chunk when LLM says it adds new information and should MERGE", async () => {
299
- let llmCalls = 0;
300
- const store = {
301
- async vectorSearch() {
302
- return [
303
- buildSearchResult(
304
- "A2A 调 Claude SDK 时,可以先看 `adapters/claude.js` 里的权限相关配置。",
305
- 0.77,
306
- ),
307
- ];
308
- },
309
- };
310
- const llm = {
311
- async dedupDecision() {
312
- llmCalls += 1;
313
- return { action: "MERGE" as const, reason: "same topic but new implementation detail" };
314
- },
315
- };
316
-
317
- const result = await dedupCheck(
318
- store as any,
319
- [1, 0, 0],
320
- "给 A2A 调用传 permissionMode: \"dontAsk\" + allowedTools,避免 Claude SDK 在后台 HTTP handler 里卡死等权限确认。",
321
- llm as any,
322
- );
323
-
324
- expect(result.action).toBe("store");
325
- expect(result.reason).toBe("llm-merge");
326
- expect(result.existingText).toContain("权限相关配置");
327
- expect(llmCalls).toBe(1);
328
- });
329
-
330
- it("skips a hard-threshold duplicate without calling the LLM", async () => {
331
- let llmCalls = 0;
332
- const store = {
333
- async vectorSearch() {
334
- return [
335
- buildSearchResult(
336
- "OpenClaw provider 配置里已经写过 `ANTHROPIC_BASE_URL` 和 `ANTHROPIC_AUTH_TOKEN` 的设置方法。",
337
- 0.95,
338
- ),
339
- ];
340
- },
341
- };
342
- const llm = {
343
- async dedupDecision() {
344
- llmCalls += 1;
345
- return { action: "MERGE" as const, reason: "should never be called" };
346
- },
347
- };
348
-
349
- const result = await dedupCheck(
350
- store as any,
351
- [1, 0, 0],
352
- "OpenClaw provider 配置里已经写过 `ANTHROPIC_BASE_URL` 和 `ANTHROPIC_AUTH_TOKEN` 的设置方法,但这里重复了一遍。",
353
- llm as any,
354
- );
355
-
356
- expect(result.action).toBe("skip");
357
- expect(result.reason).toBe("hard");
358
- expect(llmCalls).toBe(0);
359
- });
360
- });
@@ -1,184 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { normalizeEntity, normalizePredicate, KGExtractor, isKGModeEnabled } from "../kg-extractor.js";
3
-
4
- function createMockLlm(response: unknown) {
5
- return {
6
- lastSystem: null as string | null,
7
- lastUser: null as string | null,
8
- async chatJson(_system: string, _user: string) {
9
- this.lastSystem = _system;
10
- this.lastUser = _user;
11
- return response;
12
- },
13
- // Stub remaining LLMClient methods
14
- async generateL0() { return null; },
15
- async generateL1() { return null; },
16
- async smartExtract() { return null; },
17
- async smartExtractBatch() { return []; },
18
- async dedupDecision() { return null; },
19
- async dedupBatch() { return []; },
20
- async mergeTexts() { return null; },
21
- async preferenceMatch() { return null; },
22
- async ping() { return false; },
23
- } as any;
24
- }
25
-
26
- function createMockKGStore() {
27
- return {
28
- storedTriples: [] as any[],
29
- async createTriples(triples: any[]) {
30
- const result = triples.map((t: any, i: number) => ({
31
- ...t,
32
- id: `mock-${i}`,
33
- timestamp: Date.now(),
34
- }));
35
- this.storedTriples.push(...result);
36
- return result;
37
- },
38
- } as any;
39
- }
40
-
41
- describe("normalizeEntity", () => {
42
- it("trims and collapses whitespace", () => {
43
- expect(normalizeEntity(" Hello World ")).toBe("Hello World");
44
- });
45
- it("converts to title case", () => {
46
- expect(normalizeEntity("alice")).toBe("Alice");
47
- expect(normalizeEntity("PYTHON")).toBe("Python");
48
- });
49
- it("preserves CJK text", () => {
50
- expect(normalizeEntity("张三")).toBe("张三");
51
- });
52
- it("strips quotes", () => {
53
- expect(normalizeEntity('"Alice"')).toBe("Alice");
54
- });
55
- it("returns empty for empty input", () => {
56
- expect(normalizeEntity("")).toBe("");
57
- });
58
- });
59
-
60
- describe("normalizePredicate", () => {
61
- it("converts to snake_case", () => {
62
- expect(normalizePredicate("works with")).toBe("works_with");
63
- expect(normalizePredicate("Created By")).toBe("created_by");
64
- });
65
- it("preserves CJK predicates", () => {
66
- expect(normalizePredicate("属于")).toBe("属于");
67
- });
68
- });
69
-
70
- describe("isKGModeEnabled", () => {
71
- it("reads env var", () => {
72
- const orig = process.env.RECALLNEST_KG_MODE;
73
- try {
74
- process.env.RECALLNEST_KG_MODE = "true";
75
- expect(isKGModeEnabled()).toBe(true);
76
- delete process.env.RECALLNEST_KG_MODE;
77
- expect(isKGModeEnabled()).toBe(false);
78
- } finally {
79
- if (orig !== undefined) process.env.RECALLNEST_KG_MODE = orig;
80
- else delete process.env.RECALLNEST_KG_MODE;
81
- }
82
- });
83
- });
84
-
85
- describe("KGExtractor.extract", () => {
86
- it("extracts valid triples", async () => {
87
- const llm = createMockLlm({
88
- triples: [
89
- { subject: "Alice", predicate: "uses", object: "Python", confidence: 0.9 },
90
- { subject: "Alice", predicate: "works_at", object: "Google", confidence: 0.85 },
91
- ],
92
- });
93
- const ext = new KGExtractor({ llmClient: llm, kgStore: createMockKGStore() });
94
- const triples = await ext.extract("Alice uses Python at Google");
95
- expect(triples.length).toBe(2);
96
- expect(triples[0].subject).toBe("Alice");
97
- });
98
-
99
- it("filters low confidence", async () => {
100
- const llm = createMockLlm({
101
- triples: [
102
- { subject: "Alice", predicate: "uses", object: "Python", confidence: 0.9 },
103
- { subject: "Alice", predicate: "maybe", object: "Bob", confidence: 0.3 },
104
- ],
105
- });
106
- const ext = new KGExtractor({ llmClient: llm, kgStore: createMockKGStore() });
107
- const triples = await ext.extract("Alice uses Python and maybe Bob");
108
- expect(triples.length).toBe(1);
109
- });
110
-
111
- it("normalizes entities", async () => {
112
- const llm = createMockLlm({
113
- triples: [{ subject: " alice ", predicate: "WORKS WITH", object: "bob", confidence: 0.9 }],
114
- });
115
- const ext = new KGExtractor({ llmClient: llm, kgStore: createMockKGStore() });
116
- const triples = await ext.extract("alice works with bob");
117
- expect(triples[0].subject).toBe("Alice");
118
- expect(triples[0].predicate).toBe("works_with");
119
- expect(triples[0].object).toBe("Bob");
120
- });
121
-
122
- it("skips self-referencing triples", async () => {
123
- const llm = createMockLlm({
124
- triples: [
125
- { subject: "Alice", predicate: "is", object: "Alice", confidence: 0.9 },
126
- { subject: "Bob", predicate: "uses", object: "Go", confidence: 0.9 },
127
- ],
128
- });
129
- const ext = new KGExtractor({ llmClient: llm, kgStore: createMockKGStore() });
130
- const triples = await ext.extract("Alice is Alice, Bob uses Go");
131
- expect(triples.length).toBe(1);
132
- expect(triples[0].subject).toBe("Bob");
133
- });
134
-
135
- it("deduplicates within batch", async () => {
136
- const llm = createMockLlm({
137
- triples: [
138
- { subject: "Alice", predicate: "uses", object: "Python", confidence: 0.9 },
139
- { subject: "Alice", predicate: "uses", object: "Python", confidence: 0.85 },
140
- ],
141
- });
142
- const ext = new KGExtractor({ llmClient: llm, kgStore: createMockKGStore() });
143
- expect((await ext.extract("Alice uses Python a lot")).length).toBe(1);
144
- });
145
-
146
- it("handles null LLM response", async () => {
147
- const llm = createMockLlm(null);
148
- const ext = new KGExtractor({ llmClient: llm, kgStore: createMockKGStore() });
149
- expect((await ext.extract("Some text here")).length).toBe(0);
150
- });
151
-
152
- it("skips short text", async () => {
153
- const llm = createMockLlm({ triples: [] });
154
- const ext = new KGExtractor({ llmClient: llm, kgStore: createMockKGStore() });
155
- expect((await ext.extract("hi")).length).toBe(0);
156
- expect(llm.lastUser).toBeNull(); // LLM not called
157
- });
158
- });
159
-
160
- describe("KGExtractor.extractAndStore", () => {
161
- it("persists triples to KG store", async () => {
162
- const llm = createMockLlm({
163
- triples: [
164
- { subject: "Alice", predicate: "uses", object: "Python", confidence: 0.9 },
165
- ],
166
- });
167
- const kgStore = createMockKGStore();
168
- const ext = new KGExtractor({ llmClient: llm, kgStore });
169
- const count = await ext.extractAndStore("Alice uses Python", "mem-001", "global");
170
- expect(count).toBe(1);
171
- expect(kgStore.storedTriples[0].scope).toBe("global");
172
- expect(kgStore.storedTriples[0].source_memory_id).toBe("mem-001");
173
- });
174
-
175
- it("caps source_text at 500 chars", async () => {
176
- const llm = createMockLlm({
177
- triples: [{ subject: "A", predicate: "uses", object: "B", confidence: 0.9 }],
178
- });
179
- const kgStore = createMockKGStore();
180
- const ext = new KGExtractor({ llmClient: llm, kgStore });
181
- await ext.extractAndStore("x".repeat(1000), "mem-001", "global");
182
- expect(kgStore.storedTriples[0].source_text.length).toBe(500);
183
- });
184
- });