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