recallnest 2.2.1 → 2.5.4

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 (227) hide show
  1. package/CHANGELOG.md +150 -0
  2. package/README.md +46 -35
  3. package/README_CN.md +41 -26
  4. package/assets/ui/app.js +1031 -0
  5. package/assets/ui/index.html +224 -0
  6. package/assets/ui/styles.css +781 -0
  7. package/config.json.example +4 -1
  8. package/connectors/examples/email-adapter.ts +73 -0
  9. package/connectors/examples/logs-adapter.ts +103 -0
  10. package/connectors/examples/rss-adapter.ts +95 -0
  11. package/docs/api-reference.md +1291 -0
  12. package/docs/architecture.md +168 -0
  13. package/docs/connector-spec.md +126 -0
  14. package/eval/cases-canary.json +58 -0
  15. package/eval/cases-scope-robustness.json +278 -0
  16. package/eval/cases.json +117 -20
  17. package/package.json +22 -1
  18. package/src/__tests__/access-tracker-novelty.test.ts +238 -0
  19. package/src/__tests__/activity-counter.test.ts +125 -0
  20. package/src/__tests__/adaptive-fidelity.test.ts +102 -0
  21. package/src/__tests__/admission-control.test.ts +206 -0
  22. package/src/__tests__/anchor-generator.test.ts +67 -0
  23. package/src/__tests__/auto-consolidation.test.ts +168 -0
  24. package/src/__tests__/auto-recall.test.ts +131 -0
  25. package/src/__tests__/babel-integration.test.ts +144 -0
  26. package/src/__tests__/batch-dedup.test.ts +65 -0
  27. package/src/__tests__/canary-eval.test.ts +114 -0
  28. package/src/__tests__/capture-engine.test.ts +1238 -0
  29. package/src/__tests__/capture-heuristic.test.ts +171 -0
  30. package/src/__tests__/cascade-forget.test.ts +210 -0
  31. package/src/__tests__/category-min-scores.test.ts +33 -0
  32. package/src/__tests__/cc8-cc1-essential-context.test.ts +298 -0
  33. package/src/__tests__/circuit-breaker.test.ts +99 -0
  34. package/src/__tests__/cluster-consolidation.test.ts +782 -0
  35. package/src/__tests__/confidence-metadata.test.ts +125 -0
  36. package/src/__tests__/confidence-tracker.test.ts +261 -0
  37. package/src/__tests__/conflict-advisor.test.ts +206 -0
  38. package/src/__tests__/conflict-escalation.test.ts +149 -0
  39. package/src/__tests__/conflict-lifecycle.test.ts +83 -0
  40. package/src/__tests__/conflict-output.test.ts +85 -0
  41. package/src/__tests__/conflict-store.test.ts +374 -0
  42. package/src/__tests__/connector-v1.test.ts +181 -0
  43. package/src/__tests__/consolidation-engine.test.ts +333 -0
  44. package/src/__tests__/constructive-retrieval.test.ts +509 -0
  45. package/src/__tests__/context-collapse-renderer.test.ts +214 -0
  46. package/src/__tests__/context-composer-pins.test.ts +64 -0
  47. package/src/__tests__/context-composer-task-selection.test.ts +24 -0
  48. package/src/__tests__/context-reconstructor.test.ts +65 -0
  49. package/src/__tests__/context-renderer.test.ts +91 -0
  50. package/src/__tests__/continuity-eval.test.ts +275 -0
  51. package/src/__tests__/continuity-schema.test.ts +97 -0
  52. package/src/__tests__/contracts.test.ts +216 -0
  53. package/src/__tests__/conversation-importer.test.ts +692 -0
  54. package/src/__tests__/core-recall-eval.test.ts +235 -0
  55. package/src/__tests__/core-summary.test.ts +109 -0
  56. package/src/__tests__/data-checkup.test.ts +276 -0
  57. package/src/__tests__/decay-exemption.test.ts +84 -0
  58. package/src/__tests__/dedup-actions.test.ts +58 -0
  59. package/src/__tests__/dedup-guard.test.ts +39 -0
  60. package/src/__tests__/deterministic-id.test.ts +39 -0
  61. package/src/__tests__/distill-lock.test.ts +305 -0
  62. package/src/__tests__/distill-session-argshape.test.ts +79 -0
  63. package/src/__tests__/doctor.test.ts +319 -0
  64. package/src/__tests__/dream-pipeline.test.ts +321 -0
  65. package/src/__tests__/drill-down.test.ts +108 -0
  66. package/src/__tests__/embedder.test.ts +122 -0
  67. package/src/__tests__/emotion-decay.test.ts +46 -0
  68. package/src/__tests__/emotion-detector.test.ts +56 -0
  69. package/src/__tests__/emotion-phase1-integration.test.ts +153 -0
  70. package/src/__tests__/emotion-retrieval.test.ts +56 -0
  71. package/src/__tests__/entity-resolver.test.ts +58 -0
  72. package/src/__tests__/env-config.test.ts +151 -0
  73. package/src/__tests__/error-signature-boost.test.ts +87 -0
  74. package/src/__tests__/error-signature.test.ts +95 -0
  75. package/src/__tests__/error-taxonomy.test.ts +129 -0
  76. package/src/__tests__/ethics-layer.test.ts +476 -0
  77. package/src/__tests__/event-segmenter.test.ts +153 -0
  78. package/src/__tests__/feature-flags-integration.test.ts +54 -0
  79. package/src/__tests__/frequency-tracker.test.ts +113 -0
  80. package/src/__tests__/graph-export.test.ts +489 -0
  81. package/src/__tests__/hotness-blend.test.ts +76 -0
  82. package/src/__tests__/ingest-chunking.test.ts +44 -0
  83. package/src/__tests__/ingest.test.ts +360 -0
  84. package/src/__tests__/interference-detector.test.ts +203 -0
  85. package/src/__tests__/kg-extractor.test.ts +192 -0
  86. package/src/__tests__/kg-store.test.ts +300 -0
  87. package/src/__tests__/llm-consolidation.test.ts +226 -0
  88. package/src/__tests__/maintenance-locks.test.ts +174 -0
  89. package/src/__tests__/mcp-efficiency.test.ts +288 -0
  90. package/src/__tests__/mcp-related-scopes.test.ts +151 -0
  91. package/src/__tests__/memory-boundaries.test.ts +250 -0
  92. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +345 -0
  93. package/src/__tests__/memory-evolution.test.ts +271 -0
  94. package/src/__tests__/memory-health-rebalance.test.ts +144 -0
  95. package/src/__tests__/memory-lint.test.ts +421 -0
  96. package/src/__tests__/memory-output.test.ts +215 -0
  97. package/src/__tests__/memory-promotion.test.ts +315 -0
  98. package/src/__tests__/migrate-dedup-ids.test.ts +65 -0
  99. package/src/__tests__/multi-vector.test.ts +242 -0
  100. package/src/__tests__/narrative-architecture.test.ts +456 -0
  101. package/src/__tests__/noise-filter.test.ts +205 -0
  102. package/src/__tests__/obsidian-connector.test.ts +363 -0
  103. package/src/__tests__/omitted-reporting.test.ts +98 -0
  104. package/src/__tests__/pending-review.test.ts +77 -0
  105. package/src/__tests__/pii-redact.test.ts +314 -0
  106. package/src/__tests__/post-retrieval-filter.test.ts +178 -0
  107. package/src/__tests__/ppr-traversal.test.ts +256 -0
  108. package/src/__tests__/prediction-engine.test.ts +349 -0
  109. package/src/__tests__/predictive-prospective-memory.test.ts +416 -0
  110. package/src/__tests__/preference-guard-smoke.test.ts +211 -0
  111. package/src/__tests__/preference-matcher.test.ts +230 -0
  112. package/src/__tests__/preference-slots.test.ts +205 -0
  113. package/src/__tests__/prospective-memory.test.ts +283 -0
  114. package/src/__tests__/query-entity-detector.test.ts +171 -0
  115. package/src/__tests__/query-expander-alias.test.ts +146 -0
  116. package/src/__tests__/query-expander.test.ts +34 -0
  117. package/src/__tests__/recall-governor.test.ts +227 -0
  118. package/src/__tests__/recall-mode.test.ts +64 -0
  119. package/src/__tests__/result-synthesizer.test.ts +198 -0
  120. package/src/__tests__/resume-context.test.ts +4898 -0
  121. package/src/__tests__/retrieval-trace.test.ts +213 -0
  122. package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
  123. package/src/__tests__/retroactive-boost.test.ts +223 -0
  124. package/src/__tests__/salience-scoring.test.ts +106 -0
  125. package/src/__tests__/scope-inventory.test.ts +200 -0
  126. package/src/__tests__/scope-policy.test.ts +69 -0
  127. package/src/__tests__/scope-suggester.test.ts +80 -0
  128. package/src/__tests__/session-checkpoint.test.ts +291 -0
  129. package/src/__tests__/session-distiller.test.ts +640 -0
  130. package/src/__tests__/session-gc.test.ts +215 -0
  131. package/src/__tests__/skill-engine.test.ts +552 -0
  132. package/src/__tests__/skill-promotion.test.ts +601 -0
  133. package/src/__tests__/skill-verifier.test.ts +122 -0
  134. package/src/__tests__/source-heartbeat.test.ts +250 -0
  135. package/src/__tests__/store-patch-metadata.test.ts +205 -0
  136. package/src/__tests__/store-scope-audit.test.ts +81 -0
  137. package/src/__tests__/store-upsert.test.ts +184 -0
  138. package/src/__tests__/supersede-chain.test.ts +154 -0
  139. package/src/__tests__/temporal-parser.test.ts +241 -0
  140. package/src/__tests__/temporal-validity.test.ts +160 -0
  141. package/src/__tests__/term-registry.test.ts +121 -0
  142. package/src/__tests__/tool-error-surfacing.test.ts +98 -0
  143. package/src/__tests__/tool-output-compressor.test.ts +160 -0
  144. package/src/__tests__/topic-tag.test.ts +113 -0
  145. package/src/__tests__/trusted-memory.test.ts +839 -0
  146. package/src/__tests__/usage-tracker.test.ts +287 -0
  147. package/src/__tests__/version-manager.test.ts +213 -0
  148. package/src/__tests__/workflow-observation-skill-outcome.test.ts +352 -0
  149. package/src/__tests__/workflow-observation.test.ts +433 -0
  150. package/src/__tests__/write-verifier.test.ts +122 -0
  151. package/src/access-tracker.ts +38 -32
  152. package/src/activity-counter.ts +46 -18
  153. package/src/aliases.ts +133 -0
  154. package/src/api-server.ts +27 -7
  155. package/src/audit-log.ts +4 -2
  156. package/src/auto-consolidation.ts +8 -3
  157. package/src/auto-gc.ts +85 -21
  158. package/src/capture-engine.ts +54 -21
  159. package/src/cli.ts +199 -37
  160. package/src/confidence-tracker.ts +36 -18
  161. package/src/connector-types.ts +57 -0
  162. package/src/consolidation-engine.ts +109 -11
  163. package/src/context-composer.ts +8 -1
  164. package/src/conversation-importer.ts +25 -1
  165. package/src/data-checkup.ts +126 -7
  166. package/src/distill-lock.ts +194 -15
  167. package/src/doctor.ts +38 -2
  168. package/src/dream-pipeline.ts +118 -34
  169. package/src/env-config.ts +77 -0
  170. package/src/error-signature.ts +153 -0
  171. package/src/error-taxonomy.ts +115 -0
  172. package/src/eval.ts +251 -6
  173. package/src/idempotency.ts +8 -0
  174. package/src/ingest.ts +234 -10
  175. package/src/kg-extractor.ts +6 -2
  176. package/src/kg-store.ts +236 -44
  177. package/src/llm-consolidation.ts +2 -1
  178. package/src/mcp-server.ts +85 -1799
  179. package/src/mcp-tool-deps.ts +48 -0
  180. package/src/mcp-tools-advanced.ts +941 -0
  181. package/src/mcp-tools-core.ts +633 -0
  182. package/src/mcp-tools-governance.ts +475 -0
  183. package/src/memory-boundaries.ts +3 -1
  184. package/src/memory-evolution.ts +73 -1
  185. package/src/memory-lint.ts +183 -10
  186. package/src/memory-output.ts +114 -3
  187. package/src/memory-promotion.ts +246 -0
  188. package/src/memory-schema.ts +25 -2
  189. package/src/memory-store-port.ts +43 -0
  190. package/src/multi-vector.ts +2 -1
  191. package/src/narrative-schema.ts +2 -1
  192. package/src/noise-filter.ts +57 -6
  193. package/src/obsidian-connector.ts +282 -0
  194. package/src/pii-detector.ts +157 -5
  195. package/src/query-expander.ts +90 -9
  196. package/src/result-synthesizer.ts +2 -1
  197. package/src/retention-policy.ts +2 -1
  198. package/src/retriever.ts +259 -139
  199. package/src/runtime-config.ts +8 -1
  200. package/src/scope-suggester.ts +113 -0
  201. package/src/session-distiller.ts +23 -2
  202. package/src/session-engine.ts +2 -1
  203. package/src/session-schema.ts +3 -2
  204. package/src/session-store.ts +5 -0
  205. package/src/skill-engine.ts +78 -0
  206. package/src/skill-promotion.ts +247 -47
  207. package/src/skill-schema.ts +16 -3
  208. package/src/skill-verifier.ts +152 -0
  209. package/src/source-heartbeat.ts +108 -0
  210. package/src/store.ts +267 -38
  211. package/src/tool-output-compressor.ts +3 -1
  212. package/src/ui-server.ts +2 -1
  213. package/src/usage-tracker.ts +242 -0
  214. package/src/version-manager.ts +3 -2
  215. package/src/workflow-observation-engine.ts +2 -1
  216. package/src/workflow-observation-schema.ts +2 -1
  217. package/src/workflow-observation-store.ts +6 -2
  218. package/.env +0 -4
  219. package/CLAUDE.md +0 -46
  220. package/config.json +0 -55
  221. package/daily-recall.sh +0 -110
  222. package/logs/daily-recall.error.log +0 -0
  223. package/logs/daily-recall.log +0 -30
  224. package/logs/ingest-2026-04-11.log +0 -110
  225. package/logs/ingest-2026-04-12.log +0 -90
  226. package/logs/launchagent-stderr.log +0 -0
  227. package/logs/launchagent-stdout.log +0 -0
