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,374 +0,0 @@
1
- import { afterEach, describe, expect, it } from "bun:test";
2
- import { mkdtempSync, rmSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
-
6
- import { summarizeConflictAdvice } from "../conflict-advisor.js";
7
- import { buildConflictCandidateRecord, resolveConflictCandidate } from "../conflict-engine.js";
8
- import { ConflictCandidateStore } from "../conflict-store.js";
9
-
10
- const tempDirs: string[] = [];
11
-
12
- function createTempConflictStore(): ConflictCandidateStore {
13
- const dir = mkdtempSync(join(tmpdir(), "recallnest-conflicts-"));
14
- tempDirs.push(dir);
15
- return new ConflictCandidateStore(dir);
16
- }
17
-
18
- afterEach(() => {
19
- while (tempDirs.length > 0) {
20
- const dir = tempDirs.pop();
21
- if (dir) rmSync(dir, { recursive: true, force: true });
22
- }
23
- });
24
-
25
- function createConflictRecord() {
26
- return buildConflictCandidateRecord({
27
- canonicalKey: "user-reply-style",
28
- category: "preferences",
29
- fingerprint: "user-reply-style--durable-1--source-1--new-text",
30
- reason: "promotion_conflicts_with_existing_durable",
31
- existing: {
32
- memoryId: "durable-1",
33
- text: "User prefers concise, direct replies.",
34
- category: "preferences",
35
- scope: "memory:agent",
36
- importance: 0.84,
37
- metadata: JSON.stringify({
38
- source: "agent",
39
- canonicalKey: "user-reply-style",
40
- boundary: {
41
- layer: "durable",
42
- authority: "structured-memory",
43
- conflictPolicy: "latest-wins",
44
- originalCategory: "preferences",
45
- },
46
- }),
47
- canonicalKey: "user-reply-style",
48
- },
49
- incoming: {
50
- text: "User prefers colloquial writing that stays grounded and non-salesy.",
51
- category: "preferences",
52
- scope: "memory:agent",
53
- importance: 0.78,
54
- metadata: JSON.stringify({
55
- source: "agent",
56
- canonicalKey: "user-reply-style",
57
- boundary: {
58
- layer: "durable",
59
- authority: "structured-memory",
60
- conflictPolicy: "latest-wins",
61
- originalCategory: "preferences",
62
- },
63
- promotedFrom: {
64
- memoryId: "source-1",
65
- scope: "cc:session-1",
66
- category: "events",
67
- },
68
- }),
69
- source: "agent",
70
- sourceMemoryId: "source-1",
71
- sourceCategory: "events",
72
- },
73
- });
74
- }
75
-
76
- function createMergeableConflictRecord() {
77
- return buildConflictCandidateRecord({
78
- canonicalKey: "user-reply-style",
79
- category: "preferences",
80
- fingerprint: "user-reply-style--durable-1--source-1--mergeable",
81
- reason: "promotion_conflicts_with_existing_durable",
82
- existing: {
83
- memoryId: "durable-1",
84
- text: "User prefers concise grounded technical replies; avoid sales language.",
85
- category: "preferences",
86
- scope: "memory:agent",
87
- importance: 0.84,
88
- metadata: JSON.stringify({
89
- source: "agent",
90
- canonicalKey: "user-reply-style",
91
- boundary: {
92
- layer: "durable",
93
- authority: "structured-memory",
94
- conflictPolicy: "latest-wins",
95
- originalCategory: "preferences",
96
- },
97
- }),
98
- canonicalKey: "user-reply-style",
99
- },
100
- incoming: {
101
- text: "User prefers concise grounded technical replies; keep the tone colloquial.",
102
- category: "preferences",
103
- scope: "memory:agent",
104
- importance: 0.78,
105
- metadata: JSON.stringify({
106
- source: "agent",
107
- canonicalKey: "user-reply-style",
108
- boundary: {
109
- layer: "durable",
110
- authority: "structured-memory",
111
- conflictPolicy: "latest-wins",
112
- originalCategory: "preferences",
113
- },
114
- promotedFrom: {
115
- memoryId: "source-1",
116
- scope: "cc:session-1",
117
- category: "events",
118
- },
119
- }),
120
- source: "agent",
121
- sourceMemoryId: "source-1",
122
- sourceCategory: "events",
123
- },
124
- });
125
- }
126
-
127
- describe("ConflictCandidateStore", () => {
128
- it("saves, lists, and fetches open conflicts by fingerprint", async () => {
129
- const store = createTempConflictStore();
130
- const record = createConflictRecord();
131
- await store.save(record);
132
-
133
- const fetched = await store.getById(record.conflictId);
134
- expect(fetched?.conflictId).toBe(record.conflictId);
135
-
136
- const byFingerprint = await store.getOpenByFingerprint(record.fingerprint);
137
- expect(byFingerprint?.conflictId).toBe(record.conflictId);
138
-
139
- const recent = await store.listRecent({
140
- status: "open",
141
- canonicalKey: "user-reply-style",
142
- limit: 10,
143
- });
144
- expect(recent).toHaveLength(1);
145
- expect(recent[0]?.conflictId).toBe(record.conflictId);
146
- });
147
-
148
- it("accepts a unique short conflict ID prefix", async () => {
149
- const store = createTempConflictStore();
150
- const record = createConflictRecord();
151
- await store.save(record);
152
-
153
- const fetched = await store.getById(record.conflictId.slice(0, 8));
154
- expect(fetched?.conflictId).toBe(record.conflictId);
155
- });
156
- });
157
-
158
- describe("resolveConflictCandidate", () => {
159
- it("accepts incoming text and updates the existing durable memory", async () => {
160
- const conflictStore = createTempConflictStore();
161
- const record = createConflictRecord();
162
- await conflictStore.save(record);
163
-
164
- const updatedPayloads: any[] = [];
165
- const existingMemory = {
166
- id: "durable-1",
167
- text: "User prefers concise, direct replies.",
168
- vector: [1, 1, 1],
169
- category: "preferences",
170
- scope: "memory:agent",
171
- importance: 0.84,
172
- timestamp: 1_700_000_000_001,
173
- metadata: record.existing.metadata,
174
- };
175
-
176
- const result = await resolveConflictCandidate({
177
- store: {
178
- async getById(id: string) {
179
- return id === existingMemory.id ? existingMemory : null;
180
- },
181
- async update(id: string, updates: any) {
182
- updatedPayloads.push({ id, updates });
183
- Object.assign(existingMemory, updates);
184
- return existingMemory;
185
- },
186
- },
187
- embedder: {
188
- async embedPassage(text: string) {
189
- return [text.length, 9, 9];
190
- },
191
- },
192
- conflictStore,
193
- }, {
194
- conflictId: record.conflictId,
195
- resolution: "accept_incoming",
196
- notes: "Promoted evidence is cleaner than the original durable wording.",
197
- });
198
-
199
- expect(result.status).toBe("accepted-incoming");
200
- expect(result.updatedMemoryId).toBe("durable-1");
201
- expect(updatedPayloads).toHaveLength(1);
202
- expect(updatedPayloads[0]?.updates.text).toBe(record.incoming.text);
203
- expect(updatedPayloads[0]?.updates.metadata).toBe(record.incoming.metadata);
204
-
205
- const resolved = await conflictStore.getById(record.conflictId);
206
- expect(resolved?.status).toBe("accepted-incoming");
207
- expect(resolved?.resolutionNotes).toContain("cleaner");
208
- });
209
-
210
- it("can keep the existing durable memory without updating it", async () => {
211
- const conflictStore = createTempConflictStore();
212
- const record = createConflictRecord();
213
- await conflictStore.save(record);
214
-
215
- let updated = false;
216
- const result = await resolveConflictCandidate({
217
- store: {
218
- async getById() {
219
- return null;
220
- },
221
- async update() {
222
- updated = true;
223
- return null;
224
- },
225
- },
226
- embedder: {
227
- async embedPassage() {
228
- return [0, 0, 0];
229
- },
230
- },
231
- conflictStore,
232
- }, {
233
- conflictId: record.conflictId,
234
- resolution: "keep_existing",
235
- });
236
-
237
- expect(result.status).toBe("kept-existing");
238
- expect(updated).toBe(false);
239
-
240
- const resolved = await conflictStore.getById(record.conflictId);
241
- expect(resolved?.status).toBe("kept-existing");
242
- });
243
-
244
- it("merges same-category conflicts using the default merge suggestion", async () => {
245
- const conflictStore = createTempConflictStore();
246
- const record = createMergeableConflictRecord();
247
- await conflictStore.save(record);
248
-
249
- const updatedPayloads: any[] = [];
250
- const existingMemory = {
251
- id: "durable-1",
252
- text: record.existing.text,
253
- vector: [1, 1, 1],
254
- category: "preferences",
255
- scope: "memory:agent",
256
- importance: 0.84,
257
- timestamp: 1_700_000_000_001,
258
- metadata: record.existing.metadata,
259
- };
260
-
261
- const mergeSuggestion = summarizeConflictAdvice(record).mergeSuggestion;
262
- expect(mergeSuggestion).toBeTruthy();
263
-
264
- const result = await resolveConflictCandidate({
265
- store: {
266
- async getById(id: string) {
267
- return id === existingMemory.id ? existingMemory : null;
268
- },
269
- async update(id: string, updates: any) {
270
- updatedPayloads.push({ id, updates });
271
- Object.assign(existingMemory, updates);
272
- return existingMemory;
273
- },
274
- },
275
- embedder: {
276
- async embedPassage(text: string) {
277
- return [text.length, 7, 7];
278
- },
279
- },
280
- conflictStore,
281
- }, {
282
- conflictId: record.conflictId,
283
- resolution: "merge",
284
- notes: "Combine the grounded style and tone guidance.",
285
- });
286
-
287
- expect(result.status).toBe("merged");
288
- expect(result.updatedMemoryId).toBe("durable-1");
289
- expect(updatedPayloads).toHaveLength(1);
290
- expect(updatedPayloads[0]?.updates.text).toBe(mergeSuggestion);
291
- expect(updatedPayloads[0]?.updates.importance).toBe(0.84);
292
-
293
- const mergedMetadata = JSON.parse(updatedPayloads[0]?.updates.metadata || "{}");
294
- expect(mergedMetadata.canonicalKey).toBe("user-reply-style");
295
- expect(mergedMetadata.promotedFrom?.memoryId).toBe("source-1");
296
- expect(mergedMetadata.mergedFrom?.conflictId).toBe(record.conflictId);
297
-
298
- const resolved = await conflictStore.getById(record.conflictId);
299
- expect(resolved?.status).toBe("merged");
300
- expect(resolved?.resolutionNotes).toContain("grounded style");
301
- });
302
-
303
- it("rejects merge for cross-category conflicts", async () => {
304
- const conflictStore = createTempConflictStore();
305
- const record = buildConflictCandidateRecord({
306
- canonicalKey: "project-owner",
307
- category: "events",
308
- fingerprint: "project-owner--durable-1--source-2--cross-category",
309
- reason: "canonical_key_conflicts_with_existing_durable",
310
- existing: {
311
- memoryId: "durable-1",
312
- text: "Project owner: RecallNest",
313
- category: "entities",
314
- scope: "memory:agent",
315
- importance: 0.82,
316
- metadata: recordLikeMetadata("project-owner", "entities"),
317
- canonicalKey: "project-owner",
318
- },
319
- incoming: {
320
- text: "Project owner observation imported as an event.",
321
- category: "events",
322
- scope: "memory:agent",
323
- importance: 0.74,
324
- metadata: recordLikeMetadata("project-owner", "events"),
325
- source: "agent",
326
- sourceMemoryId: "source-2",
327
- sourceCategory: "events",
328
- },
329
- });
330
- await conflictStore.save(record);
331
-
332
- await expect(resolveConflictCandidate({
333
- store: {
334
- async getById() {
335
- return {
336
- id: "durable-1",
337
- text: "Project owner: RecallNest",
338
- vector: [1, 1, 1],
339
- category: "entities",
340
- scope: "memory:agent",
341
- importance: 0.82,
342
- timestamp: 1_700_000_000_002,
343
- metadata: record.existing.metadata,
344
- };
345
- },
346
- async update() {
347
- throw new Error("update should not be called");
348
- },
349
- },
350
- embedder: {
351
- async embedPassage() {
352
- return [0, 0, 0];
353
- },
354
- },
355
- conflictStore,
356
- }, {
357
- conflictId: record.conflictId,
358
- resolution: "merge",
359
- })).rejects.toThrow("same-category durable conflicts");
360
- });
361
- });
362
-
363
- function recordLikeMetadata(canonicalKey: string, category: string): string {
364
- return JSON.stringify({
365
- source: "agent",
366
- canonicalKey,
367
- boundary: {
368
- layer: "durable",
369
- authority: "structured-memory",
370
- conflictPolicy: "latest-wins",
371
- originalCategory: category,
372
- },
373
- });
374
- }
@@ -1,181 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
-
3
- import { ConsolidationEngine, DEFAULT_CONSOLIDATION_CONFIG, formatConsolidationResult, type ConsolidationResult } from "../consolidation-engine.js";
4
- import type { MemoryEntry, MemorySearchResult } from "../store.js";
5
-
6
- function makeEntry(overrides: Partial<MemoryEntry> & { id: string; text: string }): MemoryEntry {
7
- return {
8
- vector: [1, 0, 0],
9
- category: "events",
10
- scope: "project:test",
11
- importance: 0.7,
12
- timestamp: Date.now(),
13
- metadata: "{}",
14
- ...overrides,
15
- };
16
- }
17
-
18
- function createMockStore(entries: MemoryEntry[], similarityMap: Map<string, Map<string, number>> = new Map()) {
19
- const data = new Map(entries.map(e => [e.id, { ...e }]));
20
- const updates: Array<{ id: string; metadata: string }> = [];
21
-
22
- return {
23
- updates,
24
- store: {
25
- async list(scopeFilter?: string[], _category?: string, limit = 500, _offset = 0) {
26
- return [...data.values()]
27
- .filter(e => !scopeFilter || scopeFilter.some(s => e.scope === s))
28
- .slice(0, limit);
29
- },
30
- async getById(id: string) {
31
- return data.get(id) ?? null;
32
- },
33
- async vectorSearch(vector: number[], limit = 5, minScore = 0.3, scopeFilter?: string[]) {
34
- // Use the similarity map to compute fake scores
35
- const sourceEntry = [...data.values()].find(e =>
36
- e.vector.length === vector.length && e.vector.every((v, i) => v === vector[i])
37
- );
38
- if (!sourceEntry) return [];
39
-
40
- const sourceMap = similarityMap.get(sourceEntry.id);
41
- if (!sourceMap) return [];
42
-
43
- const results: MemorySearchResult[] = [];
44
- for (const [targetId, score] of sourceMap) {
45
- if (score < minScore) continue;
46
- const target = data.get(targetId);
47
- if (!target) continue;
48
- if (scopeFilter && !scopeFilter.some(s => target.scope === s)) continue;
49
- results.push({ entry: target, score });
50
- }
51
- return results.sort((a, b) => b.score - a.score).slice(0, limit);
52
- },
53
- async update(id: string, upd: { metadata?: string }) {
54
- const entry = data.get(id);
55
- if (!entry) return null;
56
- if (upd.metadata) {
57
- entry.metadata = upd.metadata;
58
- updates.push({ id, metadata: upd.metadata });
59
- }
60
- return entry;
61
- },
62
- },
63
- };
64
- }
65
-
66
- describe("ConsolidationEngine", () => {
67
- it("returns empty result for empty scope", async () => {
68
- const { store } = createMockStore([]);
69
- const engine = new ConsolidationEngine(store);
70
- const result = await engine.run("project:test");
71
- expect(result.originalCount).toBe(0);
72
- expect(result.clustersFound).toBe(0);
73
- });
74
-
75
- it("skips single-entry categories", async () => {
76
- const entries = [makeEntry({ id: "a", text: "only one" })];
77
- const { store } = createMockStore(entries);
78
- const engine = new ConsolidationEngine(store);
79
- const result = await engine.run("project:test");
80
- expect(result.originalCount).toBe(1);
81
- expect(result.clustersFound).toBe(0);
82
- });
83
-
84
- it("merges near-duplicates above mergeThreshold", async () => {
85
- const entryA = makeEntry({ id: "a", text: "I prefer TypeScript", vector: [1, 0, 0], importance: 0.9 });
86
- const entryB = makeEntry({ id: "b", text: "I prefer TypeScript language", vector: [0.99, 0.1, 0], importance: 0.5 });
87
-
88
- const simMap = new Map([
89
- ["a", new Map([["b", 0.95]])],
90
- ["b", new Map([["a", 0.95]])],
91
- ]);
92
-
93
- const { store, updates } = createMockStore([entryA, entryB], simMap);
94
- const engine = new ConsolidationEngine(store, { ...DEFAULT_CONSOLIDATION_CONFIG, mergeThreshold: 0.92 });
95
- const result = await engine.run("project:test");
96
-
97
- expect(result.clustersFound).toBe(1);
98
- expect(result.mergedCount).toBe(1);
99
- // Tier 3.3: Both entries now coexist in a version group instead of archiving.
100
- // Both A and B should have version_group metadata.
101
- const updateA = updates.find(u => u.id === "a");
102
- const updateB = updates.find(u => u.id === "b");
103
- expect(updateA).toBeTruthy();
104
- expect(updateB).toBeTruthy();
105
- const metaA = JSON.parse(updateA!.metadata);
106
- const metaB = JSON.parse(updateB!.metadata);
107
- expect(metaA.version_group).toBeTruthy();
108
- expect(metaB.version_group).toBe(metaA.version_group);
109
- // Canonical (A, higher importance) should have higher rank
110
- expect(metaA.version_rank).toBeGreaterThan(metaB.version_rank);
111
- });
112
-
113
- it("links related entries below mergeThreshold but above clusterThreshold", async () => {
114
- const entryA = makeEntry({ id: "a", text: "TypeScript config", vector: [1, 0, 0] });
115
- const entryB = makeEntry({ id: "b", text: "TypeScript setup", vector: [0.9, 0.1, 0] });
116
-
117
- const simMap = new Map([
118
- ["a", new Map([["b", 0.85]])],
119
- ["b", new Map([["a", 0.85]])],
120
- ]);
121
-
122
- const { store, updates } = createMockStore([entryA, entryB], simMap);
123
- const engine = new ConsolidationEngine(store, { ...DEFAULT_CONSOLIDATION_CONFIG, clusterThreshold: 0.82, mergeThreshold: 0.92 });
124
- const result = await engine.run("project:test");
125
-
126
- expect(result.clustersFound).toBe(1);
127
- expect(result.mergedCount).toBe(0);
128
- expect(result.relationsAdded).toBe(1);
129
- // Both should have clustering metadata
130
- const linkUpdate = updates.find(u => u.id === "b");
131
- expect(linkUpdate).toBeTruthy();
132
- const meta = JSON.parse(linkUpdate!.metadata);
133
- expect(meta.clustered_with).toBe("a");
134
- });
135
-
136
- it("detects heuristic contradictions", async () => {
137
- const entryA = makeEntry({ id: "a", text: "Always use strict mode in TypeScript projects", vector: [1, 0, 0] });
138
- const entryB = makeEntry({ id: "b", text: "Never use strict mode in TypeScript projects", vector: [0.98, 0.1, 0] });
139
-
140
- const simMap = new Map([
141
- ["a", new Map([["b", 0.95]])],
142
- ["b", new Map([["a", 0.95]])],
143
- ]);
144
-
145
- const { store } = createMockStore([entryA, entryB], simMap);
146
- const engine = new ConsolidationEngine(store, DEFAULT_CONSOLIDATION_CONFIG);
147
- const result = await engine.run("project:test");
148
-
149
- expect(result.conflictsDetected.length).toBe(1);
150
- expect(result.conflictsDetected[0].type).toBe("heuristic_contradiction");
151
- });
152
-
153
- it("skips archived entries", async () => {
154
- const entryA = makeEntry({ id: "a", text: "active entry here", vector: [1, 0, 0] });
155
- const entryB = makeEntry({ id: "b", text: "archived entry here", vector: [0.95, 0.1, 0], metadata: JSON.stringify({ state: "archived" }) });
156
-
157
- const { store } = createMockStore([entryA, entryB]);
158
- const engine = new ConsolidationEngine(store);
159
- const result = await engine.run("project:test");
160
-
161
- expect(result.originalCount).toBe(1); // only active
162
- });
163
- });
164
-
165
- describe("formatConsolidationResult", () => {
166
- it("formats a result with conflicts", () => {
167
- const result: ConsolidationResult = {
168
- originalCount: 100,
169
- clustersFound: 5,
170
- mergedCount: 3,
171
- relationsAdded: 7,
172
- conflictsDetected: [{ memoryA: "aaaa-bbbb", memoryB: "cccc-dddd", type: "heuristic_contradiction" }],
173
- scope: "project:test",
174
- };
175
- const text = formatConsolidationResult(result);
176
- expect(text).toContain("Scanned: 100");
177
- expect(text).toContain("Clusters found: 5");
178
- expect(text).toContain("Merged (versioned): 3");
179
- expect(text).toContain("Conflicts:");
180
- });
181
- });
@@ -1,64 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
-
3
- import type { PinAsset } from "../memory-assets.js";
4
- import { selectPinnedContext } from "../context-composer-pins.js";
5
-
6
- function buildPinnedAsset(overrides: Partial<PinAsset> = {}): PinAsset & { path: string } {
7
- return {
8
- id: overrides.id || "pin-1",
9
- type: "pinned-memory",
10
- createdAt: overrides.createdAt || "2026-03-20T00:00:00.000Z",
11
- updatedAt: overrides.updatedAt || "2026-03-20T00:00:00.000Z",
12
- title: overrides.title || "Pinned continuity note",
13
- summary: overrides.summary || "Pinned continuity reminder",
14
- tags: overrides.tags || [],
15
- source: overrides.source || {
16
- memoryId: "memory-1",
17
- scope: "memory:project:recallnest",
18
- timestamp: Date.parse("2026-03-20T00:00:00.000Z"),
19
- metadata: {},
20
- },
21
- retrieval: overrides.retrieval,
22
- snippet: overrides.snippet || "",
23
- path: "/tmp/pin-1.json",
24
- };
25
- }
26
-
27
- describe("context composer pins", () => {
28
- it("keeps later task-specific cues from long maintenance prompts", () => {
29
- const taskSeed =
30
- "继续 RecallNest continuity helper boundary audit ranking scoring selection orchestration context composer stable query fallback profile forwarding gap runner isolation";
31
-
32
- const pinnedContext = selectPinnedContext([
33
- buildPinnedAsset({
34
- title: "Eval runner isolation note",
35
- summary: "Fresh-window replay still needs explicit runner isolation to avoid shared component skew.",
36
- tags: ["recallnest", "runner", "isolation"],
37
- snippet: "Runner isolation is the key safeguard when continuity previews diverge across fresh-window replay.",
38
- }),
39
- ], {
40
- taskSeed,
41
- limit: 1,
42
- });
43
-
44
- expect(pinnedContext).toHaveLength(1);
45
- expect(pinnedContext[0]).toContain("Pinned: Eval runner isolation note: Fresh-window replay still needs explicit runner isolation to avoid shared component skew.");
46
- });
47
-
48
- it("ignores conversational continuation filler for vague memory-layer prompts", () => {
49
- const pinnedContext = selectPinnedContext([
50
- buildPinnedAsset({
51
- id: "pin-visual",
52
- title: "用户视觉审美偏好",
53
- summary: "用户常用视觉风格是手绘涂鸦风加高对比撞色;在内容包装和配图生成时,应优先沿用这一审美方向。",
54
- tags: ["审美偏好", "手绘涂鸦", "高对比撞色", "给刚才写的文章生成配图"],
55
- snippet: "给刚才写的文章生成配图,风格:手绘涂鸦风+高对比撞色。",
56
- }),
57
- ], {
58
- taskSeed: "把刚才那个 memory layer 接回去",
59
- limit: 1,
60
- });
61
-
62
- expect(pinnedContext).toEqual([]);
63
- });
64
- });
@@ -1,24 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
-
3
- import { countTaskSpecificTextMatches } from "../context-composer-task-selection.js";
4
-
5
- describe("context composer task selection", () => {
6
- it("keeps later task-specific cues from long maintenance prompts", () => {
7
- const taskSeed =
8
- "继续 RecallNest continuity helper boundary audit ranking scoring selection orchestration context composer stable query fallback profile forwarding gap runner isolation";
9
-
10
- expect(
11
- countTaskSpecificTextMatches(
12
- "Case: Continuity eval profile forwarding gap Problem: sparse writing prompts still failed until the eval profile was forwarded correctly.",
13
- taskSeed,
14
- ),
15
- ).toBeGreaterThan(0);
16
-
17
- expect(
18
- countTaskSpecificTextMatches(
19
- "Case: Eval runner shared components skewed later continuity previews Problem: runner isolation broke when shared component state leaked across fresh-window replay.",
20
- taskSeed,
21
- ),
22
- ).toBeGreaterThan(0);
23
- });
24
- });