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,215 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, utimesSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+
6
+ import { buildSessionCheckpointRecord } from "../session-engine.js";
7
+ import { SessionCheckpointStore, classifyCheckpointQuality } from "../session-store.js";
8
+
9
+ const FALLBACK_SUMMARY = "Checkpoint captured current task state without repo-state details.";
10
+ const NOW = new Date("2026-04-01T00:00:00.000Z");
11
+
12
+ function daysBeforeNow(days: number): string {
13
+ return new Date(NOW.getTime() - days * 86_400_000).toISOString();
14
+ }
15
+
16
+ function makeRichCheckpoint(overrides: Record<string, unknown> = {}) {
17
+ return buildSessionCheckpointRecord({
18
+ sessionId: overrides.sessionId ?? crypto.randomUUID(),
19
+ scope: overrides.scope ?? "project:test",
20
+ summary: overrides.summary ?? "Rich checkpoint with real content about the project status.",
21
+ decisions: overrides.decisions ?? ["decision-a"],
22
+ openLoops: overrides.openLoops ?? ["loop-a"],
23
+ nextActions: overrides.nextActions ?? ["action-a"],
24
+ entities: [],
25
+ files: [],
26
+ updatedAt: overrides.updatedAt ?? daysBeforeNow(1),
27
+ });
28
+ }
29
+
30
+ function makeMinimalCheckpoint(overrides: Record<string, unknown> = {}) {
31
+ return buildSessionCheckpointRecord({
32
+ sessionId: overrides.sessionId ?? crypto.randomUUID(),
33
+ scope: overrides.scope ?? "project:test",
34
+ summary: FALLBACK_SUMMARY,
35
+ decisions: [],
36
+ openLoops: [],
37
+ nextActions: [],
38
+ entities: [],
39
+ files: [],
40
+ updatedAt: overrides.updatedAt ?? daysBeforeNow(1),
41
+ });
42
+ }
43
+
44
+ function createTempStore(): { store: SessionCheckpointStore; dir: string; cleanup: () => void } {
45
+ const dir = mkdtempSync(join(tmpdir(), "rn-gc-test-"));
46
+ const checkpointDir = join(dir, "session-checkpoints");
47
+ mkdirSync(checkpointDir, { recursive: true });
48
+ const store = new SessionCheckpointStore(checkpointDir);
49
+ return {
50
+ store,
51
+ dir,
52
+ cleanup: () => rmSync(dir, { recursive: true, force: true }),
53
+ };
54
+ }
55
+
56
+ describe("checkpoint gc - retention rules", () => {
57
+ it("keeps latest N rich checkpoints per scope and archives older ones", async () => {
58
+ const { store, cleanup } = createTempStore();
59
+ try {
60
+ // 7 rich checkpoints for same scope, spread across 5-50 days
61
+ const ages = [5, 10, 15, 25, 35, 42, 50];
62
+ for (const age of ages) {
63
+ await store.save(makeRichCheckpoint({ updatedAt: daysBeforeNow(age) }));
64
+ }
65
+
66
+ const result = store.gc({ now: NOW, keepPerScope: 5, archiveAfterDays: 30 });
67
+ expect(result.kept).toBe(5);
68
+ expect(result.archived).toBe(2); // 42d and 50d
69
+ expect(result.deleted).toBe(0);
70
+ expect(result.errors).toHaveLength(0);
71
+ } finally {
72
+ cleanup();
73
+ }
74
+ });
75
+
76
+ it("keeps latest checkpoint per session if within 48 hours", async () => {
77
+ const { store, cleanup } = createTempStore();
78
+ try {
79
+ const sessionA = crypto.randomUUID();
80
+ const sessionB = crypto.randomUUID();
81
+
82
+ // sessionA: 1 hour old minimal — should be protected by session TTL
83
+ await store.save(makeMinimalCheckpoint({ sessionId: sessionA, updatedAt: daysBeforeNow(0.04) }));
84
+ // sessionB: 10 days old minimal — no session TTL protection, >7 days → delete
85
+ await store.save(makeMinimalCheckpoint({ sessionId: sessionB, updatedAt: daysBeforeNow(10) }));
86
+
87
+ const result = store.gc({ now: NOW, sessionTtlHours: 48, minimalTtlDays: 7 });
88
+ expect(result.kept).toBe(1); // sessionA protected
89
+ expect(result.deleted).toBe(1); // sessionB expired minimal
90
+ } finally {
91
+ cleanup();
92
+ }
93
+ });
94
+
95
+ it("deletes minimal checkpoints older than 7 days", async () => {
96
+ const { store, cleanup } = createTempStore();
97
+ try {
98
+ await store.save(makeMinimalCheckpoint({ updatedAt: daysBeforeNow(3) }));
99
+ await store.save(makeMinimalCheckpoint({ updatedAt: daysBeforeNow(8) }));
100
+ await store.save(makeMinimalCheckpoint({ updatedAt: daysBeforeNow(15) }));
101
+
102
+ const result = store.gc({ now: NOW, minimalTtlDays: 7 });
103
+ expect(result.kept).toBe(1); // 3d old
104
+ expect(result.deleted).toBe(2); // 8d and 15d old
105
+ } finally {
106
+ cleanup();
107
+ }
108
+ });
109
+
110
+ it("archives rich checkpoints older than 30 days not in latest-per-scope", async () => {
111
+ const { store, dir, cleanup } = createTempStore();
112
+ try {
113
+ const ages = [5, 10, 20, 35, 40, 50];
114
+ for (const age of ages) {
115
+ await store.save(makeRichCheckpoint({ updatedAt: daysBeforeNow(age) }));
116
+ }
117
+
118
+ const result = store.gc({ now: NOW, keepPerScope: 3, archiveAfterDays: 30 });
119
+ expect(result.kept).toBe(3); // 5d, 10d, 20d
120
+ expect(result.archived).toBe(3); // 35d, 40d, 50d
121
+
122
+ // Verify files actually moved to archive
123
+ const archiveDir = join(dir, "archive", "session-checkpoints");
124
+ expect(existsSync(archiveDir)).toBe(true);
125
+ expect(readdirSync(archiveDir).filter((f) => f.endsWith(".json"))).toHaveLength(3);
126
+ } finally {
127
+ cleanup();
128
+ }
129
+ });
130
+
131
+ it("hard-deletes archived files older than 90 days", async () => {
132
+ const { store, dir, cleanup } = createTempStore();
133
+ try {
134
+ // Manually place 2 files in archive directory
135
+ const archiveDir = join(dir, "archive", "session-checkpoints");
136
+ mkdirSync(archiveDir, { recursive: true });
137
+
138
+ const file60d = join(archiveDir, "old-60d-file.json");
139
+ const file100d = join(archiveDir, "old-100d-file.json");
140
+ writeFileSync(file60d, "{}");
141
+ writeFileSync(file100d, "{}");
142
+
143
+ // Backdate mtimes
144
+ const mtime60 = new Date(NOW.getTime() - 60 * 86_400_000);
145
+ const mtime100 = new Date(NOW.getTime() - 100 * 86_400_000);
146
+ utimesSync(file60d, mtime60, mtime60);
147
+ utimesSync(file100d, mtime100, mtime100);
148
+
149
+ const result = store.gc({ now: NOW, archiveTtlDays: 90 });
150
+ expect(existsSync(file60d)).toBe(true); // 60d < 90d, survives
151
+ expect(existsSync(file100d)).toBe(false); // 100d > 90d, deleted
152
+ expect(result.deleted).toBe(1);
153
+ } finally {
154
+ cleanup();
155
+ }
156
+ });
157
+ });
158
+
159
+ describe("checkpoint gc - priority and edge cases", () => {
160
+ it("session TTL protection overrides minimal delete rule", async () => {
161
+ const { store, cleanup } = createTempStore();
162
+ try {
163
+ const sessionId = crypto.randomUUID();
164
+ // 2 hours old minimal — minimalTtlDays=0 would delete, but session TTL protects
165
+ await store.save(makeMinimalCheckpoint({ sessionId, updatedAt: daysBeforeNow(0.08) }));
166
+
167
+ const result = store.gc({ now: NOW, sessionTtlHours: 48, minimalTtlDays: 0 });
168
+ expect(result.kept).toBe(1);
169
+ expect(result.deleted).toBe(0);
170
+
171
+ const keepAction = result.actions.find((a) => a.action === "keep");
172
+ expect(keepAction?.reason).toContain("session");
173
+ } finally {
174
+ cleanup();
175
+ }
176
+ });
177
+
178
+ it("dry run does not modify any files", async () => {
179
+ const { store, dir, cleanup } = createTempStore();
180
+ try {
181
+ // 1 to delete (minimal, 10d), 1 to archive (rich, 35d), 1 to keep (rich, 2d)
182
+ await store.save(makeMinimalCheckpoint({ updatedAt: daysBeforeNow(10) }));
183
+ await store.save(makeRichCheckpoint({ updatedAt: daysBeforeNow(35) }));
184
+ await store.save(makeRichCheckpoint({ updatedAt: daysBeforeNow(2) }));
185
+
186
+ const checkpointDir = join(dir, "session-checkpoints");
187
+ const beforeCount = readdirSync(checkpointDir).filter((f) => f.endsWith(".json")).length;
188
+
189
+ const result = store.gc({ now: NOW, dryRun: true, keepPerScope: 1, archiveAfterDays: 30, minimalTtlDays: 7 });
190
+ expect(result.archived).toBeGreaterThan(0);
191
+ expect(result.deleted).toBeGreaterThan(0);
192
+
193
+ // Files are still there
194
+ const afterCount = readdirSync(checkpointDir).filter((f) => f.endsWith(".json")).length;
195
+ expect(afterCount).toBe(beforeCount);
196
+ } finally {
197
+ cleanup();
198
+ }
199
+ });
200
+ });
201
+
202
+ describe("checkpoint gc - structural", () => {
203
+ it("handles empty directory gracefully", () => {
204
+ const { store, cleanup } = createTempStore();
205
+ try {
206
+ const result = store.gc({ now: NOW });
207
+ expect(result.kept).toBe(0);
208
+ expect(result.archived).toBe(0);
209
+ expect(result.deleted).toBe(0);
210
+ expect(result.errors).toHaveLength(0);
211
+ } finally {
212
+ cleanup();
213
+ }
214
+ });
215
+ });
@@ -0,0 +1,81 @@
1
+ import { afterEach, describe, expect, it } from "bun:test";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ import { MemoryStore, classifyLegacyScope } from "../store.js";
7
+
8
+ const cleanupPaths: string[] = [];
9
+
10
+ afterEach(() => {
11
+ while (cleanupPaths.length > 0) {
12
+ const target = cleanupPaths.pop();
13
+ if (target) {
14
+ rmSync(target, { recursive: true, force: true });
15
+ }
16
+ }
17
+ });
18
+
19
+ function createStore(): MemoryStore {
20
+ const dbPath = mkdtempSync(join(tmpdir(), "recallnest-scope-audit-"));
21
+ cleanupPaths.push(dbPath);
22
+ return new MemoryStore({
23
+ dbPath,
24
+ vectorDim: 3,
25
+ });
26
+ }
27
+
28
+ describe("classifyLegacyScope", () => {
29
+ it("flags missing, empty, and global scope values", () => {
30
+ expect(classifyLegacyScope(undefined)).toBe("missing");
31
+ expect(classifyLegacyScope(null)).toBe("missing");
32
+ expect(classifyLegacyScope("")).toBe("empty");
33
+ expect(classifyLegacyScope(" ")).toBe("empty");
34
+ expect(classifyLegacyScope("global")).toBe("global");
35
+ expect(classifyLegacyScope(" global ")).toBe("global");
36
+ expect(classifyLegacyScope("project:recallnest")).toBeUndefined();
37
+ });
38
+ });
39
+
40
+ describe("MemoryStore.auditLegacyScopes", () => {
41
+ it("reports global and empty scope rows without mixing in valid scoped data", async () => {
42
+ const store = createStore();
43
+
44
+ await store.store({
45
+ text: "Scoped memory should not appear in the audit.",
46
+ vector: [1, 0, 0],
47
+ category: "entities",
48
+ scope: "project:recallnest",
49
+ importance: 0.8,
50
+ metadata: "{}",
51
+ });
52
+ await store.store({
53
+ text: "Historical row that used the global fallback.",
54
+ vector: [0, 1, 0],
55
+ category: "events",
56
+ scope: "global",
57
+ importance: 0.5,
58
+ metadata: "{}",
59
+ });
60
+ await store.store({
61
+ text: "Historical row that was written without a real scope.",
62
+ vector: [0, 0, 1],
63
+ category: "events",
64
+ scope: " ",
65
+ importance: 0.4,
66
+ metadata: "{}",
67
+ });
68
+
69
+ const audit = await store.auditLegacyScopes(10);
70
+
71
+ expect(audit.totalCount).toBe(2);
72
+ expect(audit.counts).toEqual({
73
+ missing: 0,
74
+ empty: 1,
75
+ global: 1,
76
+ });
77
+ expect(audit.samples).toHaveLength(2);
78
+ expect(audit.samples.map((sample) => sample.kind).sort()).toEqual(["empty", "global"]);
79
+ expect(audit.samples.every((sample) => sample.scope !== "project:recallnest")).toBe(true);
80
+ });
81
+ });
@@ -0,0 +1,241 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import {
3
+ parseTemporalQuery,
4
+ matchesTemporalConstraint,
5
+ temporalWhereClause,
6
+ type TemporalConstraint,
7
+ } from "../temporal-parser.js";
8
+
9
+ // ============================================================================
10
+ // parseTemporalQuery
11
+ // ============================================================================
12
+
13
+ describe("parseTemporalQuery", () => {
14
+ // --- Absolute year (ZH) ---
15
+ it("parses '2023年的记忆'", () => {
16
+ const result = parseTemporalQuery("2023年的记忆里有什么");
17
+ expect(result.constraint).not.toBeNull();
18
+ expect(result.constraint!.type).toBe("range");
19
+ expect(result.constraint!.startMs).toBe(new Date(2023, 0, 1).getTime());
20
+ expect(result.constraint!.endMs).toBe(new Date(2024, 0, 1).getTime() - 1);
21
+ // Time expression removed from cleaned query
22
+ expect(result.cleanedQuery).not.toContain("2023年");
23
+ expect(result.cleanedQuery).toContain("里有什么");
24
+ });
25
+
26
+ it("parses standalone '2024年'", () => {
27
+ const result = parseTemporalQuery("关于 2024年 的项目");
28
+ expect(result.constraint).not.toBeNull();
29
+ expect(result.constraint!.type).toBe("range");
30
+ });
31
+
32
+ // --- Absolute year+month (ZH) ---
33
+ it("parses '2023年3月'", () => {
34
+ const result = parseTemporalQuery("2023年3月我们讨论了什么");
35
+ expect(result.constraint).not.toBeNull();
36
+ expect(result.constraint!.startMs).toBe(new Date(2023, 2, 1).getTime());
37
+ expect(result.constraint!.endMs).toBe(new Date(2023, 3, 1).getTime() - 1);
38
+ });
39
+
40
+ it("parses '2023年三月'", () => {
41
+ const result = parseTemporalQuery("2023年三月的对话");
42
+ expect(result.constraint).not.toBeNull();
43
+ expect(result.constraint!.startMs).toBe(new Date(2023, 2, 1).getTime());
44
+ });
45
+
46
+ // --- Absolute year+month (EN) ---
47
+ it("parses 'March 2023'", () => {
48
+ const result = parseTemporalQuery("What did we discuss in March 2023");
49
+ expect(result.constraint).not.toBeNull();
50
+ expect(result.constraint!.startMs).toBe(new Date(2023, 2, 1).getTime());
51
+ expect(result.constraint!.endMs).toBe(new Date(2023, 3, 1).getTime() - 1);
52
+ });
53
+
54
+ // --- "in YYYY" ---
55
+ it("parses 'in 2022'", () => {
56
+ const result = parseTemporalQuery("Projects discussed in 2022");
57
+ expect(result.constraint).not.toBeNull();
58
+ expect(result.constraint!.type).toBe("range");
59
+ expect(result.constraint!.startMs).toBe(new Date(2022, 0, 1).getTime());
60
+ });
61
+
62
+ // --- Relative: 最近N天/周/月 ---
63
+ it("parses '最近7天'", () => {
64
+ const result = parseTemporalQuery("最近7天的记忆");
65
+ expect(result.constraint).not.toBeNull();
66
+ expect(result.constraint!.type).toBe("range");
67
+ const now = Date.now();
68
+ expect(result.constraint!.startMs).toBeGreaterThan(now - 8 * 86_400_000);
69
+ expect(result.constraint!.endMs).toBeLessThanOrEqual(now + 1000);
70
+ });
71
+
72
+ it("parses '最近2周'", () => {
73
+ const result = parseTemporalQuery("最近2周讨论过什么");
74
+ expect(result.constraint).not.toBeNull();
75
+ const now = Date.now();
76
+ expect(result.constraint!.startMs).toBeGreaterThan(now - 15 * 86_400_000);
77
+ });
78
+
79
+ it("parses '最近3个月'", () => {
80
+ const result = parseTemporalQuery("最近3个月的项目进展");
81
+ expect(result.constraint).not.toBeNull();
82
+ const now = Date.now();
83
+ expect(result.constraint!.startMs).toBeGreaterThan(now - 100 * 86_400_000);
84
+ });
85
+
86
+ // --- Relative: last N days/weeks/months ---
87
+ it("parses 'last 30 days'", () => {
88
+ const result = parseTemporalQuery("What happened in the last 30 days");
89
+ expect(result.constraint).not.toBeNull();
90
+ expect(result.constraint!.type).toBe("range");
91
+ const now = Date.now();
92
+ expect(result.constraint!.startMs).toBeGreaterThan(now - 31 * 86_400_000);
93
+ });
94
+
95
+ it("parses 'last 2 weeks'", () => {
96
+ const result = parseTemporalQuery("Changes in the last 2 weeks");
97
+ expect(result.constraint).not.toBeNull();
98
+ });
99
+
100
+ // --- Relative: 上周/上个月/去年 ---
101
+ it("parses '去年'", () => {
102
+ const result = parseTemporalQuery("去年我们做了什么项目");
103
+ expect(result.constraint).not.toBeNull();
104
+ const lastYear = new Date().getFullYear() - 1;
105
+ expect(result.constraint!.startMs).toBe(new Date(lastYear, 0, 1).getTime());
106
+ expect(result.constraint!.endMs).toBe(new Date(lastYear + 1, 0, 1).getTime() - 1);
107
+ });
108
+
109
+ it("parses 'last year'", () => {
110
+ const result = parseTemporalQuery("Projects from last year");
111
+ expect(result.constraint).not.toBeNull();
112
+ expect(result.constraint!.type).toBe("range");
113
+ });
114
+
115
+ it("parses '上个月'", () => {
116
+ const result = parseTemporalQuery("上个月的讨论");
117
+ expect(result.constraint).not.toBeNull();
118
+ });
119
+
120
+ it("parses '去年3月'", () => {
121
+ const result = parseTemporalQuery("去年3月的事情");
122
+ expect(result.constraint).not.toBeNull();
123
+ const lastYear = new Date().getFullYear() - 1;
124
+ expect(result.constraint!.startMs).toBe(new Date(lastYear, 2, 1).getTime());
125
+ });
126
+
127
+ // --- Operator syntax ---
128
+ it("parses 'after:2024-01'", () => {
129
+ const result = parseTemporalQuery("TypeScript projects after:2024-01");
130
+ expect(result.constraint).not.toBeNull();
131
+ expect(result.constraint!.type).toBe("after");
132
+ expect(result.constraint!.startMs).toBe(new Date(2024, 0, 1).getTime());
133
+ expect(result.cleanedQuery).toContain("TypeScript projects");
134
+ });
135
+
136
+ it("parses 'before:2025-06'", () => {
137
+ const result = parseTemporalQuery("memory entries before:2025-06");
138
+ expect(result.constraint).not.toBeNull();
139
+ expect(result.constraint!.type).toBe("before");
140
+ });
141
+
142
+ // --- No temporal expression ---
143
+ it("returns null for non-temporal queries", () => {
144
+ const result = parseTemporalQuery("How to configure TypeScript");
145
+ expect(result.constraint).toBeNull();
146
+ expect(result.cleanedQuery).toBe("How to configure TypeScript");
147
+ });
148
+
149
+ it("returns null for empty query", () => {
150
+ const result = parseTemporalQuery("");
151
+ expect(result.constraint).toBeNull();
152
+ });
153
+
154
+ // --- Cleaned query ---
155
+ it("removes temporal expression and preserves the rest", () => {
156
+ const result = parseTemporalQuery("2023年我们讨论了 Docker 配置");
157
+ expect(result.cleanedQuery).toContain("Docker");
158
+ expect(result.cleanedQuery).not.toContain("2023年");
159
+ });
160
+ });
161
+
162
+ // ============================================================================
163
+ // matchesTemporalConstraint
164
+ // ============================================================================
165
+
166
+ describe("matchesTemporalConstraint", () => {
167
+ it("matches timestamp within range", () => {
168
+ const constraint: TemporalConstraint = {
169
+ type: "range",
170
+ startMs: new Date(2023, 0, 1).getTime(),
171
+ endMs: new Date(2024, 0, 1).getTime() - 1,
172
+ anchor: "2023年",
173
+ };
174
+ const mid2023 = new Date(2023, 6, 1).getTime();
175
+ expect(matchesTemporalConstraint(mid2023, constraint)).toBe(true);
176
+ });
177
+
178
+ it("rejects timestamp outside range", () => {
179
+ const constraint: TemporalConstraint = {
180
+ type: "range",
181
+ startMs: new Date(2023, 0, 1).getTime(),
182
+ endMs: new Date(2024, 0, 1).getTime() - 1,
183
+ anchor: "2023年",
184
+ };
185
+ const mid2022 = new Date(2022, 6, 1).getTime();
186
+ expect(matchesTemporalConstraint(mid2022, constraint)).toBe(false);
187
+ });
188
+
189
+ it("handles 'after' constraint", () => {
190
+ const constraint: TemporalConstraint = {
191
+ type: "after",
192
+ startMs: new Date(2024, 0, 1).getTime(),
193
+ anchor: "after:2024-01",
194
+ };
195
+ expect(matchesTemporalConstraint(new Date(2024, 6, 1).getTime(), constraint)).toBe(true);
196
+ expect(matchesTemporalConstraint(new Date(2023, 6, 1).getTime(), constraint)).toBe(false);
197
+ });
198
+
199
+ it("handles 'before' constraint", () => {
200
+ const constraint: TemporalConstraint = {
201
+ type: "before",
202
+ endMs: new Date(2024, 0, 1).getTime(),
203
+ anchor: "before:2024-01",
204
+ };
205
+ expect(matchesTemporalConstraint(new Date(2023, 6, 1).getTime(), constraint)).toBe(true);
206
+ expect(matchesTemporalConstraint(new Date(2025, 0, 1).getTime(), constraint)).toBe(false);
207
+ });
208
+ });
209
+
210
+ // ============================================================================
211
+ // temporalWhereClause
212
+ // ============================================================================
213
+
214
+ describe("temporalWhereClause", () => {
215
+ it("generates range clause", () => {
216
+ const clause = temporalWhereClause({
217
+ type: "range",
218
+ startMs: 1000,
219
+ endMs: 2000,
220
+ anchor: "test",
221
+ });
222
+ expect(clause).toBe("timestamp >= 1000 AND timestamp <= 2000");
223
+ });
224
+
225
+ it("generates after-only clause", () => {
226
+ const clause = temporalWhereClause({
227
+ type: "after",
228
+ startMs: 1000,
229
+ anchor: "test",
230
+ });
231
+ expect(clause).toBe("timestamp >= 1000");
232
+ });
233
+
234
+ it("returns null for empty constraint", () => {
235
+ const clause = temporalWhereClause({
236
+ type: "range",
237
+ anchor: "test",
238
+ });
239
+ expect(clause).toBeNull();
240
+ });
241
+ });
@@ -0,0 +1,121 @@
1
+ import { describe, expect, it } from "bun:test";
2
+
3
+ import {
4
+ buildTaskHintTerms,
5
+ extractTerms,
6
+ looksLikeContinuityTask,
7
+ looksLikeRecallOnlyTask,
8
+ taskCueCoverage,
9
+ } from "../term-registry.js";
10
+
11
+ describe("term registry", () => {
12
+ it("extracts compact Chinese and latin cue terms", () => {
13
+ expect(extractTerms("继续 RecallNest continuity bridge 适配")).toEqual([
14
+ "继续",
15
+ "recallnest",
16
+ "continuity",
17
+ "bridge",
18
+ "适配",
19
+ ]);
20
+ });
21
+
22
+ it("can keep later terms when a wider extraction window is requested", () => {
23
+ expect(
24
+ extractTerms(
25
+ "继续 RecallNest continuity helper boundary audit ranking scoring selection orchestration context composer stable query fallback profile forwarding gap runner isolation",
26
+ 24,
27
+ ),
28
+ ).toEqual(expect.arrayContaining([
29
+ "profile",
30
+ "forwarding",
31
+ "runner",
32
+ "isolation",
33
+ ]));
34
+ });
35
+
36
+ it("builds task hints for writing prompts across languages", () => {
37
+ expect(buildTaskHintTerms("continue my AI writing project")).toEqual([
38
+ "写作",
39
+ "文章",
40
+ "语气",
41
+ "风格",
42
+ "口语化",
43
+ "不端着",
44
+ "ai",
45
+ "公众号",
46
+ ]);
47
+ });
48
+
49
+ it("treats recall-only wording differently from writing actions", () => {
50
+ expect(looksLikeRecallOnlyTask("你还记得我之前的偏好吗")).toBe(true);
51
+ expect(looksLikeRecallOnlyTask("不要让我重复前情,继续写文章")).toBe(false);
52
+ });
53
+
54
+ it("tracks workflow cue coverage from pattern text", () => {
55
+ expect(
56
+ taskCueCoverage(
57
+ "patterns",
58
+ "Workflow pattern: Cross-window continuity handoff Tools: resume_context, checkpoint_session, search_memory",
59
+ ),
60
+ ).toEqual(["search_memory", "resume_context", "checkpoint"]);
61
+ });
62
+
63
+ it("treats named RecallNest and memory-layer continues as continuity tasks", () => {
64
+ expect(looksLikeContinuityTask("回到 RecallNest 继续做")).toBe(true);
65
+ expect(looksLikeContinuityTask("接着弄 RecallNest 那个 memory layer")).toBe(true);
66
+ expect(looksLikeContinuityTask("把记忆层这条线先续上")).toBe(true);
67
+ expect(looksLikeContinuityTask("把之前那套 recall 管线先捡起来")).toBe(true);
68
+ expect(looksLikeContinuityTask("pick up where we left off on RecallNest")).toBe(true);
69
+ expect(looksLikeContinuityTask("回到 A2A 继续做")).toBe(false);
70
+ // Status queries are NOT continuation tasks — entity+cases is sufficient
71
+ expect(looksLikeContinuityTask("RecallNest 现在啥情况")).toBe(false);
72
+ });
73
+
74
+ it("builds RecallNest continuity hints for memory-layer shorthand", () => {
75
+ expect(buildTaskHintTerms("把刚才那个 memory layer 接回去")).toEqual(expect.arrayContaining([
76
+ "recallnest",
77
+ "checkpoint_session",
78
+ "resume_context",
79
+ "store_memory",
80
+ ]));
81
+ });
82
+
83
+ it("builds RecallNest continuity hints for colloquial memory shorthand", () => {
84
+ expect(buildTaskHintTerms("把那条跨窗口记忆的活接着弄完")).toEqual(expect.arrayContaining([
85
+ "recallnest",
86
+ "checkpoint_session",
87
+ "resume_context",
88
+ ]));
89
+ expect(buildTaskHintTerms("把之前那套 recall 管线先捡起来")).toEqual(expect.arrayContaining([
90
+ "recallnest",
91
+ "checkpoint_session",
92
+ "resume_context",
93
+ ]));
94
+ });
95
+
96
+ it("treats 记忆项目 as continuity task and builds RecallNest hints", () => {
97
+ expect(looksLikeContinuityTask("那个记忆项目,之前做到哪了")).toBe(true);
98
+ expect(looksLikeContinuityTask("继续记忆项目")).toBe(true);
99
+ expect(buildTaskHintTerms("那个记忆项目,之前做到哪了")).toEqual(expect.arrayContaining([
100
+ "recallnest",
101
+ "resume_context",
102
+ ]));
103
+ expect(buildTaskHintTerms("continue the memory project")).toEqual(expect.arrayContaining([
104
+ "recallnest",
105
+ ]));
106
+ });
107
+
108
+ it("treats 跨终端记忆/记忆功能/记忆服务 shorthand as continuity cue", () => {
109
+ // "还搞吗" is a status question, not a continuation verb — correct to return false
110
+ expect(looksLikeContinuityTask("那个跨终端记忆还搞吗")).toBe(false);
111
+ expect(looksLikeContinuityTask("继续那个跨终端记忆")).toBe(true);
112
+ expect(looksLikeContinuityTask("上次做的那个记忆功能")).toBe(true);
113
+ expect(looksLikeContinuityTask("之前那个 MCP 记忆服务")).toBe(true);
114
+ expect(buildTaskHintTerms("那个跨终端记忆还搞吗")).toEqual(expect.arrayContaining([
115
+ "recallnest",
116
+ ]));
117
+ expect(buildTaskHintTerms("上次做的记忆功能")).toEqual(expect.arrayContaining([
118
+ "recallnest",
119
+ ]));
120
+ });
121
+ });