recallnest 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. package/.claude-plugin/CLAUDE.md +29 -0
  2. package/.claude-plugin/marketplace.json +18 -0
  3. package/.env.example +3 -0
  4. package/.github/workflows/ci.yml +28 -0
  5. package/.mcp.json +9 -0
  6. package/CHANGELOG.md +51 -0
  7. package/CLAUDE.md +42 -0
  8. package/DEMO.md +94 -0
  9. package/EVAL.md +89 -0
  10. package/FAILURES.md +75 -0
  11. package/LICENSE +21 -0
  12. package/OPERATIONS.md +59 -0
  13. package/README.md +427 -0
  14. package/README_CN.md +425 -0
  15. package/ROADMAP.md +198 -0
  16. package/assets/recallnest-workbench.png +0 -0
  17. package/assets/ui/app.js +807 -0
  18. package/assets/ui/index.html +154 -0
  19. package/assets/ui/styles.css +605 -0
  20. package/assets/wechat-qr.jpg +0 -0
  21. package/config.json.example +38 -0
  22. package/daily-recall.sh +87 -0
  23. package/daily-reflections/2026-03-05.md +15 -0
  24. package/daily-reflections/2026-03-06.md +15 -0
  25. package/daily-reflections/2026-03-07.md +7 -0
  26. package/daily-reflections/2026-03-08.md +34 -0
  27. package/daily-reflections/2026-03-09.md +35 -0
  28. package/daily-reflections/2026-03-10.md +36 -0
  29. package/daily-reflections/2026-03-11.md +35 -0
  30. package/daily-reflections/2026-03-12.md +36 -0
  31. package/daily-reflections/2026-03-13.md +35 -0
  32. package/daily-reflections/2026-03-14.md +35 -0
  33. package/daily-reflections/2026-03-15.md +35 -0
  34. package/daily-reflections/2026-03-16.md +41 -0
  35. package/daily-reflections/2026-03-17.md +10 -0
  36. package/daily-reflections/2026-03-18.md +10 -0
  37. package/daily-reflections/2026-03-19.md +10 -0
  38. package/daily-reflections/2026-03-20.md +10 -0
  39. package/daily-reflections/2026-03-21.md +10 -0
  40. package/daily-reflections/2026-03-22.md +10 -0
  41. package/daily-reflections/2026-03-23.md +10 -0
  42. package/daily-reflections/2026-03-24.md +10 -0
  43. package/daily-reflections/2026-03-25.md +32 -0
  44. package/daily-reflections/2026-03-26.md +37 -0
  45. package/daily-reflections/2026-03-27.md +35 -0
  46. package/daily-reflections/2026-03-28.md +35 -0
  47. package/daily-reflections/2026-03-29.md +35 -0
  48. package/daily-reflections/2026-03-30.md +36 -0
  49. package/daily-reflections/2026-03-31.md +35 -0
  50. package/daily-reflections/2026-04-01.md +35 -0
  51. package/docs/api-reference.md +1291 -0
  52. package/docs/architecture.md +168 -0
  53. package/docs/memory-boundary-contract.md +226 -0
  54. package/docs/memory-categories.md +119 -0
  55. package/docs/pipeline-audit-ultramemory.md +177 -0
  56. package/docs/plans/2026-03-08-query-expansion.md +270 -0
  57. package/docs/plans/2026-03-16-continuity-layer.md +196 -0
  58. package/docs/plans/mcp-tiering-plan.md +126 -0
  59. package/docs/research/recall-quality-audit-2026-03-22.md +153 -0
  60. package/docs/self-evolution.md +198 -0
  61. package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +782 -0
  62. package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +77 -0
  63. package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +95 -0
  64. package/eval/cases.json +85 -0
  65. package/eval/continuity/case-seeds.json +90 -0
  66. package/eval/continuity/cases.json +302 -0
  67. package/eval/continuity/memory-seeds.json +26 -0
  68. package/eval/continuity/pattern-seeds.json +100 -0
  69. package/eval/continuity/results-2026-03-16.md +117 -0
  70. package/eval/continuity/results-2026-03-19.md +453 -0
  71. package/eval/continuity/runbook.md +110 -0
  72. package/eval/continuity/seed-cases.md +54 -0
  73. package/eval/reports/2026-03-06-abstract-query-baseline.md +96 -0
  74. package/eval/reports/2026-03-06-aws-ssh-baseline.md +82 -0
  75. package/eval/reports/2026-03-06-baseline.md +82 -0
  76. package/eval/reports/query-expansion.md +138 -0
  77. package/integrations/README.md +57 -0
  78. package/integrations/claude-code/README.md +133 -0
  79. package/integrations/claude-code/claude-md-snippet.md +31 -0
  80. package/integrations/claude-code/setup.sh +66 -0
  81. package/integrations/claude-code/smoke-continuity.sh +452 -0
  82. package/integrations/codex/README.md +49 -0
  83. package/integrations/codex/agents-md-snippet.md +31 -0
  84. package/integrations/codex/setup.sh +58 -0
  85. package/integrations/examples/claude-agent-sdk/memory-agent.ts +182 -0
  86. package/integrations/examples/langchain/memory-chain.py +111 -0
  87. package/integrations/examples/openai-agents-sdk/memory-agent.py +109 -0
  88. package/integrations/gemini-cli/README.md +57 -0
  89. package/integrations/gemini-cli/gemini-md-snippet.md +31 -0
  90. package/integrations/gemini-cli/setup.sh +64 -0
  91. package/integrations/shared/install-managed-block.sh +51 -0
  92. package/lm +19 -0
  93. package/package.json +50 -0
  94. package/plan-v1.2-distributable.md +105 -0
  95. package/scripts/archive-distilled-junk.ts +116 -0
  96. package/scripts/backfill-l0l1l2.ts +98 -0
  97. package/scripts/blind-scan.ts +588 -0
  98. package/scripts/cleanup-step1-exact-dups.ts +117 -0
  99. package/scripts/cleanup-step2-template-dups.ts +113 -0
  100. package/scripts/cleanup-step3-stale-content.ts +149 -0
  101. package/scripts/cleanup-step4-orphan-scopes.ts +131 -0
  102. package/scripts/delete-distilled-junk.ts +88 -0
  103. package/scripts/distill-facts.ts +305 -0
  104. package/scripts/health-check.ts +343 -0
  105. package/scripts/incremental-ingest.sh +71 -0
  106. package/scripts/mlp-health-check.ts +382 -0
  107. package/scripts/pull-claude-desktop.ts +515 -0
  108. package/scripts/re-ingest-cc.sh +66 -0
  109. package/scripts/rebalance-memory-health.ts +181 -0
  110. package/scripts/run-desktop-ingest.sh +11 -0
  111. package/scripts/start-server.sh +12 -0
  112. package/scripts/viz-dashboard.html +423 -0
  113. package/scripts/viz-export.ts +703 -0
  114. package/src/__tests__/access-tracker-novelty.test.ts +228 -0
  115. package/src/__tests__/auto-consolidation.test.ts +168 -0
  116. package/src/__tests__/auto-recall.test.ts +131 -0
  117. package/src/__tests__/batch-dedup.test.ts +65 -0
  118. package/src/__tests__/capture-engine.test.ts +1142 -0
  119. package/src/__tests__/capture-heuristic.test.ts +171 -0
  120. package/src/__tests__/cascade-forget.test.ts +210 -0
  121. package/src/__tests__/category-min-scores.test.ts +33 -0
  122. package/src/__tests__/confidence-tracker.test.ts +196 -0
  123. package/src/__tests__/conflict-advisor.test.ts +206 -0
  124. package/src/__tests__/conflict-escalation.test.ts +149 -0
  125. package/src/__tests__/conflict-lifecycle.test.ts +83 -0
  126. package/src/__tests__/conflict-output.test.ts +85 -0
  127. package/src/__tests__/conflict-store.test.ts +374 -0
  128. package/src/__tests__/consolidation-engine.test.ts +181 -0
  129. package/src/__tests__/context-composer-pins.test.ts +64 -0
  130. package/src/__tests__/context-composer-task-selection.test.ts +24 -0
  131. package/src/__tests__/context-renderer.test.ts +91 -0
  132. package/src/__tests__/continuity-eval.test.ts +275 -0
  133. package/src/__tests__/continuity-schema.test.ts +97 -0
  134. package/src/__tests__/core-recall-eval.test.ts +235 -0
  135. package/src/__tests__/core-summary.test.ts +109 -0
  136. package/src/__tests__/dedup-actions.test.ts +58 -0
  137. package/src/__tests__/deterministic-id.test.ts +39 -0
  138. package/src/__tests__/doctor.test.ts +303 -0
  139. package/src/__tests__/drill-down.test.ts +108 -0
  140. package/src/__tests__/embedder.test.ts +122 -0
  141. package/src/__tests__/entity-resolver.test.ts +58 -0
  142. package/src/__tests__/event-segmenter.test.ts +153 -0
  143. package/src/__tests__/hotness-blend.test.ts +76 -0
  144. package/src/__tests__/ingest.test.ts +360 -0
  145. package/src/__tests__/kg-extractor.test.ts +184 -0
  146. package/src/__tests__/kg-store.test.ts +198 -0
  147. package/src/__tests__/llm-consolidation.test.ts +226 -0
  148. package/src/__tests__/memory-boundaries.test.ts +237 -0
  149. package/src/__tests__/memory-health-rebalance.test.ts +144 -0
  150. package/src/__tests__/memory-output.test.ts +215 -0
  151. package/src/__tests__/multi-vector.test.ts +242 -0
  152. package/src/__tests__/ppr-traversal.test.ts +256 -0
  153. package/src/__tests__/preference-guard-smoke.test.ts +211 -0
  154. package/src/__tests__/preference-matcher.test.ts +230 -0
  155. package/src/__tests__/preference-slots.test.ts +106 -0
  156. package/src/__tests__/prospective-memory.test.ts +283 -0
  157. package/src/__tests__/query-entity-detector.test.ts +171 -0
  158. package/src/__tests__/query-expander.test.ts +34 -0
  159. package/src/__tests__/recall-mode.test.ts +64 -0
  160. package/src/__tests__/result-synthesizer.test.ts +198 -0
  161. package/src/__tests__/resume-context.test.ts +4743 -0
  162. package/src/__tests__/retrieval-trace.test.ts +213 -0
  163. package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
  164. package/src/__tests__/retroactive-boost.test.ts +223 -0
  165. package/src/__tests__/scope-inventory.test.ts +200 -0
  166. package/src/__tests__/scope-policy.test.ts +69 -0
  167. package/src/__tests__/session-checkpoint.test.ts +259 -0
  168. package/src/__tests__/session-gc.test.ts +215 -0
  169. package/src/__tests__/store-scope-audit.test.ts +81 -0
  170. package/src/__tests__/temporal-parser.test.ts +241 -0
  171. package/src/__tests__/term-registry.test.ts +121 -0
  172. package/src/__tests__/tool-output-compressor.test.ts +133 -0
  173. package/src/__tests__/version-manager.test.ts +213 -0
  174. package/src/__tests__/workflow-observation.test.ts +400 -0
  175. package/src/access-tracker.ts +316 -0
  176. package/src/adaptive-retrieval.ts +64 -0
  177. package/src/api-server.ts +667 -0
  178. package/src/asset-sync.ts +79 -0
  179. package/src/auto-consolidation.ts +133 -0
  180. package/src/auto-gc.ts +124 -0
  181. package/src/auto-recall.ts +95 -0
  182. package/src/capture-engine.ts +931 -0
  183. package/src/capture-heuristic.ts +177 -0
  184. package/src/cascade-forget.ts +135 -0
  185. package/src/chunker.ts +253 -0
  186. package/src/cli.ts +1917 -0
  187. package/src/confidence-tracker.ts +166 -0
  188. package/src/conflict-advisor.ts +433 -0
  189. package/src/conflict-engine.ts +181 -0
  190. package/src/conflict-escalation.ts +138 -0
  191. package/src/conflict-lifecycle.ts +67 -0
  192. package/src/conflict-output.ts +218 -0
  193. package/src/conflict-schema.ts +130 -0
  194. package/src/conflict-store.ts +127 -0
  195. package/src/consolidation-engine.ts +256 -0
  196. package/src/context-composer-pins.ts +183 -0
  197. package/src/context-composer-queries.ts +121 -0
  198. package/src/context-composer-scope.ts +56 -0
  199. package/src/context-composer-stable-selection.ts +200 -0
  200. package/src/context-composer-stable.ts +217 -0
  201. package/src/context-composer-task-fallbacks.ts +36 -0
  202. package/src/context-composer-task-ranking.ts +197 -0
  203. package/src/context-composer-task-results.ts +137 -0
  204. package/src/context-composer-task-selection.ts +342 -0
  205. package/src/context-composer-text.ts +57 -0
  206. package/src/context-composer.ts +300 -0
  207. package/src/context-renderer.ts +179 -0
  208. package/src/decay-engine.ts +189 -0
  209. package/src/doctor.ts +543 -0
  210. package/src/embedder.ts +557 -0
  211. package/src/entity-resolver.ts +131 -0
  212. package/src/eval.ts +632 -0
  213. package/src/event-segmenter.ts +216 -0
  214. package/src/ingest.ts +1822 -0
  215. package/src/kg-extractor.ts +217 -0
  216. package/src/kg-store.ts +353 -0
  217. package/src/legacy-scope-review.ts +53 -0
  218. package/src/llm-client.ts +629 -0
  219. package/src/llm-consolidation.ts +168 -0
  220. package/src/mcp-server.ts +1337 -0
  221. package/src/memory-assets.ts +475 -0
  222. package/src/memory-boundaries.ts +354 -0
  223. package/src/memory-health-rebalance.ts +220 -0
  224. package/src/memory-output.ts +474 -0
  225. package/src/memory-schema.ts +184 -0
  226. package/src/multi-vector.ts +234 -0
  227. package/src/noise-filter.ts +155 -0
  228. package/src/ppr-traversal.ts +279 -0
  229. package/src/preference-matcher.ts +174 -0
  230. package/src/preference-slots.ts +259 -0
  231. package/src/prospective-memory.ts +198 -0
  232. package/src/query-entity-detector.ts +207 -0
  233. package/src/query-expander.ts +98 -0
  234. package/src/result-synthesizer.ts +114 -0
  235. package/src/retrieval-profiles.ts +101 -0
  236. package/src/retrieval-trace.ts +94 -0
  237. package/src/retriever.ts +1338 -0
  238. package/src/retroactive-boost.ts +154 -0
  239. package/src/rif.ts +73 -0
  240. package/src/runtime-config.ts +172 -0
  241. package/src/schema-utils.ts +56 -0
  242. package/src/scope-inventory.ts +310 -0
  243. package/src/scope-policy.ts +142 -0
  244. package/src/session-engine.ts +168 -0
  245. package/src/session-output.ts +96 -0
  246. package/src/session-schema.ts +60 -0
  247. package/src/session-store.ts +293 -0
  248. package/src/stderr-log.ts +16 -0
  249. package/src/store.ts +903 -0
  250. package/src/temporal-parser.ts +318 -0
  251. package/src/term-registry.ts +791 -0
  252. package/src/tool-output-compressor.ts +115 -0
  253. package/src/tracker.ts +57 -0
  254. package/src/ui-server.ts +430 -0
  255. package/src/version-manager.ts +172 -0
  256. package/src/workflow-observation-engine.ts +316 -0
  257. package/src/workflow-observation-managed.ts +83 -0
  258. package/src/workflow-observation-output.ts +86 -0
  259. package/src/workflow-observation-schema.ts +87 -0
  260. package/src/workflow-observation-scope-review.ts +409 -0
  261. package/src/workflow-observation-store.ts +69 -0
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Tests for Tier 3.2: Conditional access reinforcement (prediction error gating).
3
+ *
4
+ * Validates that:
5
+ * 1. High-similarity (low-novelty) results are NOT reinforced
6
+ * 2. Low-similarity (high-novelty) results ARE reinforced
7
+ * 3. Cooldown prevents duplicate reinforcement within the window
8
+ * 4. When no scores provided, all entries are reinforced (backward compat)
9
+ * 5. shouldReinforce() logic is correct in isolation
10
+ */
11
+ import { describe, expect, it, beforeEach } from "bun:test";
12
+ import { AccessTracker, DEFAULT_ACCESS_TRACKER_CONFIG } from "../access-tracker.js";
13
+ import type { MemoryEntry } from "../store.js";
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Helpers
17
+ // ---------------------------------------------------------------------------
18
+
19
+ function makeEntry(id: string, overrides: Partial<MemoryEntry> = {}): MemoryEntry {
20
+ return {
21
+ id,
22
+ text: `memory-${id}`,
23
+ vector: [1, 0, 0],
24
+ category: "events",
25
+ scope: "project:test",
26
+ importance: 0.7,
27
+ timestamp: Date.now(),
28
+ metadata: JSON.stringify({ accessCount: 0, lastAccessedAt: 0 }),
29
+ ...overrides,
30
+ };
31
+ }
32
+
33
+ function createMockStore(entries: MemoryEntry[]) {
34
+ const data = new Map(entries.map(e => [e.id, { ...e }]));
35
+ const updates: Array<{ id: string; metadata?: string }> = [];
36
+
37
+ return {
38
+ updates,
39
+ data,
40
+ async getById(id: string) {
41
+ return data.get(id) ?? null;
42
+ },
43
+ async update(id: string, upd: { metadata?: string }) {
44
+ const entry = data.get(id);
45
+ if (!entry) return null;
46
+ if (upd.metadata) {
47
+ entry.metadata = upd.metadata;
48
+ updates.push({ id, metadata: upd.metadata });
49
+ }
50
+ return entry;
51
+ },
52
+ };
53
+ }
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // shouldReinforce() unit tests
57
+ // ---------------------------------------------------------------------------
58
+
59
+ describe("shouldReinforce", () => {
60
+ let tracker: AccessTracker;
61
+
62
+ beforeEach(() => {
63
+ tracker = new AccessTracker(
64
+ createMockStore([]) as any,
65
+ { ...DEFAULT_ACCESS_TRACKER_CONFIG, noveltyThreshold: 0.35, cooldownMs: 300_000 },
66
+ );
67
+ });
68
+
69
+ it("rejects high-similarity (low-novelty) results", () => {
70
+ // similarity 0.90 → novelty 0.10 < threshold 0.35 → reject
71
+ expect(tracker.shouldReinforce("a", 0.90)).toBe(false);
72
+ });
73
+
74
+ it("rejects exact match (similarity 1.0)", () => {
75
+ expect(tracker.shouldReinforce("a", 1.0)).toBe(false);
76
+ });
77
+
78
+ it("accepts low-similarity (high-novelty) results", () => {
79
+ // similarity 0.50 → novelty 0.50 > threshold 0.35 → accept
80
+ expect(tracker.shouldReinforce("a", 0.50)).toBe(true);
81
+ });
82
+
83
+ it("accepts borderline novelty (exactly at threshold)", () => {
84
+ // similarity 0.65 → novelty 0.35 = threshold 0.35 → accept (not strictly less)
85
+ expect(tracker.shouldReinforce("a", 0.65)).toBe(true);
86
+ });
87
+
88
+ it("rejects same entry within cooldown window", () => {
89
+ // First call: accepted
90
+ expect(tracker.shouldReinforce("a", 0.50)).toBe(true);
91
+ // Simulate recording to set cooldown
92
+ tracker.recordAccess(["a"], [0.50]);
93
+ // Second call within cooldown: rejected
94
+ expect(tracker.shouldReinforce("a", 0.50)).toBe(false);
95
+ });
96
+
97
+ it("accepts different entry even if another is on cooldown", () => {
98
+ tracker.recordAccess(["a"], [0.50]);
99
+ expect(tracker.shouldReinforce("b", 0.50)).toBe(true);
100
+ });
101
+
102
+ it("bypasses novelty gate when threshold is 0", () => {
103
+ const noGateTracker = new AccessTracker(
104
+ createMockStore([]) as any,
105
+ { ...DEFAULT_ACCESS_TRACKER_CONFIG, noveltyThreshold: 0, cooldownMs: 0 },
106
+ );
107
+ // Even high similarity passes
108
+ expect(noGateTracker.shouldReinforce("a", 0.99)).toBe(true);
109
+ });
110
+ });
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // recordAccess() with scores
114
+ // ---------------------------------------------------------------------------
115
+
116
+ describe("recordAccess with novelty gating", () => {
117
+ it("only queues novel entries for flush", async () => {
118
+ const entries = [makeEntry("a"), makeEntry("b"), makeEntry("c")];
119
+ const store = createMockStore(entries);
120
+ const tracker = new AccessTracker(store as any, {
121
+ ...DEFAULT_ACCESS_TRACKER_CONFIG,
122
+ flushIntervalMs: 0, // immediate flush for testing
123
+ noveltyThreshold: 0.35,
124
+ cooldownMs: 0,
125
+ });
126
+
127
+ // scores: a=0.90 (low novelty, reject), b=0.50 (novel, accept), c=0.40 (novel, accept)
128
+ tracker.recordAccess(["a", "b", "c"], [0.90, 0.50, 0.40]);
129
+
130
+ await tracker.flush();
131
+
132
+ // Only b and c should have been updated
133
+ const updatedIds = store.updates.map(u => u.id);
134
+ expect(updatedIds).not.toContain("a");
135
+ expect(updatedIds).toContain("b");
136
+ expect(updatedIds).toContain("c");
137
+ });
138
+
139
+ it("reinforces all entries when no scores provided (backward compat)", async () => {
140
+ const entries = [makeEntry("a"), makeEntry("b")];
141
+ const store = createMockStore(entries);
142
+ const tracker = new AccessTracker(store as any, {
143
+ ...DEFAULT_ACCESS_TRACKER_CONFIG,
144
+ flushIntervalMs: 0,
145
+ noveltyThreshold: 0.35,
146
+ cooldownMs: 0,
147
+ });
148
+
149
+ // No scores → all reinforced
150
+ tracker.recordAccess(["a", "b"]);
151
+ await tracker.flush();
152
+
153
+ const updatedIds = store.updates.map(u => u.id);
154
+ expect(updatedIds).toContain("a");
155
+ expect(updatedIds).toContain("b");
156
+ });
157
+
158
+ it("respects cooldown across multiple recordAccess calls", async () => {
159
+ const entries = [makeEntry("x")];
160
+ const store = createMockStore(entries);
161
+ const tracker = new AccessTracker(store as any, {
162
+ ...DEFAULT_ACCESS_TRACKER_CONFIG,
163
+ flushIntervalMs: 0,
164
+ noveltyThreshold: 0.35,
165
+ cooldownMs: 600_000, // 10 min cooldown
166
+ });
167
+
168
+ // First call: novel enough → queued
169
+ tracker.recordAccess(["x"], [0.50]);
170
+ await tracker.flush();
171
+ expect(store.updates.length).toBe(1);
172
+
173
+ // Second call: same entry within cooldown → not queued
174
+ tracker.recordAccess(["x"], [0.50]);
175
+ await tracker.flush();
176
+ // Still only 1 update (no new flush)
177
+ expect(store.updates.length).toBe(1);
178
+ });
179
+
180
+ it("skips flush scheduling when all entries are gated out", () => {
181
+ const store = createMockStore([]);
182
+ const tracker = new AccessTracker(store as any, {
183
+ ...DEFAULT_ACCESS_TRACKER_CONFIG,
184
+ flushIntervalMs: 5000,
185
+ noveltyThreshold: 0.35,
186
+ cooldownMs: 0,
187
+ });
188
+
189
+ // All scores too high (low novelty) → nothing queued
190
+ tracker.recordAccess(["a", "b"], [0.95, 0.90]);
191
+ expect(tracker.pendingCount).toBe(0);
192
+ });
193
+
194
+ it("accessCount increments correctly after novelty-gated flush", async () => {
195
+ const entries = [makeEntry("m1")];
196
+ const store = createMockStore(entries);
197
+ const tracker = new AccessTracker(store as any, {
198
+ ...DEFAULT_ACCESS_TRACKER_CONFIG,
199
+ flushIntervalMs: 0,
200
+ noveltyThreshold: 0.35,
201
+ cooldownMs: 0,
202
+ });
203
+
204
+ tracker.recordAccess(["m1"], [0.40]); // novel → accepted
205
+ await tracker.flush();
206
+
207
+ const updated = store.data.get("m1")!;
208
+ const meta = JSON.parse(updated.metadata);
209
+ expect(meta.accessCount).toBe(1);
210
+ });
211
+ });
212
+
213
+ // ---------------------------------------------------------------------------
214
+ // destroy() cleans cooldown
215
+ // ---------------------------------------------------------------------------
216
+
217
+ describe("destroy cleans cooldown", () => {
218
+ it("clears cooldown map on destroy", () => {
219
+ const tracker = new AccessTracker(
220
+ createMockStore([]) as any,
221
+ { ...DEFAULT_ACCESS_TRACKER_CONFIG, cooldownMs: 600_000 },
222
+ );
223
+ tracker.recordAccess(["a"], [0.40]);
224
+ tracker.destroy();
225
+ // After destroy, same entry should be reinforceable again
226
+ expect(tracker.shouldReinforce("a", 0.40)).toBe(true);
227
+ });
228
+ });
@@ -0,0 +1,168 @@
1
+ import { describe, expect, it, beforeEach } from "bun:test";
2
+
3
+ import {
4
+ maybeConsolidate,
5
+ resetConsolidationState,
6
+ DEFAULT_AUTO_CONSOLIDATION_CONFIG,
7
+ type AutoConsolidationConfig,
8
+ } from "../auto-consolidation.js";
9
+ import type { MemoryEntry, MemorySearchResult } from "../store.js";
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Helpers
13
+ // ---------------------------------------------------------------------------
14
+
15
+ function makeEntry(id: string, text: string, overrides: Partial<MemoryEntry> = {}): MemoryEntry {
16
+ return {
17
+ id,
18
+ text,
19
+ vector: [1, 0, 0],
20
+ category: "events",
21
+ scope: "project:test",
22
+ importance: 0.7,
23
+ timestamp: Date.now(),
24
+ metadata: "{}",
25
+ ...overrides,
26
+ };
27
+ }
28
+
29
+ function createMockStore(entries: MemoryEntry[]) {
30
+ const data = new Map(entries.map(e => [e.id, { ...e }]));
31
+ return {
32
+ async list(scopeFilter?: string[], _category?: string, limit = 500, _offset = 0) {
33
+ return [...data.values()]
34
+ .filter(e => !scopeFilter || scopeFilter.some(s => e.scope === s))
35
+ .slice(0, limit);
36
+ },
37
+ async getById(id: string) {
38
+ return data.get(id) ?? null;
39
+ },
40
+ async vectorSearch(_vector: number[], _limit = 5, _minScore = 0.3, _scopeFilter?: string[]) {
41
+ return [] as MemorySearchResult[];
42
+ },
43
+ async update(id: string, updates: any, _scopeFilter?: string[]) {
44
+ const entry = data.get(id);
45
+ if (!entry) return null;
46
+ Object.assign(entry, updates);
47
+ return entry;
48
+ },
49
+ async stats(scopeFilter?: string[]) {
50
+ const filtered = [...data.values()].filter(
51
+ e => !scopeFilter || scopeFilter.some(s => e.scope === s),
52
+ );
53
+ return { total: filtered.length, byCategory: {}, byScope: {} };
54
+ },
55
+ hasFtsSupport: false,
56
+ } as any;
57
+ }
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // Tests
61
+ // ---------------------------------------------------------------------------
62
+
63
+ describe("auto-consolidation", () => {
64
+ beforeEach(() => {
65
+ resetConsolidationState();
66
+ });
67
+
68
+ it("skips when not enough new memories", async () => {
69
+ const entries = Array.from({ length: 10 }, (_, i) =>
70
+ makeEntry(`id-${i}`, `memory ${i}`),
71
+ );
72
+ const store = createMockStore(entries);
73
+
74
+ const result = await maybeConsolidate(store, "project:test");
75
+ expect(result.triggered).toBe(false);
76
+ expect(result.reason).toBe("insufficient_new_memories");
77
+ });
78
+
79
+ it("skips when too soon since last run", async () => {
80
+ // Start with 60 entries, trigger first run, then grow to 120 so
81
+ // the memory-count gate passes but the time gate blocks.
82
+ const data: MemoryEntry[] = Array.from({ length: 60 }, (_, i) =>
83
+ makeEntry(`id-${i}`, `memory ${i}`),
84
+ );
85
+ const store = createMockStore(data);
86
+
87
+ const config: AutoConsolidationConfig = {
88
+ minNewMemories: 50,
89
+ minHoursSinceLastRun: 12,
90
+ consolidation: DEFAULT_AUTO_CONSOLIDATION_CONFIG.consolidation,
91
+ };
92
+
93
+ // First run: triggers (never run before, 60 > 50 new memories)
94
+ const first = await maybeConsolidate(store, "project:test", config);
95
+ expect(first.triggered).toBe(true);
96
+
97
+ // Grow the store so memory-count gate would pass
98
+ for (let i = 60; i < 120; i++) {
99
+ data.push(makeEntry(`id-${i}`, `memory ${i}`));
100
+ }
101
+ const bigStore = createMockStore(data);
102
+
103
+ // Second run immediately: time gate blocks (< 12 hours)
104
+ const second = await maybeConsolidate(bigStore, "project:test", config);
105
+ expect(second.triggered).toBe(false);
106
+ expect(second.reason).toBe("too_soon");
107
+ });
108
+
109
+ it("triggers when both conditions met", async () => {
110
+ const entries = Array.from({ length: 60 }, (_, i) =>
111
+ makeEntry(`id-${i}`, `memory ${i}`),
112
+ );
113
+ const store = createMockStore(entries);
114
+
115
+ const config: AutoConsolidationConfig = {
116
+ minNewMemories: 50,
117
+ minHoursSinceLastRun: 0, // disable time gate for test
118
+ consolidation: {
119
+ clusterThreshold: 0.82,
120
+ mergeThreshold: 0.92,
121
+ maxEntriesPerRun: 500,
122
+ },
123
+ };
124
+
125
+ const result = await maybeConsolidate(store, "project:test", config);
126
+ expect(result.triggered).toBe(true);
127
+ expect(result.consolidation).toBeDefined();
128
+ expect(result.consolidation!.scope).toBe("project:test");
129
+ });
130
+
131
+ it("returns consolidation result when triggered", async () => {
132
+ const entries = Array.from({ length: 60 }, (_, i) =>
133
+ makeEntry(`id-${i}`, `memory ${i}`),
134
+ );
135
+ const store = createMockStore(entries);
136
+
137
+ const config: AutoConsolidationConfig = {
138
+ minNewMemories: 50,
139
+ minHoursSinceLastRun: 0,
140
+ consolidation: {
141
+ clusterThreshold: 0.82,
142
+ mergeThreshold: 0.92,
143
+ maxEntriesPerRun: 500,
144
+ },
145
+ };
146
+
147
+ const result = await maybeConsolidate(store, "project:test", config);
148
+ expect(result.triggered).toBe(true);
149
+ expect(result.consolidation!.originalCount).toBe(60);
150
+ });
151
+
152
+ it("respects custom thresholds", async () => {
153
+ const entries = Array.from({ length: 200 }, (_, i) =>
154
+ makeEntry(`id-${i}`, `memory ${i}`),
155
+ );
156
+ const store = createMockStore(entries);
157
+
158
+ const config: AutoConsolidationConfig = {
159
+ minNewMemories: 300, // higher than available
160
+ minHoursSinceLastRun: 0,
161
+ consolidation: DEFAULT_AUTO_CONSOLIDATION_CONFIG.consolidation,
162
+ };
163
+
164
+ const result = await maybeConsolidate(store, "project:test", config);
165
+ expect(result.triggered).toBe(false);
166
+ expect(result.reason).toBe("insufficient_new_memories");
167
+ });
168
+ });
@@ -0,0 +1,131 @@
1
+ import { describe, expect, it } from "bun:test";
2
+
3
+ import { runAutoRecall } from "../auto-recall.js";
4
+ import type { RetrievalContext, RetrievalResult } from "../retriever.js";
5
+ import type { SessionCheckpointRecord } from "../session-schema.js";
6
+
7
+ function buildResult(id: string, category: "profile" | "preferences" | "entities" | "patterns" | "cases" | "events", text: string, scope = "project:test"): RetrievalResult {
8
+ return {
9
+ entry: {
10
+ id,
11
+ text,
12
+ vector: [],
13
+ category,
14
+ scope,
15
+ importance: 0.8,
16
+ timestamp: Date.parse("2026-03-17T00:00:00.000Z"),
17
+ metadata: "{}",
18
+ },
19
+ score: 0.9,
20
+ sources: {
21
+ fused: { score: 0.9 },
22
+ },
23
+ };
24
+ }
25
+
26
+ describe("runAutoRecall", () => {
27
+ it("reuses an explicit scope for focused recall after composing resume context", async () => {
28
+ const calls: RetrievalContext[] = [];
29
+ const retriever = {
30
+ async retrieve(context: RetrievalContext): Promise<RetrievalResult[]> {
31
+ calls.push(context);
32
+ if (context.category === "profile") {
33
+ return [buildResult("profile-1", "profile", "Profile: RecallNest maintainer.", "project:alpha")];
34
+ }
35
+ if (!context.category) {
36
+ return [buildResult("event-1", "events", "RecallNest uses Weibull decay.", "project:alpha")];
37
+ }
38
+ return [];
39
+ },
40
+ };
41
+
42
+ const response = await runAutoRecall({
43
+ retriever,
44
+ checkpointStore: { async getLatest() { return null; } },
45
+ listPins: () => [],
46
+ }, {
47
+ message: "那个衰减曲线用的什么公式来着",
48
+ scope: "project:alpha",
49
+ limit: 5,
50
+ profile: "default",
51
+ operation: "test:auto-recall",
52
+ });
53
+
54
+ expect(response.mode).toBe("resume+search");
55
+ expect(response.resolvedScope).toBe("project:alpha");
56
+ expect(response.results[0]?.entry.id).toBe("event-1");
57
+ const searchCall = calls.find((call) => !call.category);
58
+ expect(searchCall?.scopeFilter).toEqual(["project:alpha"]);
59
+ expect(searchCall?.source).toBe("auto-recall");
60
+ });
61
+
62
+ it("uses the checkpoint-resolved scope when the request only carries sessionId", async () => {
63
+ const calls: RetrievalContext[] = [];
64
+ const checkpoint: SessionCheckpointRecord = {
65
+ checkpointId: "checkpoint-1",
66
+ sessionId: "session-123",
67
+ resolvedScope: "project:recallnest",
68
+ summary: "Continue proactive recall work.",
69
+ task: "Implement auto recall route",
70
+ decisions: [],
71
+ openLoops: [],
72
+ nextActions: [],
73
+ entities: [],
74
+ files: [],
75
+ updatedAt: "2026-03-17T10:00:00.000Z",
76
+ };
77
+
78
+ const retriever = {
79
+ async retrieve(context: RetrievalContext): Promise<RetrievalResult[]> {
80
+ calls.push(context);
81
+ if (!context.category) {
82
+ return [buildResult("event-1", "events", "Add a true auto recall route.", "project:recallnest")];
83
+ }
84
+ return [];
85
+ },
86
+ };
87
+
88
+ const response = await runAutoRecall({
89
+ retriever,
90
+ checkpointStore: {
91
+ async getLatest(query?: { sessionId?: string }) {
92
+ return query?.sessionId === "session-123" ? checkpoint : null;
93
+ },
94
+ },
95
+ listPins: () => [],
96
+ }, {
97
+ message: "继续做主动 recall",
98
+ sessionId: "session-123",
99
+ operation: "test:auto-recall",
100
+ });
101
+
102
+ expect(response.resolvedScope).toBe("project:recallnest");
103
+ expect(response.resume.latestCheckpoint?.resolvedScope).toBe("project:recallnest");
104
+ const searchCall = calls.find((call) => !call.category);
105
+ expect(searchCall?.scopeFilter).toEqual(["project:recallnest"]);
106
+ });
107
+
108
+ it("falls back to resume-only mode when no scope can be inferred for focused search", async () => {
109
+ const calls: RetrievalContext[] = [];
110
+ const retriever = {
111
+ async retrieve(context: RetrievalContext): Promise<RetrievalResult[]> {
112
+ calls.push(context);
113
+ return [];
114
+ },
115
+ };
116
+
117
+ const response = await runAutoRecall({
118
+ retriever,
119
+ checkpointStore: { async getLatest() { return null; } },
120
+ listPins: () => [],
121
+ }, {
122
+ message: "what do you remember about this",
123
+ operation: "test:auto-recall",
124
+ });
125
+
126
+ expect(response.mode).toBe("resume-only");
127
+ expect(response.results).toHaveLength(0);
128
+ expect(response.searchSkippedReason).toContain("No explicit or inferred scope");
129
+ expect(calls.some((call) => !call.category)).toBe(false);
130
+ });
131
+ });
@@ -0,0 +1,65 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { batchInternalDedup } from "../ingest.js";
3
+
4
+ describe("batchInternalDedup", () => {
5
+ it("keeps all entries when vectors are dissimilar", () => {
6
+ const vectors = [
7
+ [1, 0, 0],
8
+ [0, 1, 0],
9
+ [0, 0, 1],
10
+ ];
11
+ const importances = [0.8, 0.7, 0.6];
12
+ const kept = batchInternalDedup(vectors, importances);
13
+ expect(kept).toEqual([0, 1, 2]);
14
+ });
15
+
16
+ it("drops lower-importance duplicate when vectors are near-identical", () => {
17
+ const vectors = [
18
+ [1, 0, 0], // importance 0.6
19
+ [1, 0.01, 0], // importance 0.9 — nearly identical to [0]
20
+ [0, 1, 0], // importance 0.7 — dissimilar
21
+ ];
22
+ const importances = [0.6, 0.9, 0.7];
23
+ const kept = batchInternalDedup(vectors, importances);
24
+ // [1] has higher importance and is near-identical to [0], so [0] gets dropped
25
+ expect(kept).toContain(1);
26
+ expect(kept).toContain(2);
27
+ expect(kept).not.toContain(0);
28
+ });
29
+
30
+ it("returns single-item batch unchanged", () => {
31
+ expect(batchInternalDedup([[1, 0, 0]], [0.5])).toEqual([0]);
32
+ });
33
+
34
+ it("returns empty for empty input", () => {
35
+ expect(batchInternalDedup([], [])).toEqual([]);
36
+ });
37
+
38
+ it("preserves original order in output", () => {
39
+ const vectors = [
40
+ [0, 1, 0], // idx 0, imp 0.9
41
+ [1, 0, 0], // idx 1, imp 0.5
42
+ [0, 0, 1], // idx 2, imp 0.7
43
+ ];
44
+ const importances = [0.9, 0.5, 0.7];
45
+ const kept = batchInternalDedup(vectors, importances);
46
+ // All dissimilar, all kept, in original order
47
+ expect(kept).toEqual([0, 1, 2]);
48
+ });
49
+
50
+ it("respects custom threshold", () => {
51
+ const vectors = [
52
+ [1, 0, 0],
53
+ [0.95, 0.31, 0], // cosine ~0.95 with [0]
54
+ ];
55
+ const importances = [0.8, 0.7];
56
+
57
+ // Default threshold 0.85 → should dedup
58
+ const strictKept = batchInternalDedup(vectors, importances, 0.85);
59
+ expect(strictKept).toHaveLength(1);
60
+
61
+ // Loose threshold 0.99 → should keep both
62
+ const looseKept = batchInternalDedup(vectors, importances, 0.99);
63
+ expect(looseKept).toHaveLength(2);
64
+ });
65
+ });