recallnest 3.0.0 → 4.0.0

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 (246) hide show
  1. package/CHANGELOG.md +304 -0
  2. package/README.md +251 -258
  3. package/README_CN.md +226 -247
  4. package/assets/ui/app.js +6 -1
  5. package/bin/recallnest.mjs +20 -37
  6. package/docs/api-reference.md +8 -0
  7. package/docs/architecture.md +18 -13
  8. package/eval/cases-canary.json +107 -9
  9. package/integrations/examples/claude-agent-sdk/memory-agent.ts +2 -0
  10. package/integrations/examples/langchain/memory-chain.py +4 -0
  11. package/integrations/examples/openai-agents-sdk/memory-agent.py +4 -0
  12. package/package.json +12 -3
  13. package/src/activity-counter.ts +1 -0
  14. package/src/adaptive-retrieval.ts +23 -0
  15. package/src/api-server.ts +13 -11
  16. package/src/asset-sync.ts +26 -2
  17. package/src/audit-log.ts +2 -0
  18. package/src/auto-gc.ts +4 -3
  19. package/src/belief-history.ts +137 -31
  20. package/src/capture-engine.ts +459 -71
  21. package/src/cascade-forget.ts +7 -3
  22. package/src/checkpoint-mirror.ts +417 -0
  23. package/src/cli.ts +574 -54
  24. package/src/conflict-engine.ts +43 -15
  25. package/src/consolidation-engine.ts +24 -3
  26. package/src/context-collapse-renderer.ts +8 -1
  27. package/src/context-composer-stable.ts +5 -1
  28. package/src/context-composer.ts +11 -2
  29. package/src/distill-lock.ts +246 -74
  30. package/src/dream-pipeline.ts +3 -1
  31. package/src/embedder.ts +24 -0
  32. package/src/env-config.ts +136 -0
  33. package/src/flock.ts +97 -0
  34. package/src/forget-engine.ts +342 -58
  35. package/src/gateway-server.ts +4 -0
  36. package/src/ingest-plan.ts +56 -0
  37. package/src/ingest.ts +294 -121
  38. package/src/kg-contradiction.ts +753 -0
  39. package/src/kg-extractor.ts +17 -0
  40. package/src/kg-store.ts +1 -1
  41. package/src/known-item-eval.ts +455 -0
  42. package/src/llm-client.ts +13 -26
  43. package/src/llm-consolidation.ts +2 -2
  44. package/src/mcp-tool-deps.ts +6 -0
  45. package/src/mcp-tools-advanced.ts +80 -45
  46. package/src/mcp-tools-core.ts +23 -17
  47. package/src/mcp-tools-governance.ts +13 -7
  48. package/src/memory-assets.ts +144 -8
  49. package/src/memory-boundaries.ts +19 -0
  50. package/src/memory-evolution.ts +9 -0
  51. package/src/memory-lint.ts +1 -1
  52. package/src/memory-output.ts +311 -14
  53. package/src/memory-reconcile.ts +1437 -0
  54. package/src/memory-schema.ts +11 -0
  55. package/src/memory-store-port.ts +1 -0
  56. package/src/minis-archive.ts +151 -0
  57. package/src/noise-filter.ts +15 -2
  58. package/src/pii-detector.ts +31 -1
  59. package/src/pivot-apply.ts +55 -5
  60. package/src/pivot-evidence.ts +128 -0
  61. package/src/ppr-traversal.ts +25 -1
  62. package/src/preference-matcher.ts +40 -28
  63. package/src/prospective-memory.ts +7 -4
  64. package/src/retriever.ts +297 -57
  65. package/src/retroactive-boost.ts +3 -2
  66. package/src/runtime-config.ts +15 -1
  67. package/src/scope-policy.ts +1 -0
  68. package/src/session-distiller.ts +6 -2
  69. package/src/session-engine.ts +11 -4
  70. package/src/session-output.ts +20 -1
  71. package/src/session-schema.ts +5 -2
  72. package/src/session-store.ts +59 -12
  73. package/src/skill-promotion.ts +67 -1
  74. package/src/store-receipts.ts +109 -0
  75. package/src/store-write-budget.ts +37 -0
  76. package/src/store.ts +298 -57
  77. package/src/text-fingerprint.ts +25 -0
  78. package/src/trigger-store.ts +443 -0
  79. package/src/ui-server.ts +22 -21
  80. package/src/version-manager.ts +19 -5
  81. package/src/write-verifier.ts +16 -11
  82. package/src/__tests__/access-tracker-novelty.test.ts +0 -324
  83. package/src/__tests__/activity-counter.test.ts +0 -125
  84. package/src/__tests__/adaptive-fidelity.test.ts +0 -102
  85. package/src/__tests__/admission-control.test.ts +0 -206
  86. package/src/__tests__/age-label.test.ts +0 -33
  87. package/src/__tests__/anchor-generator.test.ts +0 -67
  88. package/src/__tests__/api-server-security.test.ts +0 -102
  89. package/src/__tests__/auto-consolidation.test.ts +0 -168
  90. package/src/__tests__/auto-recall.test.ts +0 -131
  91. package/src/__tests__/babel-integration.test.ts +0 -144
  92. package/src/__tests__/batch-dedup.test.ts +0 -65
  93. package/src/__tests__/belief-history.test.ts +0 -220
  94. package/src/__tests__/canary-eval.test.ts +0 -114
  95. package/src/__tests__/capture-engine.test.ts +0 -1328
  96. package/src/__tests__/capture-heuristic.test.ts +0 -171
  97. package/src/__tests__/cascade-forget.test.ts +0 -210
  98. package/src/__tests__/category-min-scores.test.ts +0 -116
  99. package/src/__tests__/cc8-cc1-essential-context.test.ts +0 -298
  100. package/src/__tests__/circuit-breaker.test.ts +0 -99
  101. package/src/__tests__/cluster-consolidation.test.ts +0 -1236
  102. package/src/__tests__/confidence-metadata.test.ts +0 -125
  103. package/src/__tests__/confidence-tracker.test.ts +0 -261
  104. package/src/__tests__/conflict-advisor.test.ts +0 -206
  105. package/src/__tests__/conflict-escalation.test.ts +0 -149
  106. package/src/__tests__/conflict-lifecycle.test.ts +0 -83
  107. package/src/__tests__/conflict-output.test.ts +0 -85
  108. package/src/__tests__/conflict-store.test.ts +0 -418
  109. package/src/__tests__/connector-v1.test.ts +0 -181
  110. package/src/__tests__/consolidation-engine.test.ts +0 -520
  111. package/src/__tests__/constructive-retrieval.test.ts +0 -509
  112. package/src/__tests__/context-collapse-renderer.test.ts +0 -214
  113. package/src/__tests__/context-composer-pins.test.ts +0 -64
  114. package/src/__tests__/context-composer-task-selection.test.ts +0 -24
  115. package/src/__tests__/context-reconstructor.test.ts +0 -65
  116. package/src/__tests__/context-renderer.test.ts +0 -91
  117. package/src/__tests__/continuity-eval.test.ts +0 -275
  118. package/src/__tests__/continuity-schema.test.ts +0 -97
  119. package/src/__tests__/contracts.test.ts +0 -262
  120. package/src/__tests__/conversation-importer.test.ts +0 -692
  121. package/src/__tests__/core-recall-eval.test.ts +0 -235
  122. package/src/__tests__/core-summary.test.ts +0 -109
  123. package/src/__tests__/data-checkup.test.ts +0 -304
  124. package/src/__tests__/decay-exemption.test.ts +0 -129
  125. package/src/__tests__/dedup-actions.test.ts +0 -58
  126. package/src/__tests__/dedup-guard.test.ts +0 -39
  127. package/src/__tests__/deterministic-id.test.ts +0 -39
  128. package/src/__tests__/distill-lock.test.ts +0 -305
  129. package/src/__tests__/distill-session-argshape.test.ts +0 -79
  130. package/src/__tests__/doctor.test.ts +0 -319
  131. package/src/__tests__/dream-pipeline.test.ts +0 -820
  132. package/src/__tests__/dream-scope-isolation.test.ts +0 -386
  133. package/src/__tests__/dream-write-batching.test.ts +0 -211
  134. package/src/__tests__/drill-down.test.ts +0 -108
  135. package/src/__tests__/embedder.test.ts +0 -122
  136. package/src/__tests__/emotion-decay.test.ts +0 -46
  137. package/src/__tests__/emotion-detector.test.ts +0 -56
  138. package/src/__tests__/emotion-phase1-integration.test.ts +0 -153
  139. package/src/__tests__/emotion-retrieval.test.ts +0 -56
  140. package/src/__tests__/entity-resolver.test.ts +0 -58
  141. package/src/__tests__/env-config-invariant.test.ts +0 -80
  142. package/src/__tests__/env-config.test.ts +0 -211
  143. package/src/__tests__/error-signature-boost.test.ts +0 -87
  144. package/src/__tests__/error-signature.test.ts +0 -95
  145. package/src/__tests__/error-taxonomy.test.ts +0 -129
  146. package/src/__tests__/ethics-layer.test.ts +0 -476
  147. package/src/__tests__/event-segmenter.test.ts +0 -153
  148. package/src/__tests__/feature-flags-integration.test.ts +0 -54
  149. package/src/__tests__/frequency-tracker.test.ts +0 -113
  150. package/src/__tests__/freshness.test.ts +0 -186
  151. package/src/__tests__/graph-export.test.ts +0 -489
  152. package/src/__tests__/hotness-blend.test.ts +0 -283
  153. package/src/__tests__/ingest-chunking.test.ts +0 -44
  154. package/src/__tests__/ingest-scope-prefix.test.ts +0 -69
  155. package/src/__tests__/ingest.test.ts +0 -360
  156. package/src/__tests__/interference-detector.test.ts +0 -203
  157. package/src/__tests__/kg-extractor.test.ts +0 -192
  158. package/src/__tests__/kg-store.test.ts +0 -300
  159. package/src/__tests__/llm-client-pivot.test.ts +0 -628
  160. package/src/__tests__/llm-consolidation.test.ts +0 -226
  161. package/src/__tests__/maintenance-locks.test.ts +0 -197
  162. package/src/__tests__/mcp-efficiency.test.ts +0 -288
  163. package/src/__tests__/mcp-related-scopes.test.ts +0 -151
  164. package/src/__tests__/memory-boundaries.test.ts +0 -282
  165. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +0 -362
  166. package/src/__tests__/memory-evolution.test.ts +0 -271
  167. package/src/__tests__/memory-health-rebalance.test.ts +0 -144
  168. package/src/__tests__/memory-lint.test.ts +0 -421
  169. package/src/__tests__/memory-output.test.ts +0 -224
  170. package/src/__tests__/memory-promotion.test.ts +0 -315
  171. package/src/__tests__/memory-utility.test.ts +0 -256
  172. package/src/__tests__/migrate-dedup-ids.test.ts +0 -65
  173. package/src/__tests__/multi-vector.test.ts +0 -242
  174. package/src/__tests__/narrative-architecture.test.ts +0 -456
  175. package/src/__tests__/noise-filter.test.ts +0 -205
  176. package/src/__tests__/noise-prototype-bank.test.ts +0 -280
  177. package/src/__tests__/obsidian-connector.test.ts +0 -363
  178. package/src/__tests__/omitted-reporting.test.ts +0 -98
  179. package/src/__tests__/openai-contract.test.ts +0 -513
  180. package/src/__tests__/pending-review.test.ts +0 -77
  181. package/src/__tests__/pii-redact.test.ts +0 -333
  182. package/src/__tests__/pivot-apply.test.ts +0 -373
  183. package/src/__tests__/pivot-distill-supervisor.test.ts +0 -457
  184. package/src/__tests__/pivot-distill.test.ts +0 -1178
  185. package/src/__tests__/post-retrieval-filter.test.ts +0 -178
  186. package/src/__tests__/ppr-traversal.test.ts +0 -256
  187. package/src/__tests__/prediction-engine.test.ts +0 -349
  188. package/src/__tests__/predictive-prospective-memory.test.ts +0 -416
  189. package/src/__tests__/preference-guard-smoke.test.ts +0 -211
  190. package/src/__tests__/preference-matcher.test.ts +0 -282
  191. package/src/__tests__/preference-slots.test.ts +0 -205
  192. package/src/__tests__/promotion-distinct-sources.test.ts +0 -162
  193. package/src/__tests__/prospective-memory.test.ts +0 -283
  194. package/src/__tests__/pull-from-macbook-sync-only.test.ts +0 -679
  195. package/src/__tests__/query-entity-detector.test.ts +0 -171
  196. package/src/__tests__/query-expander-alias.test.ts +0 -146
  197. package/src/__tests__/query-expander.test.ts +0 -34
  198. package/src/__tests__/recall-gate.test.ts +0 -266
  199. package/src/__tests__/recall-governor.test.ts +0 -227
  200. package/src/__tests__/recall-mode.test.ts +0 -64
  201. package/src/__tests__/resolve-db-path.test.ts +0 -130
  202. package/src/__tests__/resolve-tier.test.ts +0 -64
  203. package/src/__tests__/result-synthesizer.test.ts +0 -198
  204. package/src/__tests__/resume-context.test.ts +0 -4898
  205. package/src/__tests__/retrieval-eval.test.ts +0 -163
  206. package/src/__tests__/retrieval-trace.test.ts +0 -213
  207. package/src/__tests__/retrieve-audit-revision.test.ts +0 -255
  208. package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
  209. package/src/__tests__/retriever-lazy-init-dispatch.test.ts +0 -123
  210. package/src/__tests__/retroactive-boost.test.ts +0 -223
  211. package/src/__tests__/salience-scoring.test.ts +0 -106
  212. package/src/__tests__/scope-inventory.test.ts +0 -200
  213. package/src/__tests__/scope-policy.test.ts +0 -69
  214. package/src/__tests__/scope-suggester.test.ts +0 -80
  215. package/src/__tests__/session-checkpoint.test.ts +0 -291
  216. package/src/__tests__/session-distiller.test.ts +0 -640
  217. package/src/__tests__/session-gc.test.ts +0 -215
  218. package/src/__tests__/skill-engine.test.ts +0 -552
  219. package/src/__tests__/skill-promotion.test.ts +0 -654
  220. package/src/__tests__/skill-verifier.test.ts +0 -122
  221. package/src/__tests__/source-heartbeat.test.ts +0 -254
  222. package/src/__tests__/stable-context-length-contract.test.ts +0 -62
  223. package/src/__tests__/store-patch-metadata.test.ts +0 -235
  224. package/src/__tests__/store-read-consistency.test.ts +0 -122
  225. package/src/__tests__/store-schema-migration.test.ts +0 -87
  226. package/src/__tests__/store-scope-audit.test.ts +0 -81
  227. package/src/__tests__/store-upsert.test.ts +0 -201
  228. package/src/__tests__/supersede-chain.test.ts +0 -154
  229. package/src/__tests__/synthesis-contract.test.ts +0 -252
  230. package/src/__tests__/synthesis-promotion.test.ts +0 -369
  231. package/src/__tests__/temporal-parser.test.ts +0 -241
  232. package/src/__tests__/temporal-validity.test.ts +0 -160
  233. package/src/__tests__/term-registry-terminal-roster.test.ts +0 -127
  234. package/src/__tests__/term-registry.test.ts +0 -121
  235. package/src/__tests__/tool-error-surfacing.test.ts +0 -98
  236. package/src/__tests__/tool-output-compressor.test.ts +0 -160
  237. package/src/__tests__/topic-tag.test.ts +0 -113
  238. package/src/__tests__/trusted-memory.test.ts +0 -867
  239. package/src/__tests__/ui-server-security.test.ts +0 -93
  240. package/src/__tests__/usage-tracker.test.ts +0 -287
  241. package/src/__tests__/utility-weight.test.ts +0 -57
  242. package/src/__tests__/verbatim-self-recall.test.ts +0 -279
  243. package/src/__tests__/version-manager.test.ts +0 -248
  244. package/src/__tests__/workflow-observation-skill-outcome.test.ts +0 -352
  245. package/src/__tests__/workflow-observation.test.ts +0 -433
  246. package/src/__tests__/write-verifier.test.ts +0 -122
