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,228 +0,0 @@
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
- });
@@ -1,168 +0,0 @@
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
- });
@@ -1,131 +0,0 @@
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
- });
@@ -1,65 +0,0 @@
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
- });