@@ -0,0 +1,345 @@
1
+ /**
2
+ * Tests for Memory Evolution Phase B-2 (Archive Strategy),
3
+ * B-3 (LLM Importance Assessment), and E-1 (Multi-factor Retrieval Scoring).
4
+ */
5
+ import { describe, expect, test } from "bun:test";
6
+ import {
7
+ parseEvolution,
8
+ computeDecayScore,
9
+ computeUsageAdjustedDecayScore,
10
+ buildArchivedMetadata,
11
+ isActiveMemory,
12
+ patchEvolution,
13
+ type EvolutionMetadata,
14
+ } from "../memory-evolution.js";
15
+ import {
16
+ maybeRunGc,
17
+ resetGcTimestamp,
18
+ DEFAULT_AUTO_GC_CONFIG,
19
+ type AutoGcConfig,
20
+ } from "../auto-gc.js";
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // B-2: Archive Strategy Tests
24
+ // ---------------------------------------------------------------------------
25
+
26
+ describe("B-2: Archive Strategy", () => {
27
+ test("buildArchivedMetadata sets status to archived", () => {
28
+ const meta = JSON.stringify({ evolution: { status: "active", version: 1 } });
29
+ const archived = buildArchivedMetadata(meta);
30
+ const evo = parseEvolution(archived);
31
+ expect(evo.status).toBe("archived");
32
+ });
33
+
34
+ test("archived memory is not active", () => {
35
+ const meta = JSON.stringify({ evolution: { status: "archived", version: 1 } });
36
+ expect(isActiveMemory(meta)).toBe(false);
37
+ });
38
+
39
+ test("active memory is active", () => {
40
+ const meta = JSON.stringify({ evolution: { status: "active", version: 1 } });
41
+ expect(isActiveMemory(meta)).toBe(true);
42
+ });
43
+
44
+ describe("auto-gc with evolution system", () => {
45
+ function makeStore(entries: Array<{
46
+ id: string;
47
+ importance: number;
48
+ timestamp: number;
49
+ metadata: string;
50
+ }>) {
51
+ const data = new Map(entries.map(e => [e.id, {
52
+ ...e,
53
+ text: "test",
54
+ vector: [1, 0, 0],
55
+ category: "events",
56
+ scope: "project:test",
57
+ }]));
58
+ const updates: Array<{ id: string; metadata: string }> = [];
59
+ return {
60
+ store: {
61
+ stats: async () => ({ totalCount: data.size, scopeCounts: {}, categoryCounts: {} }),
62
+ list: async () => Array.from(data.values()),
63
+ listPage: async (opts: { limit?: number; offset?: number } = {}) => {
64
+ const { limit = 1000, offset = 0 } = opts;
65
+ return Array.from(data.values()).slice(offset, offset + limit);
66
+ },
67
+ update: async (id: string, patch: { metadata: string }) => {
68
+ updates.push({ id, metadata: patch.metadata });
69
+ const entry = data.get(id);
70
+ if (!entry) return null;
71
+ entry.metadata = patch.metadata;
72
+ return entry;
73
+ },
74
+ },
75
+ updates,
76
+ };
77
+ }
78
+
79
+ test("archives low-decay-score memories", async () => {
80
+ resetGcTimestamp();
81
+ const now = Date.now();
82
+ const oldTimestamp = now - 120 * 86_400_000; // 120 days ago
83
+ const activeMeta = JSON.stringify({
84
+ evolution: {
85
+ status: "active",
86
+ version: 1,
87
+ accessCount: 0,
88
+ lastAccessedAt: null,
89
+ validFrom: oldTimestamp,
90
+ validUntil: null,
91
+ },
92
+ });
93
+
94
+ const { store, updates } = makeStore([
95
+ { id: "old-low", importance: 0.1, timestamp: oldTimestamp, metadata: activeMeta },
96
+ ]);
97
+
98
+ const config: AutoGcConfig = {
99
+ ...DEFAULT_AUTO_GC_CONFIG,
100
+ minMemoryCount: 1,
101
+ minHoursSinceLastGc: 0,
102
+ decayScoreThreshold: 0.2,
103
+ minAgeDays: 30,
104
+ };
105
+
106
+ const result = await maybeRunGc(store as any, config);
107
+ expect(result.triggered).toBe(true);
108
+ expect(result.archivedCount).toBe(1);
109
+ expect(updates.length).toBe(1);
110
+
111
+ const archivedEvo = parseEvolution(updates[0].metadata);
112
+ expect(archivedEvo.status).toBe("archived");
113
+ });
114
+
115
+ test("skips pinned memories (importance >= 0.95)", async () => {
116
+ resetGcTimestamp();
117
+ const now = Date.now();
118
+ const oldTimestamp = now - 120 * 86_400_000;
119
+ const activeMeta = JSON.stringify({
120
+ evolution: {
121
+ status: "active",
122
+ version: 1,
123
+ accessCount: 0,
124
+ lastAccessedAt: null,
125
+ validFrom: oldTimestamp,
126
+ validUntil: null,
127
+ },
128
+ });
129
+
130
+ const { store, updates } = makeStore([
131
+ { id: "pinned", importance: 0.95, timestamp: oldTimestamp, metadata: activeMeta },
132
+ ]);
133
+
134
+ const config: AutoGcConfig = {
135
+ ...DEFAULT_AUTO_GC_CONFIG,
136
+ minMemoryCount: 1,
137
+ minHoursSinceLastGc: 0,
138
+ };
139
+
140
+ const result = await maybeRunGc(store as any, config);
141
+ expect(result.triggered).toBe(true);
142
+ expect(result.archivedCount).toBe(0);
143
+ expect(updates.length).toBe(0);
144
+ });
145
+
146
+ test("usage-adjusted GC is gated and can archive cold high-frequency memories", async () => {
147
+ const savedUsageDecay = process.env.RECALLNEST_USAGE_DECAY;
148
+ const savedConstructive = process.env.RECALLNEST_CONSTRUCTIVE_RETRIEVAL;
149
+ try {
150
+ const now = Date.now();
151
+ const oldTimestamp = now - 180 * 86_400_000;
152
+ const coldMeta = JSON.stringify({
153
+ accessCount: 10,
154
+ evolution: {
155
+ status: "active",
156
+ version: 1,
157
+ accessCount: 10,
158
+ lastAccessedAt: now,
159
+ validFrom: oldTimestamp,
160
+ validUntil: null,
161
+ },
162
+ });
163
+ const config: AutoGcConfig = {
164
+ ...DEFAULT_AUTO_GC_CONFIG,
165
+ minMemoryCount: 1,
166
+ minHoursSinceLastGc: 0,
167
+ decayScoreThreshold: 0.15,
168
+ minAgeDays: 30,
169
+ };
170
+
171
+ delete process.env.RECALLNEST_USAGE_DECAY;
172
+ delete process.env.RECALLNEST_CONSTRUCTIVE_RETRIEVAL;
173
+ resetGcTimestamp();
174
+ const withoutFlag = makeStore([
175
+ { id: "cold-no-flag", importance: 0.05, timestamp: oldTimestamp, metadata: coldMeta },
176
+ ]);
177
+ const baseline = await maybeRunGc(withoutFlag.store as any, config);
178
+ expect(baseline.archivedCount).toBe(0);
179
+ expect(withoutFlag.updates).toHaveLength(0);
180
+
181
+ process.env.RECALLNEST_USAGE_DECAY = "true";
182
+ process.env.RECALLNEST_CONSTRUCTIVE_RETRIEVAL = "true";
183
+ resetGcTimestamp();
184
+ const withFlag = makeStore([
185
+ { id: "cold-with-flag", importance: 0.05, timestamp: oldTimestamp, metadata: coldMeta },
186
+ ]);
187
+ const adjusted = await maybeRunGc(withFlag.store as any, config);
188
+ expect(adjusted.archivedCount).toBe(1);
189
+ expect(withFlag.updates).toHaveLength(1);
190
+ } finally {
191
+ if (savedUsageDecay === undefined) delete process.env.RECALLNEST_USAGE_DECAY;
192
+ else process.env.RECALLNEST_USAGE_DECAY = savedUsageDecay;
193
+ if (savedConstructive === undefined) delete process.env.RECALLNEST_CONSTRUCTIVE_RETRIEVAL;
194
+ else process.env.RECALLNEST_CONSTRUCTIVE_RETRIEVAL = savedConstructive;
195
+ }
196
+ });
197
+
198
+ test("skips recently created memories", async () => {
199
+ resetGcTimestamp();
200
+ const recentMeta = JSON.stringify({
201
+ evolution: {
202
+ status: "active",
203
+ version: 1,
204
+ accessCount: 0,
205
+ lastAccessedAt: null,
206
+ validFrom: Date.now(),
207
+ validUntil: null,
208
+ },
209
+ });
210
+
211
+ const { store, updates } = makeStore([
212
+ { id: "recent", importance: 0.1, timestamp: Date.now(), metadata: recentMeta },
213
+ ]);
214
+
215
+ const config: AutoGcConfig = {
216
+ ...DEFAULT_AUTO_GC_CONFIG,
217
+ minMemoryCount: 1,
218
+ minHoursSinceLastGc: 0,
219
+ minAgeDays: 30,
220
+ };
221
+
222
+ const result = await maybeRunGc(store as any, config);
223
+ expect(result.triggered).toBe(true);
224
+ expect(result.archivedCount).toBe(0);
225
+ });
226
+
227
+ test("does not trigger when below memory threshold", async () => {
228
+ resetGcTimestamp();
229
+ const { store } = makeStore([
230
+ { id: "one", importance: 0.1, timestamp: Date.now() - 90 * 86_400_000, metadata: "{}" },
231
+ ]);
232
+
233
+ const result = await maybeRunGc(store as any, {
234
+ ...DEFAULT_AUTO_GC_CONFIG,
235
+ minMemoryCount: 1000,
236
+ });
237
+ expect(result.triggered).toBe(false);
238
+ expect(result.reason).toBe("below_memory_threshold");
239
+ });
240
+ });
241
+ });
242
+
243
+ // ---------------------------------------------------------------------------
244
+ // E-1: Access Count Boost Tests (via computeDecayScore)
245
+ // ---------------------------------------------------------------------------
246
+
247
+ describe("E-1: Multi-factor Retrieval Scoring", () => {
248
+ test("higher accessCount produces higher decay score", () => {
249
+ const base: EvolutionMetadata = {
250
+ status: "active",
251
+ version: 1,
252
+ accessCount: 0,
253
+ lastAccessedAt: null,
254
+ supersededBy: null,
255
+ consolidatedInto: null,
256
+ sourceMemories: [],
257
+ validFrom: Date.now() - 30 * 86_400_000,
258
+ validUntil: null,
259
+ };
260
+ const lowAccess = computeDecayScore(base, 0.5);
261
+ const highAccess = computeDecayScore({ ...base, accessCount: 10, lastAccessedAt: Date.now() }, 0.5);
262
+ expect(highAccess).toBeGreaterThan(lowAccess);
263
+ });
264
+
265
+ test("higher importance produces higher decay score", () => {
266
+ const base: EvolutionMetadata = {
267
+ status: "active",
268
+ version: 1,
269
+ accessCount: 2,
270
+ lastAccessedAt: Date.now() - 86_400_000,
271
+ supersededBy: null,
272
+ consolidatedInto: null,
273
+ sourceMemories: [],
274
+ validFrom: Date.now() - 30 * 86_400_000,
275
+ validUntil: null,
276
+ };
277
+ const lowImportance = computeDecayScore(base, 0.3);
278
+ const highImportance = computeDecayScore(base, 0.9);
279
+ expect(highImportance).toBeGreaterThan(lowImportance);
280
+ });
281
+
282
+ test("older memories without access have lower decay scores", () => {
283
+ const recent: EvolutionMetadata = {
284
+ status: "active",
285
+ version: 1,
286
+ accessCount: 0,
287
+ lastAccessedAt: null,
288
+ supersededBy: null,
289
+ consolidatedInto: null,
290
+ sourceMemories: [],
291
+ validFrom: Date.now() - 7 * 86_400_000,
292
+ validUntil: null,
293
+ };
294
+ const old: EvolutionMetadata = {
295
+ ...recent,
296
+ validFrom: Date.now() - 180 * 86_400_000,
297
+ };
298
+ expect(computeDecayScore(recent, 0.5)).toBeGreaterThan(computeDecayScore(old, 0.5));
299
+ });
300
+ });
301
+
302
+ describe("P0 B-2: usage-adjusted GC scoring", () => {
303
+ const now = Date.now();
304
+ const oldColdEvo: EvolutionMetadata = {
305
+ status: "active",
306
+ version: 1,
307
+ accessCount: 10,
308
+ lastAccessedAt: now,
309
+ supersededBy: null,
310
+ supersedes: null,
311
+ evolutionNote: null,
312
+ consolidatedInto: null,
313
+ contributedToPattern: null,
314
+ sourceMemories: [],
315
+ validFrom: now - 180 * 86_400_000,
316
+ validUntil: null,
317
+ eventTime: null,
318
+ };
319
+
320
+ test("discounts frequency only for cold memories in GC scoring", () => {
321
+ const metadata = JSON.stringify({ accessCount: 10 });
322
+ const base = computeDecayScore(oldColdEvo, 0.05, now, metadata);
323
+ const adjusted = computeUsageAdjustedDecayScore(oldColdEvo, 0.05, now, metadata);
324
+
325
+ expect(base).toBeGreaterThan(0.15);
326
+ expect(adjusted).toBeLessThan(base);
327
+ expect(adjusted).toBeLessThan(0.15);
328
+ });
329
+
330
+ test("does not discount pending reminders or promoted memories", () => {
331
+ const pendingReminder = JSON.stringify({
332
+ accessCount: 10,
333
+ prospective: { status: "pending" },
334
+ });
335
+ const promoted = JSON.stringify({
336
+ accessCount: 10,
337
+ promotedTo: "memory:durable-target",
338
+ });
339
+
340
+ expect(computeUsageAdjustedDecayScore(oldColdEvo, 0.05, now, pendingReminder))
341
+ .toBe(computeDecayScore(oldColdEvo, 0.05, now, pendingReminder));
342
+ expect(computeUsageAdjustedDecayScore(oldColdEvo, 0.05, now, promoted))
343
+ .toBe(computeDecayScore(oldColdEvo, 0.05, now, promoted));
344
+ });
345
+ });
@@ -0,0 +1,271 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ defaultEvolution,
4
+ parseEvolution,
5
+ patchEvolution,
6
+ isActiveMemory,
7
+ recordAccess,
8
+ buildSupersedeMetadata,
9
+ buildConsolidatedMetadata,
10
+ buildArchivedMetadata,
11
+ computeDecayScore,
12
+ type EvolutionMetadata,
13
+ } from "../memory-evolution.js";
14
+
15
+ describe("memory-evolution", () => {
16
+ // -----------------------------------------------------------------------
17
+ // defaultEvolution
18
+ // -----------------------------------------------------------------------
19
+ describe("defaultEvolution", () => {
20
+ test("returns active status with version 1", () => {
21
+ const evo = defaultEvolution(1000);
22
+ expect(evo.status).toBe("active");
23
+ expect(evo.version).toBe(1);
24
+ expect(evo.accessCount).toBe(0);
25
+ expect(evo.lastAccessedAt).toBeNull();
26
+ expect(evo.supersededBy).toBeNull();
27
+ expect(evo.consolidatedInto).toBeNull();
28
+ expect(evo.sourceMemories).toEqual([]);
29
+ expect(evo.validFrom).toBe(1000);
30
+ expect(evo.validUntil).toBeNull();
31
+ });
32
+
33
+ test("uses Date.now() when no timestamp provided", () => {
34
+ const before = Date.now();
35
+ const evo = defaultEvolution();
36
+ expect(evo.validFrom).toBeGreaterThanOrEqual(before);
37
+ expect(evo.validFrom).toBeLessThanOrEqual(Date.now());
38
+ });
39
+ });
40
+
41
+ // -----------------------------------------------------------------------
42
+ // parseEvolution
43
+ // -----------------------------------------------------------------------
44
+ describe("parseEvolution", () => {
45
+ test("returns defaults for undefined metadata", () => {
46
+ const evo = parseEvolution(undefined, 2000);
47
+ expect(evo.status).toBe("active");
48
+ expect(evo.validFrom).toBe(2000);
49
+ });
50
+
51
+ test("returns defaults for empty string metadata", () => {
52
+ const evo = parseEvolution("", 2000);
53
+ expect(evo.status).toBe("active");
54
+ });
55
+
56
+ test("returns defaults for metadata without evolution field", () => {
57
+ const meta = JSON.stringify({ source: "manual", tags: [] });
58
+ const evo = parseEvolution(meta, 3000);
59
+ expect(evo.status).toBe("active");
60
+ expect(evo.validFrom).toBe(3000);
61
+ });
62
+
63
+ test("returns defaults for invalid JSON", () => {
64
+ const evo = parseEvolution("not json", 4000);
65
+ expect(evo.status).toBe("active");
66
+ expect(evo.validFrom).toBe(4000);
67
+ });
68
+
69
+ test("parses valid evolution metadata", () => {
70
+ const meta = JSON.stringify({
71
+ source: "agent",
72
+ evolution: {
73
+ status: "superseded",
74
+ version: 3,
75
+ accessCount: 42,
76
+ lastAccessedAt: 5000,
77
+ supersededBy: "abc-123",
78
+ consolidatedInto: null,
79
+ sourceMemories: ["x", "y"],
80
+ validFrom: 1000,
81
+ validUntil: 4000,
82
+ },
83
+ });
84
+ const evo = parseEvolution(meta);
85
+ expect(evo.status).toBe("superseded");
86
+ expect(evo.version).toBe(3);
87
+ expect(evo.accessCount).toBe(42);
88
+ expect(evo.lastAccessedAt).toBe(5000);
89
+ expect(evo.supersededBy).toBe("abc-123");
90
+ expect(evo.sourceMemories).toEqual(["x", "y"]);
91
+ expect(evo.validFrom).toBe(1000);
92
+ expect(evo.validUntil).toBe(4000);
93
+ });
94
+
95
+ test("fills missing fields with defaults", () => {
96
+ const meta = JSON.stringify({ evolution: { status: "archived" } });
97
+ const evo = parseEvolution(meta, 6000);
98
+ expect(evo.status).toBe("archived");
99
+ expect(evo.version).toBe(1);
100
+ expect(evo.accessCount).toBe(0);
101
+ expect(evo.validFrom).toBe(6000);
102
+ });
103
+ });
104
+
105
+ // -----------------------------------------------------------------------
106
+ // patchEvolution
107
+ // -----------------------------------------------------------------------
108
+ describe("patchEvolution", () => {
109
+ test("creates evolution on empty metadata", () => {
110
+ const patched = patchEvolution(undefined, { status: "archived" });
111
+ const parsed = JSON.parse(patched);
112
+ expect(parsed.evolution.status).toBe("archived");
113
+ });
114
+
115
+ test("merges into existing evolution", () => {
116
+ const original = JSON.stringify({
117
+ source: "manual",
118
+ evolution: { status: "active", version: 1, accessCount: 5 },
119
+ });
120
+ const patched = patchEvolution(original, { accessCount: 6, lastAccessedAt: 9000 });
121
+ const parsed = JSON.parse(patched);
122
+ expect(parsed.source).toBe("manual");
123
+ expect(parsed.evolution.status).toBe("active");
124
+ expect(parsed.evolution.version).toBe(1);
125
+ expect(parsed.evolution.accessCount).toBe(6);
126
+ expect(parsed.evolution.lastAccessedAt).toBe(9000);
127
+ });
128
+
129
+ test("preserves non-evolution fields", () => {
130
+ const original = JSON.stringify({ source: "agent", tags: ["a"], canonicalKey: "k1" });
131
+ const patched = patchEvolution(original, { status: "superseded" });
132
+ const parsed = JSON.parse(patched);
133
+ expect(parsed.source).toBe("agent");
134
+ expect(parsed.tags).toEqual(["a"]);
135
+ expect(parsed.canonicalKey).toBe("k1");
136
+ });
137
+ });
138
+
139
+ // -----------------------------------------------------------------------
140
+ // isActiveMemory
141
+ // -----------------------------------------------------------------------
142
+ describe("isActiveMemory", () => {
143
+ test("true for undefined metadata (backward compat)", () => {
144
+ expect(isActiveMemory(undefined)).toBe(true);
145
+ });
146
+
147
+ test("true for metadata without evolution", () => {
148
+ expect(isActiveMemory(JSON.stringify({ source: "manual" }))).toBe(true);
149
+ });
150
+
151
+ test("true for active status", () => {
152
+ const meta = JSON.stringify({ evolution: { status: "active" } });
153
+ expect(isActiveMemory(meta)).toBe(true);
154
+ });
155
+
156
+ test("false for superseded", () => {
157
+ const meta = JSON.stringify({ evolution: { status: "superseded" } });
158
+ expect(isActiveMemory(meta)).toBe(false);
159
+ });
160
+
161
+ test("false for archived", () => {
162
+ const meta = JSON.stringify({ evolution: { status: "archived" } });
163
+ expect(isActiveMemory(meta)).toBe(false);
164
+ });
165
+
166
+ test("false for consolidated", () => {
167
+ const meta = JSON.stringify({ evolution: { status: "consolidated" } });
168
+ expect(isActiveMemory(meta)).toBe(false);
169
+ });
170
+ });
171
+
172
+ // -----------------------------------------------------------------------
173
+ // recordAccess
174
+ // -----------------------------------------------------------------------
175
+ describe("recordAccess", () => {
176
+ test("increments count from 0 on undefined metadata", () => {
177
+ const updated = recordAccess(undefined);
178
+ const evo = parseEvolution(updated);
179
+ expect(evo.accessCount).toBe(1);
180
+ expect(evo.lastAccessedAt).toBeGreaterThan(0);
181
+ });
182
+
183
+ test("increments existing count", () => {
184
+ const original = JSON.stringify({
185
+ evolution: { accessCount: 5, lastAccessedAt: 1000 },
186
+ });
187
+ const updated = recordAccess(original);
188
+ const evo = parseEvolution(updated);
189
+ expect(evo.accessCount).toBe(6);
190
+ expect(evo.lastAccessedAt!).toBeGreaterThan(1000);
191
+ });
192
+ });
193
+
194
+ // -----------------------------------------------------------------------
195
+ // buildSupersedeMetadata / buildConsolidatedMetadata / buildArchivedMetadata
196
+ // -----------------------------------------------------------------------
197
+ describe("supersede/consolidate/archive helpers", () => {
198
+ test("buildSupersedeMetadata sets status and links", () => {
199
+ const original = JSON.stringify({ source: "manual", evolution: defaultEvolution(1000) });
200
+ const result = buildSupersedeMetadata(original, "new-id-123");
201
+ const evo = parseEvolution(result);
202
+ expect(evo.status).toBe("superseded");
203
+ expect(evo.supersededBy).toBe("new-id-123");
204
+ expect(evo.validUntil).toBeGreaterThan(0);
205
+ });
206
+
207
+ test("buildConsolidatedMetadata sets status and link", () => {
208
+ const result = buildConsolidatedMetadata(undefined, "consolidated-id");
209
+ const evo = parseEvolution(result);
210
+ expect(evo.status).toBe("consolidated");
211
+ expect(evo.consolidatedInto).toBe("consolidated-id");
212
+ });
213
+
214
+ test("buildArchivedMetadata sets status", () => {
215
+ const result = buildArchivedMetadata(undefined);
216
+ const evo = parseEvolution(result);
217
+ expect(evo.status).toBe("archived");
218
+ });
219
+ });
220
+
221
+ // -----------------------------------------------------------------------
222
+ // computeDecayScore
223
+ // -----------------------------------------------------------------------
224
+ describe("computeDecayScore", () => {
225
+ test("brand new high-importance memory scores high", () => {
226
+ const now = Date.now();
227
+ const evo: EvolutionMetadata = {
228
+ ...defaultEvolution(now),
229
+ accessCount: 0,
230
+ };
231
+ const score = computeDecayScore(evo, 1.0, now);
232
+ // 0.2 * 1.0 (fresh) + 0.3 * 0 (no access) * 0.5 + 0.5 * 1.0 = 0.7 + small freq
233
+ expect(score).toBeGreaterThan(0.6);
234
+ expect(score).toBeLessThanOrEqual(1.0);
235
+ });
236
+
237
+ test("old unaccessed low-importance memory scores low", () => {
238
+ const now = Date.now();
239
+ const evo: EvolutionMetadata = {
240
+ ...defaultEvolution(now - 365 * 86_400_000), // 1 year old
241
+ accessCount: 0,
242
+ };
243
+ const score = computeDecayScore(evo, 0.1, now);
244
+ expect(score).toBeLessThan(0.2);
245
+ });
246
+
247
+ test("frequently accessed memory gets frequency boost", () => {
248
+ const now = Date.now();
249
+ const evoNoAccess: EvolutionMetadata = {
250
+ ...defaultEvolution(now - 30 * 86_400_000),
251
+ accessCount: 0,
252
+ };
253
+ const evoHighAccess: EvolutionMetadata = {
254
+ ...defaultEvolution(now - 30 * 86_400_000),
255
+ accessCount: 100,
256
+ lastAccessedAt: now - 86_400_000, // yesterday
257
+ };
258
+ const scoreNo = computeDecayScore(evoNoAccess, 0.5, now);
259
+ const scoreHigh = computeDecayScore(evoHighAccess, 0.5, now);
260
+ expect(scoreHigh).toBeGreaterThan(scoreNo);
261
+ });
262
+
263
+ test("importance dominates scoring (0.5 weight)", () => {
264
+ const now = Date.now();
265
+ const evo = defaultEvolution(now);
266
+ const lowImp = computeDecayScore(evo, 0.1, now);
267
+ const highImp = computeDecayScore(evo, 1.0, now);
268
+ expect(highImp - lowImp).toBeGreaterThan(0.3); // 0.5 * (1.0 - 0.1) = 0.45
269
+ });
270
+ });
271
+ });