@@ -1,122 +0,0 @@
1
- import { afterEach, describe, expect, it } from "bun:test";
2
- import { mkdtempSync, rmSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
-
6
- import { readConsistencyInterval as envReadConsistencyInterval } from "../env-config.js";
7
- import { MemoryStore } from "../store.js";
8
-
9
- const cleanupPaths: string[] = [];
10
- const savedEnv = process.env.RECALLNEST_READ_CONSISTENCY_INTERVAL;
11
-
12
- afterEach(() => {
13
- while (cleanupPaths.length > 0) {
14
- const target = cleanupPaths.pop();
15
- if (target) {
16
- rmSync(target, { recursive: true, force: true });
17
- }
18
- }
19
- if (savedEnv === undefined) {
20
- delete process.env.RECALLNEST_READ_CONSISTENCY_INTERVAL;
21
- } else {
22
- process.env.RECALLNEST_READ_CONSISTENCY_INTERVAL = savedEnv;
23
- }
24
- });
25
-
26
- function tempDb(): string {
27
- const dbPath = mkdtempSync(join(tmpdir(), "recallnest-read-consistency-"));
28
- cleanupPaths.push(dbPath);
29
- return dbPath;
30
- }
31
-
32
- function entry(i: number) {
33
- return {
34
- text: `read-consistency memory ${i}`,
35
- vector: [0.1 * i, 0.2 * i, 0.3 * i],
36
- category: "fact" as const,
37
- scope: "project:rci-test",
38
- importance: 0.5,
39
- };
40
- }
41
-
42
- // A writer always sees its own commits on its own handle; the staleness gap
43
- // only exists ACROSS handles. Two independent MemoryStore instances against
44
- // the same dbPath reproduce the cross-process topology (CLI ingest vs resident
45
- // MCP/API/UI server) in-process: the writer deletes a row after the reader's
46
- // handle has already opened the table.
47
- describe("read consistency across independent connections", () => {
48
- it("default (env unset) is strong consistency: reader sees the writer's delete", async () => {
49
- delete process.env.RECALLNEST_READ_CONSISTENCY_INTERVAL;
50
- const dbPath = tempDb();
51
-
52
- const writer = new MemoryStore({ dbPath, vectorDim: 3 });
53
- const e1 = await writer.store(entry(1));
54
- await writer.store(entry(2));
55
-
56
- const reader = new MemoryStore({ dbPath, vectorDim: 3 });
57
- expect((await reader.list(undefined, undefined, 20, 0)).length).toBe(2);
58
-
59
- await writer.delete(e1.id);
60
-
61
- expect((await reader.list(undefined, undefined, 20, 0)).length).toBe(1);
62
- });
63
-
64
- it("env 'off' restores the legacy pinned-handle behavior (negative control)", async () => {
65
- process.env.RECALLNEST_READ_CONSISTENCY_INTERVAL = "off";
66
- const dbPath = tempDb();
67
-
68
- const writer = new MemoryStore({ dbPath, vectorDim: 3 });
69
- const e1 = await writer.store(entry(1));
70
- await writer.store(entry(2));
71
-
72
- const reader = new MemoryStore({ dbPath, vectorDim: 3 });
73
- expect((await reader.list(undefined, undefined, 20, 0)).length).toBe(2);
74
-
75
- await writer.delete(e1.id);
76
-
77
- expect((await reader.list(undefined, undefined, 20, 0)).length).toBe(2);
78
- });
79
-
80
- it("explicit config wins over env: interval 0 sees the delete despite env 'off'", async () => {
81
- process.env.RECALLNEST_READ_CONSISTENCY_INTERVAL = "off";
82
- const dbPath = tempDb();
83
-
84
- const writer = new MemoryStore({ dbPath, vectorDim: 3 });
85
- const e1 = await writer.store(entry(1));
86
- await writer.store(entry(2));
87
-
88
- const reader = new MemoryStore({ dbPath, vectorDim: 3, readConsistencyInterval: 0 });
89
- expect((await reader.list(undefined, undefined, 20, 0)).length).toBe(2);
90
-
91
- await writer.delete(e1.id);
92
-
93
- expect((await reader.list(undefined, undefined, 20, 0)).length).toBe(1);
94
- });
95
-
96
- // Cross-handle visibility behavior is pinned by the three tests above;
97
- // what remains is the env parsing matrix of the resolver itself.
98
- it("env resolver: unset/empty → 0, off/none → undefined, numbers pass, junk clamps to 0", () => {
99
- delete process.env.RECALLNEST_READ_CONSISTENCY_INTERVAL;
100
- expect(envReadConsistencyInterval()).toBe(0);
101
-
102
- process.env.RECALLNEST_READ_CONSISTENCY_INTERVAL = "";
103
- expect(envReadConsistencyInterval()).toBe(0);
104
-
105
- for (const legacy of ["off", "none"]) {
106
- process.env.RECALLNEST_READ_CONSISTENCY_INTERVAL = legacy;
107
- expect(envReadConsistencyInterval()).toBeUndefined();
108
- }
109
-
110
- process.env.RECALLNEST_READ_CONSISTENCY_INTERVAL = "7";
111
- expect(envReadConsistencyInterval()).toBe(7);
112
- process.env.RECALLNEST_READ_CONSISTENCY_INTERVAL = "1.5";
113
- expect(envReadConsistencyInterval()).toBe(1.5);
114
- process.env.RECALLNEST_READ_CONSISTENCY_INTERVAL = "0";
115
- expect(envReadConsistencyInterval()).toBe(0);
116
-
117
- for (const junk of ["-3", "abc", "Infinity", "NaN"]) {
118
- process.env.RECALLNEST_READ_CONSISTENCY_INTERVAL = junk;
119
- expect(envReadConsistencyInterval()).toBe(0);
120
- }
121
- });
122
- });
@@ -1,87 +0,0 @@
1
- import { describe, expect, it, afterEach } from "bun:test";
2
- import { mkdtemp, rm } from "fs/promises";
3
- import { tmpdir } from "os";
4
- import { join } from "path";
5
- import * as lancedb from "@lancedb/lancedb";
6
-
7
- import { MemoryStore } from "../store.js";
8
-
9
- const DIM = 4;
10
-
11
- /**
12
- * Regression: the pre-2026-04-09 table schema has 8 columns (no language /
13
- * fts_text). The old migration check sampled a row to detect missing columns,
14
- * so a 0-row legacy table never migrated and EVERY write then failed with
15
- * "Found field not in schema: language". Production hit exactly this: a table
16
- * created one day before the columns existed, empty for 3 months.
17
- */
18
-
19
- async function createLegacyTable(dbPath: string, withRow: boolean) {
20
- const db = await lancedb.connect(dbPath);
21
- const seed = [
22
- {
23
- id: "__seed__",
24
- text: withRow ? "legacy row" : "",
25
- vector: new Array(DIM).fill(0),
26
- category: "other",
27
- scope: "s",
28
- importance: 0.5,
29
- timestamp: 1,
30
- metadata: "{}",
31
- },
32
- ];
33
- const t = await db.createTable("memories", seed);
34
- if (!withRow) await t.delete('id = "__seed__"');
35
- return t;
36
- }
37
-
38
- describe("MemoryStore schema migration (empty legacy table)", () => {
39
- let dir = "";
40
- afterEach(async () => {
41
- if (dir) await rm(dir, { recursive: true, force: true });
42
- dir = "";
43
- });
44
-
45
- it("migrates an EMPTY legacy table so writes succeed", async () => {
46
- dir = await mkdtemp(join(tmpdir(), "rn-mig-empty-"));
47
- await createLegacyTable(dir, false);
48
-
49
- const store = new MemoryStore({ dbPath: dir, vectorDim: DIM });
50
- const entry = await store.store({
51
- text: "first write after migration",
52
- vector: new Array(DIM).fill(0.1),
53
- category: "other",
54
- scope: "s",
55
- importance: 0.5,
56
- });
57
- expect(entry.id).toBeTruthy();
58
-
59
- const db = await lancedb.connect(dir);
60
- const t = await db.openTable("memories");
61
- const fields = (await t.schema()).fields.map((f) => f.name);
62
- expect(fields).toContain("language");
63
- expect(fields).toContain("fts_text");
64
- });
65
-
66
- it("migrates a NON-EMPTY legacy table and backfills defaults", async () => {
67
- dir = await mkdtemp(join(tmpdir(), "rn-mig-full-"));
68
- await createLegacyTable(dir, true);
69
-
70
- const store = new MemoryStore({ dbPath: dir, vectorDim: DIM });
71
- await store.store({
72
- text: "second row",
73
- vector: new Array(DIM).fill(0.2),
74
- category: "other",
75
- scope: "s",
76
- importance: 0.5,
77
- });
78
-
79
- const db = await lancedb.connect(dir);
80
- const t = await db.openTable("memories");
81
- const rows = await t.query().toArray();
82
- expect(rows).toHaveLength(2);
83
- const legacy = rows.find((r) => r.id === "__seed__") as any;
84
- expect(legacy.language).toBe("en");
85
- expect(legacy.fts_text).toBe("legacy row");
86
- });
87
- });
@@ -1,81 +0,0 @@
1
- import { afterEach, describe, expect, it } from "bun:test";
2
- import { mkdtempSync, rmSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
-
6
- import { 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
- });
@@ -1,201 +0,0 @@
1
- import { afterEach, describe, expect, it } from "bun:test";
2
- import { mkdtempSync, rmSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
-
6
- import { MemoryStore, deterministicId, type MemoryEntry } from "../store.js";
7
- import { getWriteCount } from "../activity-counter.js";
8
-
9
- /**
10
- * P0-2 回归:store()/storeBatch() 改走 upsert(delete+add 入全局 store-write 锁)后,
11
- * 相同 deterministicId 的写入是幂等的 —— 同 (scope,text) 写两次只留一行,不再堆 dup-id。
12
- */
13
-
14
- const tmpDirs: string[] = [];
15
-
16
- function makeStore(): MemoryStore {
17
- const dir = mkdtempSync(join(tmpdir(), "recallnest-upsert-"));
18
- tmpDirs.push(dir);
19
- return new MemoryStore({ dbPath: dir, vectorDim: 3 });
20
- }
21
-
22
- afterEach(() => {
23
- while (tmpDirs.length > 0) {
24
- try {
25
- rmSync(tmpDirs.pop()!, { recursive: true, force: true });
26
- } catch {
27
- /* ignore */
28
- }
29
- }
30
- });
31
-
32
- describe("store P0-2 idempotency (upsert-backed store/storeBatch)", () => {
33
- it("store() of the same (scope,text) twice yields one row, latest wins", async () => {
34
- const store = makeStore();
35
- const scope = "project:upsert-test";
36
- await store.store({ text: "同一条内容", vector: [1, 0, 0], category: "entities", scope, importance: 0.7, metadata: "{}" });
37
- await store.store({ text: "同一条内容", vector: [1, 0, 0], category: "entities", scope, importance: 0.9, metadata: "{}" });
38
-
39
- const rows = await store.list([scope]);
40
- const target = rows.filter((r) => r.text === "同一条内容");
41
- expect(target.length).toBe(1);
42
- expect(target[0].importance).toBeCloseTo(0.9, 5); // upsert overwrote the 0.7 row
43
- });
44
-
45
- it("upsert() with the same id twice overwrites (single row, latest content)", async () => {
46
- const store = makeStore();
47
- const scope = "project:upsert-test";
48
- const id = deterministicId(scope, "anchor");
49
- const base: MemoryEntry = {
50
- id,
51
- text: "v1",
52
- vector: [0, 1, 0],
53
- category: "entities",
54
- scope,
55
- importance: 0.5,
56
- timestamp: Date.now(),
57
- metadata: "{}",
58
- language: "en",
59
- fts_text: "v1",
60
- };
61
- await store.upsert(base);
62
- await store.upsert({ ...base, text: "v2", fts_text: "v2", importance: 0.8 });
63
-
64
- const rows = await store.list([scope]);
65
- const byId = rows.filter((r) => r.id === id);
66
- expect(byId.length).toBe(1);
67
- expect(byId[0].text).toBe("v2");
68
- });
69
-
70
- it("upsert failure leaves the prior row intact (mergeInsert is atomic — no delete+add loss)", async () => {
71
- const store = makeStore(); // vectorDim 3
72
- const scope = "project:upsert-test";
73
- const id = deterministicId(scope, "atomic-anchor");
74
- const good: MemoryEntry = {
75
- id, text: "original", vector: [1, 0, 0], category: "entities", scope, importance: 0.5,
76
- timestamp: Date.now(), metadata: "{}", language: "en", fts_text: "original",
77
- };
78
- await store.upsert(good);
79
-
80
- // Inject a write failure. With the old delete-then-add path the delete would already
81
- // have removed the row before add threw (row lost); the atomic mergeInsert touches
82
- // nothing on failure, so the prior row must survive.
83
- const table = (store as unknown as { table: { mergeInsert: unknown } }).table;
84
- const orig = table.mergeInsert;
85
- table.mergeInsert = () => {
86
- throw new Error("simulated write failure");
87
- };
88
- try {
89
- await expect(store.upsert({ ...good, text: "corrupt", fts_text: "corrupt" })).rejects.toThrow("simulated write failure");
90
- } finally {
91
- table.mergeInsert = orig;
92
- }
93
-
94
- const survivor = (await store.list([scope])).filter((r) => r.id === id);
95
- expect(survivor.length).toBe(1);
96
- expect(survivor[0].text).toBe("original"); // prior row preserved, not lost
97
- });
98
-
99
- it("storeBatch() honors a caller-supplied id (idempotent re-run)", async () => {
100
- const store = makeStore();
101
- const scope = "project:upsert-test";
102
- const fixedId = deterministicId(scope, "batch-anchor");
103
- const entry = {
104
- id: fixedId,
105
- text: "batch-a",
106
- vector: [0, 0, 1] as number[],
107
- category: "entities" as const,
108
- scope,
109
- importance: 0.6,
110
- metadata: "{}",
111
- };
112
- await store.storeBatch([entry]);
113
- await store.storeBatch([{ ...entry, text: "batch-a-updated", fts_text: "batch-a-updated" }]);
114
-
115
- const rows = await store.list([scope]);
116
- const byId = rows.filter((r) => r.id === fixedId);
117
- expect(byId.length).toBe(1);
118
- expect(byId[0].text).toBe("batch-a-updated");
119
- });
120
-
121
- it("storeBatch() collapses in-batch duplicate ids (latest-wins) into one row", async () => {
122
- const store = makeStore();
123
- const scope = "project:upsert-test";
124
- const stored = await store.storeBatch([
125
- { text: "dup-in-batch", vector: [1, 0, 0], category: "entities", scope, importance: 0.3, metadata: "{}" },
126
- { text: "dup-in-batch", vector: [1, 0, 0], category: "entities", scope, importance: 0.8, metadata: "{}" },
127
- ]);
128
- expect(stored).toBe(1); // two same-(scope,text) entries collapse to one
129
- const rows = await store.list([scope]);
130
- const target = rows.filter((r) => r.text === "dup-in-batch");
131
- expect(target.length).toBe(1);
132
- expect(target[0].importance).toBeCloseTo(0.8, 5); // last occurrence wins
133
- });
134
-
135
- // A store whose activity-stats sits in its own unique dir (dirname(dbPath)), so the
136
- // per-scope counter assertions are isolated from other tests' shared tmproot writes.
137
- function makeIsolatedStore(): { store: MemoryStore; statsPath: string } {
138
- const dir = mkdtempSync(join(tmpdir(), "recallnest-count-"));
139
- tmpDirs.push(dir);
140
- const dbPath = join(dir, "lancedb");
141
- return { store: new MemoryStore({ dbPath, vectorDim: 3 }), statsPath: join(dir, "activity-stats.json") };
142
- }
143
-
144
- it("store() increments the per-scope write counter once each (no double-count via upsert)", async () => {
145
- const { store, statsPath } = makeIsolatedStore();
146
- const scope = "cc:project:count-a";
147
- await store.store({ text: "m1", vector: [1, 0, 0], category: "entities", scope, importance: 0.5, metadata: "{}" });
148
- await store.store({ text: "m2", vector: [0, 1, 0], category: "entities", scope, importance: 0.5, metadata: "{}" });
149
- // store()→upsert() counts once per call; two stores ⇒ 2 (not 4).
150
- expect(getWriteCount(scope, { statsPath })).toBe(2);
151
- });
152
-
153
- it("storeBatch() counts per scope by number of entries in that scope", async () => {
154
- const { store, statsPath } = makeIsolatedStore();
155
- await store.storeBatch([
156
- { text: "a", vector: [1, 0, 0], category: "entities", scope: "cc:s1", importance: 0.5, metadata: "{}" },
157
- { text: "b", vector: [0, 1, 0], category: "entities", scope: "cc:s1", importance: 0.5, metadata: "{}" },
158
- { text: "c", vector: [0, 0, 1], category: "entities", scope: "cc:s2", importance: 0.5, metadata: "{}" },
159
- ]);
160
- expect(getWriteCount("cc:s1", { statsPath })).toBe(2);
161
- expect(getWriteCount("cc:s2", { statsPath })).toBe(1);
162
- });
163
-
164
- it("storeBatch() with an in-batch duplicate counts the collapsed row once", async () => {
165
- const { store, statsPath } = makeIsolatedStore();
166
- await store.storeBatch([
167
- { text: "dup", vector: [1, 0, 0], category: "entities", scope: "cc:s3", importance: 0.3, metadata: "{}" },
168
- { text: "dup", vector: [1, 0, 0], category: "entities", scope: "cc:s3", importance: 0.8, metadata: "{}" },
169
- ]);
170
- expect(getWriteCount("cc:s3", { statsPath })).toBe(1); // deduped to one row ⇒ counted once
171
- });
172
-
173
- it("storeBatch() without ids still stores distinct entries", async () => {
174
- const store = makeStore();
175
- const scope = "project:upsert-test";
176
- const stored = await store.storeBatch([
177
- { text: "batch-x", vector: [1, 0, 0], category: "entities", scope, importance: 0.5, metadata: "{}" },
178
- { text: "batch-y", vector: [0, 1, 0], category: "entities", scope, importance: 0.5, metadata: "{}" },
179
- ]);
180
- expect(stored).toBe(2);
181
- const rows = await store.list([scope]);
182
- expect(rows.filter((r) => r.text === "batch-x" || r.text === "batch-y").length).toBe(2);
183
- });
184
- });
185
-
186
- describe("update() vector fallback (2026-07-23 null-vector regression)", () => {
187
- it("metadata-only update preserves the original vector on healthy rows", async () => {
188
- const store = makeStore();
189
- const scope = "project:nullvec-test";
190
- const entry = await store.store({
191
- text: "健康行", vector: [0.1, 0.2, 0.3], category: "events", scope, importance: 0.7, metadata: "{}",
192
- });
193
- const updated = await store.update(entry.id, { metadata: JSON.stringify({ touched: true }) });
194
- expect(updated).not.toBeNull();
195
- // 修复 row.vector null-guard 后,正常行的 vector 必须原样保留(回归保护)
196
- const rows = await store.list([scope]);
197
- expect(rows.length).toBe(1);
198
- const meta = JSON.parse(rows[0].metadata || "{}") as Record<string, unknown>;
199
- expect(meta.touched).toBe(true);
200
- });
201
- });
@@ -1,154 +0,0 @@
1
- import { describe, it, expect } from "bun:test";
2
- import {
3
- buildSupersedeMetadata,
4
- buildSupersedeMetadataForNew,
5
- parseEvolution,
6
- traceEvolution,
7
- patchEvolution,
8
- defaultEvolution,
9
- } from "../memory-evolution.js";
10
-
11
- describe("HP-1: Superseded chain enhancement", () => {
12
- describe("bidirectional linking", () => {
13
- it("buildSupersedeMetadata marks old → supersededBy", () => {
14
- const oldMeta = patchEvolution("{}", defaultEvolution());
15
- const result = buildSupersedeMetadata(oldMeta, "new-id");
16
- const evo = parseEvolution(result);
17
- expect(evo.status).toBe("superseded");
18
- expect(evo.supersededBy).toBe("new-id");
19
- expect(evo.validUntil).toBeGreaterThan(0);
20
- });
21
-
22
- it("buildSupersedeMetadataForNew marks new → supersedes + note", () => {
23
- const newMeta = patchEvolution("{}", defaultEvolution());
24
- const result = buildSupersedeMetadataForNew(newMeta, "old-id", "updated preference");
25
- const evo = parseEvolution(result);
26
- expect(evo.status).toBe("active"); // new memory stays active
27
- expect(evo.supersedes).toBe("old-id");
28
- expect(evo.evolutionNote).toBe("updated preference");
29
- });
30
-
31
- it("bidirectional: old.supersededBy == new.id AND new.supersedes == old.id", () => {
32
- const oldMeta = patchEvolution("{}", defaultEvolution());
33
- const newMeta = patchEvolution("{}", defaultEvolution());
34
-
35
- const updatedOld = buildSupersedeMetadata(oldMeta, "new-id");
36
- const updatedNew = buildSupersedeMetadataForNew(newMeta, "old-id");
37
-
38
- const oldEvo = parseEvolution(updatedOld);
39
- const newEvo = parseEvolution(updatedNew);
40
-
41
- expect(oldEvo.supersededBy).toBe("new-id");
42
- expect(newEvo.supersedes).toBe("old-id");
43
- });
44
- });
45
-
46
- describe("traceEvolution", () => {
47
- it("traces a 3-node chain", async () => {
48
- // v1 → v2 → v3
49
- const entries: Record<string, { metadata: string; timestamp: number }> = {
50
- v1: {
51
- metadata: patchEvolution("{}", {
52
- status: "superseded",
53
- supersededBy: "v2",
54
- validFrom: 1000,
55
- validUntil: 2000,
56
- }),
57
- timestamp: 1000,
58
- },
59
- v2: {
60
- metadata: patchEvolution("{}", {
61
- status: "superseded",
62
- supersedes: "v1",
63
- supersededBy: "v3",
64
- evolutionNote: "added detail",
65
- validFrom: 2000,
66
- validUntil: 3000,
67
- }),
68
- timestamp: 2000,
69
- },
70
- v3: {
71
- metadata: patchEvolution("{}", {
72
- status: "active",
73
- supersedes: "v2",
74
- evolutionNote: "corrected error",
75
- validFrom: 3000,
76
- }),
77
- timestamp: 3000,
78
- },
79
- };
80
-
81
- const getEntry = async (id: string) => entries[id] ?? null;
82
-
83
- // Trace from v2 (middle)
84
- const trace = await traceEvolution("v2", getEntry);
85
- expect(trace.length).toBe(3);
86
- expect(trace[0].id).toBe("v1");
87
- expect(trace[0].direction).toBe("predecessor");
88
- expect(trace[1].id).toBe("v2");
89
- expect(trace[1].direction).toBe("self");
90
- expect(trace[2].id).toBe("v3");
91
- expect(trace[2].direction).toBe("successor");
92
- });
93
-
94
- it("handles single node (no chain)", async () => {
95
- const getEntry = async (id: string) => {
96
- if (id === "solo") return { metadata: patchEvolution("{}", defaultEvolution()), timestamp: 1000 };
97
- return null;
98
- };
99
-
100
- const trace = await traceEvolution("solo", getEntry);
101
- expect(trace.length).toBe(1);
102
- expect(trace[0].direction).toBe("self");
103
- });
104
-
105
- it("respects maxDepth", async () => {
106
- // Create a long chain: v0 → v1 → v2 → ... → v20
107
- const entries: Record<string, { metadata: string; timestamp: number }> = {};
108
- for (let i = 0; i <= 20; i++) {
109
- entries[`v${i}`] = {
110
- metadata: patchEvolution("{}", {
111
- status: i < 20 ? "superseded" : "active",
112
- supersedes: i > 0 ? `v${i - 1}` : null,
113
- supersededBy: i < 20 ? `v${i + 1}` : null,
114
- validFrom: i * 1000,
115
- }),
116
- timestamp: i * 1000,
117
- };
118
- }
119
-
120
- const getEntry = async (id: string) => entries[id] ?? null;
121
- const trace = await traceEvolution("v10", getEntry, 3);
122
- // maxDepth=3: at most 3 predecessors + self + 3 successors = 7
123
- expect(trace.length).toBeLessThanOrEqual(7);
124
- });
125
-
126
- it("handles missing entries gracefully", async () => {
127
- const entries: Record<string, { metadata: string; timestamp: number }> = {
128
- v1: {
129
- metadata: patchEvolution("{}", {
130
- status: "active",
131
- supersedes: "missing",
132
- validFrom: 1000,
133
- }),
134
- timestamp: 1000,
135
- },
136
- };
137
- const getEntry = async (id: string) => entries[id] ?? null;
138
- const trace = await traceEvolution("v1", getEntry);
139
- expect(trace.length).toBe(1); // only self, missing predecessor stops chain
140
- });
141
- });
142
-
143
- describe("backward compatibility", () => {
144
- it("parseEvolution defaults new fields to null", () => {
145
- // Old metadata without supersedes/evolutionNote
146
- const oldMeta = JSON.stringify({
147
- evolution: { status: "active", version: 1, accessCount: 0 },
148
- });
149
- const evo = parseEvolution(oldMeta);
150
- expect(evo.supersedes).toBeNull();
151
- expect(evo.evolutionNote).toBeNull();
152
- });
153
- });
154
- });