recallnest 2.6.1 → 3.0.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 (207) hide show
  1. package/CHANGELOG.md +292 -0
  2. package/README.md +280 -195
  3. package/README_CN.md +266 -199
  4. package/config.json.example +8 -1
  5. package/docs/api-reference.md +1 -1
  6. package/integrations/README.md +1 -1
  7. package/integrations/agy/README.md +73 -0
  8. package/integrations/agy/setup.sh +75 -0
  9. package/integrations/setup-all.sh +1 -1
  10. package/package.json +7 -5
  11. package/src/access-tracker.ts +10 -1
  12. package/src/activity-counter.ts +69 -11
  13. package/src/age-label.ts +29 -0
  14. package/src/audit-log.ts +35 -0
  15. package/src/auto-gc.ts +97 -30
  16. package/src/capture-engine.ts +26 -4
  17. package/src/cli.ts +224 -10
  18. package/src/consolidation-engine.ts +317 -123
  19. package/src/context-composer-stable.ts +5 -1
  20. package/src/data-checkup.ts +17 -3
  21. package/src/dream-pipeline.ts +90 -12
  22. package/src/embedder.ts +44 -5
  23. package/src/env-config.ts +71 -0
  24. package/src/forget-engine.ts +8 -2
  25. package/src/gateway-server.ts +272 -0
  26. package/src/ingest.ts +248 -22
  27. package/src/kg-extractor.ts +17 -0
  28. package/src/llm-client.ts +86 -25
  29. package/src/mcp-server.ts +2 -1
  30. package/src/mcp-tools-advanced.ts +7 -2
  31. package/src/mcp-tools-core.ts +11 -1
  32. package/src/mcp-tools-governance.ts +45 -3
  33. package/src/memory-evolution.ts +16 -0
  34. package/src/memory-output.ts +47 -8
  35. package/src/memory-promotion.ts +280 -0
  36. package/src/memory-store-port.ts +32 -3
  37. package/src/memory-utility.ts +314 -0
  38. package/src/pivot-apply.ts +52 -1
  39. package/src/pivot-evidence.ts +128 -0
  40. package/src/ppr-traversal.ts +25 -1
  41. package/src/recall-ledger.ts +76 -0
  42. package/src/retriever.ts +82 -9
  43. package/src/runtime-config.ts +9 -0
  44. package/src/scope-policy.ts +22 -1
  45. package/src/session-engine.ts +11 -4
  46. package/src/session-output.ts +13 -4
  47. package/src/session-store.ts +2 -3
  48. package/src/skill-promotion.ts +218 -1
  49. package/src/store.ts +231 -71
  50. package/src/synthesis-contract.ts +286 -0
  51. package/src/version-manager.ts +83 -0
  52. package/src/workflow-observation-schema.ts +26 -0
  53. package/integrations/gemini-cli/README.md +0 -57
  54. package/integrations/gemini-cli/setup.sh +0 -64
  55. package/src/__tests__/access-tracker-novelty.test.ts +0 -304
  56. package/src/__tests__/activity-counter.test.ts +0 -125
  57. package/src/__tests__/adaptive-fidelity.test.ts +0 -102
  58. package/src/__tests__/admission-control.test.ts +0 -206
  59. package/src/__tests__/anchor-generator.test.ts +0 -67
  60. package/src/__tests__/api-server-security.test.ts +0 -102
  61. package/src/__tests__/auto-consolidation.test.ts +0 -168
  62. package/src/__tests__/auto-recall.test.ts +0 -131
  63. package/src/__tests__/babel-integration.test.ts +0 -144
  64. package/src/__tests__/batch-dedup.test.ts +0 -65
  65. package/src/__tests__/belief-history.test.ts +0 -220
  66. package/src/__tests__/canary-eval.test.ts +0 -114
  67. package/src/__tests__/capture-engine.test.ts +0 -1328
  68. package/src/__tests__/capture-heuristic.test.ts +0 -171
  69. package/src/__tests__/cascade-forget.test.ts +0 -210
  70. package/src/__tests__/category-min-scores.test.ts +0 -116
  71. package/src/__tests__/cc8-cc1-essential-context.test.ts +0 -298
  72. package/src/__tests__/circuit-breaker.test.ts +0 -99
  73. package/src/__tests__/cluster-consolidation.test.ts +0 -945
  74. package/src/__tests__/confidence-metadata.test.ts +0 -125
  75. package/src/__tests__/confidence-tracker.test.ts +0 -261
  76. package/src/__tests__/conflict-advisor.test.ts +0 -206
  77. package/src/__tests__/conflict-escalation.test.ts +0 -149
  78. package/src/__tests__/conflict-lifecycle.test.ts +0 -83
  79. package/src/__tests__/conflict-output.test.ts +0 -85
  80. package/src/__tests__/conflict-store.test.ts +0 -418
  81. package/src/__tests__/connector-v1.test.ts +0 -181
  82. package/src/__tests__/consolidation-engine.test.ts +0 -430
  83. package/src/__tests__/constructive-retrieval.test.ts +0 -509
  84. package/src/__tests__/context-collapse-renderer.test.ts +0 -214
  85. package/src/__tests__/context-composer-pins.test.ts +0 -64
  86. package/src/__tests__/context-composer-task-selection.test.ts +0 -24
  87. package/src/__tests__/context-reconstructor.test.ts +0 -65
  88. package/src/__tests__/context-renderer.test.ts +0 -91
  89. package/src/__tests__/continuity-eval.test.ts +0 -275
  90. package/src/__tests__/continuity-schema.test.ts +0 -97
  91. package/src/__tests__/contracts.test.ts +0 -262
  92. package/src/__tests__/conversation-importer.test.ts +0 -692
  93. package/src/__tests__/core-recall-eval.test.ts +0 -235
  94. package/src/__tests__/core-summary.test.ts +0 -109
  95. package/src/__tests__/data-checkup.test.ts +0 -276
  96. package/src/__tests__/decay-exemption.test.ts +0 -129
  97. package/src/__tests__/dedup-actions.test.ts +0 -58
  98. package/src/__tests__/dedup-guard.test.ts +0 -39
  99. package/src/__tests__/deterministic-id.test.ts +0 -39
  100. package/src/__tests__/distill-lock.test.ts +0 -305
  101. package/src/__tests__/distill-session-argshape.test.ts +0 -79
  102. package/src/__tests__/doctor.test.ts +0 -319
  103. package/src/__tests__/dream-pipeline.test.ts +0 -780
  104. package/src/__tests__/drill-down.test.ts +0 -108
  105. package/src/__tests__/embedder.test.ts +0 -122
  106. package/src/__tests__/emotion-decay.test.ts +0 -46
  107. package/src/__tests__/emotion-detector.test.ts +0 -56
  108. package/src/__tests__/emotion-phase1-integration.test.ts +0 -153
  109. package/src/__tests__/emotion-retrieval.test.ts +0 -56
  110. package/src/__tests__/entity-resolver.test.ts +0 -58
  111. package/src/__tests__/env-config.test.ts +0 -151
  112. package/src/__tests__/error-signature-boost.test.ts +0 -87
  113. package/src/__tests__/error-signature.test.ts +0 -95
  114. package/src/__tests__/error-taxonomy.test.ts +0 -129
  115. package/src/__tests__/ethics-layer.test.ts +0 -476
  116. package/src/__tests__/event-segmenter.test.ts +0 -153
  117. package/src/__tests__/feature-flags-integration.test.ts +0 -54
  118. package/src/__tests__/frequency-tracker.test.ts +0 -113
  119. package/src/__tests__/freshness.test.ts +0 -186
  120. package/src/__tests__/graph-export.test.ts +0 -489
  121. package/src/__tests__/hotness-blend.test.ts +0 -283
  122. package/src/__tests__/ingest-chunking.test.ts +0 -44
  123. package/src/__tests__/ingest.test.ts +0 -360
  124. package/src/__tests__/interference-detector.test.ts +0 -203
  125. package/src/__tests__/kg-extractor.test.ts +0 -192
  126. package/src/__tests__/kg-store.test.ts +0 -300
  127. package/src/__tests__/llm-client-pivot.test.ts +0 -628
  128. package/src/__tests__/llm-consolidation.test.ts +0 -226
  129. package/src/__tests__/maintenance-locks.test.ts +0 -189
  130. package/src/__tests__/mcp-efficiency.test.ts +0 -288
  131. package/src/__tests__/mcp-related-scopes.test.ts +0 -151
  132. package/src/__tests__/memory-boundaries.test.ts +0 -282
  133. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +0 -345
  134. package/src/__tests__/memory-evolution.test.ts +0 -271
  135. package/src/__tests__/memory-health-rebalance.test.ts +0 -144
  136. package/src/__tests__/memory-lint.test.ts +0 -421
  137. package/src/__tests__/memory-output.test.ts +0 -215
  138. package/src/__tests__/memory-promotion.test.ts +0 -315
  139. package/src/__tests__/migrate-dedup-ids.test.ts +0 -65
  140. package/src/__tests__/multi-vector.test.ts +0 -242
  141. package/src/__tests__/narrative-architecture.test.ts +0 -456
  142. package/src/__tests__/noise-filter.test.ts +0 -205
  143. package/src/__tests__/noise-prototype-bank.test.ts +0 -280
  144. package/src/__tests__/obsidian-connector.test.ts +0 -363
  145. package/src/__tests__/omitted-reporting.test.ts +0 -98
  146. package/src/__tests__/pending-review.test.ts +0 -77
  147. package/src/__tests__/pii-redact.test.ts +0 -333
  148. package/src/__tests__/pivot-apply.test.ts +0 -373
  149. package/src/__tests__/pivot-distill-supervisor.test.ts +0 -457
  150. package/src/__tests__/pivot-distill.test.ts +0 -1178
  151. package/src/__tests__/post-retrieval-filter.test.ts +0 -178
  152. package/src/__tests__/ppr-traversal.test.ts +0 -256
  153. package/src/__tests__/prediction-engine.test.ts +0 -349
  154. package/src/__tests__/predictive-prospective-memory.test.ts +0 -416
  155. package/src/__tests__/preference-guard-smoke.test.ts +0 -211
  156. package/src/__tests__/preference-matcher.test.ts +0 -282
  157. package/src/__tests__/preference-slots.test.ts +0 -205
  158. package/src/__tests__/prospective-memory.test.ts +0 -283
  159. package/src/__tests__/pull-from-macbook-sync-only.test.ts +0 -679
  160. package/src/__tests__/query-entity-detector.test.ts +0 -171
  161. package/src/__tests__/query-expander-alias.test.ts +0 -146
  162. package/src/__tests__/query-expander.test.ts +0 -34
  163. package/src/__tests__/recall-gate.test.ts +0 -266
  164. package/src/__tests__/recall-governor.test.ts +0 -227
  165. package/src/__tests__/recall-mode.test.ts +0 -64
  166. package/src/__tests__/resolve-db-path.test.ts +0 -130
  167. package/src/__tests__/resolve-tier.test.ts +0 -64
  168. package/src/__tests__/result-synthesizer.test.ts +0 -198
  169. package/src/__tests__/resume-context.test.ts +0 -4898
  170. package/src/__tests__/retrieval-eval.test.ts +0 -163
  171. package/src/__tests__/retrieval-trace.test.ts +0 -213
  172. package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
  173. package/src/__tests__/retriever-lazy-init-dispatch.test.ts +0 -123
  174. package/src/__tests__/retroactive-boost.test.ts +0 -223
  175. package/src/__tests__/salience-scoring.test.ts +0 -106
  176. package/src/__tests__/scope-inventory.test.ts +0 -200
  177. package/src/__tests__/scope-policy.test.ts +0 -69
  178. package/src/__tests__/scope-suggester.test.ts +0 -80
  179. package/src/__tests__/session-checkpoint.test.ts +0 -291
  180. package/src/__tests__/session-distiller.test.ts +0 -640
  181. package/src/__tests__/session-gc.test.ts +0 -215
  182. package/src/__tests__/skill-engine.test.ts +0 -552
  183. package/src/__tests__/skill-promotion.test.ts +0 -654
  184. package/src/__tests__/skill-verifier.test.ts +0 -122
  185. package/src/__tests__/source-heartbeat.test.ts +0 -250
  186. package/src/__tests__/stable-context-length-contract.test.ts +0 -62
  187. package/src/__tests__/store-patch-metadata.test.ts +0 -205
  188. package/src/__tests__/store-read-consistency.test.ts +0 -122
  189. package/src/__tests__/store-schema-migration.test.ts +0 -87
  190. package/src/__tests__/store-scope-audit.test.ts +0 -81
  191. package/src/__tests__/store-upsert.test.ts +0 -201
  192. package/src/__tests__/supersede-chain.test.ts +0 -154
  193. package/src/__tests__/temporal-parser.test.ts +0 -241
  194. package/src/__tests__/temporal-validity.test.ts +0 -160
  195. package/src/__tests__/term-registry-terminal-roster.test.ts +0 -127
  196. package/src/__tests__/term-registry.test.ts +0 -121
  197. package/src/__tests__/tool-error-surfacing.test.ts +0 -98
  198. package/src/__tests__/tool-output-compressor.test.ts +0 -160
  199. package/src/__tests__/topic-tag.test.ts +0 -113
  200. package/src/__tests__/trusted-memory.test.ts +0 -839
  201. package/src/__tests__/ui-server-security.test.ts +0 -93
  202. package/src/__tests__/usage-tracker.test.ts +0 -287
  203. package/src/__tests__/version-manager.test.ts +0 -213
  204. package/src/__tests__/workflow-observation-skill-outcome.test.ts +0 -352
  205. package/src/__tests__/workflow-observation.test.ts +0 -433
  206. package/src/__tests__/write-verifier.test.ts +0 -122
  207. /package/integrations/{gemini-cli/gemini-md-snippet.md → agy/agy-md-snippet.md} +0 -0
@@ -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
- });