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,386 +0,0 @@
1
- /**
2
- * dream 的 scope 隔离与策略排除(2026-08-16)。
3
- *
4
- * 为什么单开一个文件而不是塞进 dream-pipeline.test.ts:那边的 mock store **吞掉 scope
5
- * 参数**(`list()` 无视 scopeFilter、`vectorSearch(_vec, limit, _threshold, _scopes)` 直接
6
- * 返回全部行),所以哪怕 exact 模式压根没传到 3a,那些测试也照样全绿 —— 互审 C3 指出的
7
- * 正是这一点。本文件两层:
8
- * L1 真实 MemoryStore 验 store 的 family/exact 两种语义(现有 prefix 行为必须保住)
9
- * L2 scope-aware mock 验 dream/3a 是否真把模式传到了每一层取数点
10
- * 外加 L3 策略排除、L4 派生 insight 不参与 3a。
11
- */
12
- import { afterEach, describe, expect, it } from "bun:test";
13
- import { mkdtempSync, rmSync } from "node:fs";
14
- import { tmpdir } from "node:os";
15
- import { join } from "node:path";
16
-
17
- import { MemoryStore, type MemoryEntry } from "../store.js";
18
- import type { MemoryStorePort } from "../memory-store-port.js";
19
- import type { ScopeMatchMode } from "../scope-policy.js";
20
- import { matchesScopeFilter } from "../scope-policy.js";
21
- import { cosineSimilarity } from "../multi-vector.js";
22
- import { runDream, DEFAULT_DREAM_CONFIG } from "../dream-pipeline.js";
23
- import { ConsolidationEngine, DEFAULT_CONSOLIDATION_CONFIG } from "../consolidation-engine.js";
24
- import { getWriteCount, incrementWriteCount, resetWriteCount } from "../activity-counter.js";
25
- import type { LLMClient } from "../llm-client.js";
26
- import type { Embedder } from "../embedder.js";
27
-
28
- const cleanupPaths: string[] = [];
29
-
30
- afterEach(() => {
31
- while (cleanupPaths.length > 0) {
32
- const target = cleanupPaths.pop();
33
- if (target) rmSync(target, { recursive: true, force: true });
34
- }
35
- });
36
-
37
- function tempDb(): string {
38
- const dbPath = mkdtempSync(join(tmpdir(), "recallnest-dream-scope-"));
39
- cleanupPaths.push(dbPath);
40
- return dbPath;
41
- }
42
-
43
- /**
44
- * ⚠️ activity-counter 的默认 statsPath 指向**生产** `data/activity-stats.json`。
45
- * 不传 statsPath 就调 resetWriteCount/incrementWriteCount = 直接改真库的写计数
46
- * (本文件初版就这么干了:把生产的 `memory: 14` 删了,已手工恢复并留 bak)。
47
- * 所有涉及写计数的测试一律走这个临时路径。
48
- */
49
- function tempStatsPath(): { statsPath: string } {
50
- const dir = mkdtempSync(join(tmpdir(), "recallnest-dream-stats-"));
51
- cleanupPaths.push(dir);
52
- return { statsPath: join(dir, "activity-stats.json") };
53
- }
54
-
55
- // ---------------------------------------------------------------------------
56
- // L1 — 真实 MemoryStore:family(历史行为)vs exact(新增)
57
- // ---------------------------------------------------------------------------
58
-
59
- describe("MemoryStore scope matching modes", () => {
60
- async function seeded(): Promise<MemoryStore> {
61
- const store = new MemoryStore({ dbPath: tempDb(), vectorDim: 3 });
62
- await store.store({ text: "rule doc chunk", vector: [1, 0, 0], category: "fact", scope: "memory", importance: 0.5 });
63
- await store.store({ text: "hand-written pivot", vector: [1, 0, 0], category: "fact", scope: "memory:pivot", importance: 0.9 });
64
- await store.store({ text: "unrelated", vector: [0, 1, 0], category: "fact", scope: "project:other", importance: 0.5 });
65
- return store;
66
- }
67
-
68
- it("list: family 模式对无冒号 scope 按前缀(保住既有行为)", async () => {
69
- const store = await seeded();
70
- const rows = await store.list(["memory"], undefined, 100, 0);
71
- expect(rows.map(r => r.scope).sort()).toEqual(["memory", "memory:pivot"]);
72
- });
73
-
74
- it("list: exact 模式只命中同名 scope", async () => {
75
- const store = await seeded();
76
- const rows = await store.list(["memory"], undefined, 100, 0, "exact");
77
- expect(rows.map(r => r.scope)).toEqual(["memory"]);
78
- });
79
-
80
- it("stats: 两种模式的 totalCount 不同", async () => {
81
- const store = await seeded();
82
- expect((await store.stats(["memory"])).totalCount).toBe(2);
83
- expect((await store.stats(["memory"], "exact")).totalCount).toBe(1);
84
- });
85
-
86
- it("vectorSearch: exact 模式不返回兄弟 scope 的行", async () => {
87
- const store = await seeded();
88
- const family = await store.vectorSearch([1, 0, 0], 10, 0.1, ["memory"]);
89
- expect(family.map(r => r.entry.scope).sort()).toEqual(["memory", "memory:pivot"]);
90
-
91
- const exact = await store.vectorSearch([1, 0, 0], 10, 0.1, ["memory"], "exact");
92
- expect(exact.map(r => r.entry.scope)).toEqual(["memory"]);
93
- });
94
-
95
- it("含冒号的 scope 两种模式都精确(family 的隐式规则不受影响)", async () => {
96
- const store = await seeded();
97
- expect((await store.list(["memory:pivot"], undefined, 100, 0)).length).toBe(1);
98
- expect((await store.list(["memory:pivot"], undefined, 100, 0, "exact")).length).toBe(1);
99
- });
100
-
101
- it("exact 模式下 `memory:` 命中空集 —— 拿冒号形当修法会静默 noop", async () => {
102
- // 互审 K2:曾被考虑过的"让 weekly 传 memory: 绕开前缀"路线在这里被钉死为反例。
103
- const store = await seeded();
104
- expect((await store.list(["memory:"], undefined, 100, 0)).length).toBe(0);
105
- });
106
- });
107
-
108
- // ---------------------------------------------------------------------------
109
- // L2/L3/L4 — scope-aware mock:验模式有没有真传到每一层
110
- // ---------------------------------------------------------------------------
111
-
112
- /**
113
- * 与 dream-pipeline.test.ts 的 mock 的关键差别:**这个 mock 认 scope 也认 mode**。
114
- * 漏传 exact 会让下面的断言真的红,这正是它存在的理由。
115
- */
116
- function createScopeAwareStore(entries: MemoryEntry[]) {
117
- const stored: MemoryEntry[] = entries.map(e => ({ ...e }));
118
- let counter = 0;
119
- const patchedIds: string[] = [];
120
-
121
- const select = (scopeFilter?: string[], mode: ScopeMatchMode = "family") =>
122
- stored.filter(e => matchesScopeFilter(e.scope, scopeFilter, mode));
123
-
124
- const store = {
125
- async list(scopeFilter?: string[], _category?: string, limit = 20, offset = 0, scopeMatch: ScopeMatchMode = "family") {
126
- // 复刻生产:list 恒返回假空向量
127
- return select(scopeFilter, scopeMatch).slice(offset, offset + limit).map(e => ({ ...e, vector: [] as number[] }));
128
- },
129
- async listPage(opts: { scopeFilter?: string[]; limit?: number; offset?: number } = {}) {
130
- const { scopeFilter, limit = 1000, offset = 0 } = opts;
131
- return select(scopeFilter).slice(offset, offset + limit).map(e => ({ ...e, vector: [] as number[] }));
132
- },
133
- async getVectors(ids: string[]) {
134
- const m = new Map<string, number[]>();
135
- for (const id of ids) {
136
- const e = stored.find(x => x.id === id);
137
- if (e?.vector?.length) m.set(id, e.vector);
138
- }
139
- return m;
140
- },
141
- async stats(scopeFilter?: string[], scopeMatch: ScopeMatchMode = "family") {
142
- return { totalCount: select(scopeFilter, scopeMatch).length, scopeCounts: {}, categoryCounts: {} };
143
- },
144
- async repairSingletonVersionGroups() { return 0; },
145
- async getById(id: string) {
146
- return stored.find(e => e.id === id) ?? null;
147
- },
148
- async store(entry: Partial<MemoryEntry>) {
149
- const full = {
150
- id: entry.id || `gen-${counter++}`,
151
- text: entry.text || "",
152
- vector: entry.vector || [],
153
- category: entry.category || "events",
154
- scope: entry.scope || "project:test",
155
- importance: entry.importance ?? 0.5,
156
- timestamp: Date.now(),
157
- metadata: entry.metadata || "{}",
158
- } as MemoryEntry;
159
- stored.push(full);
160
- return full;
161
- },
162
- async update(id: string, upd: Partial<MemoryEntry>) {
163
- const e = stored.find(x => x.id === id);
164
- if (e && upd.metadata) e.metadata = upd.metadata;
165
- return e ?? null;
166
- },
167
- async patchMetadata(id: string, patchFn: (m: Record<string, unknown>, e: MemoryEntry) => Record<string, unknown>) {
168
- const e = stored.find(x => x.id === id);
169
- if (!e) return null;
170
- patchedIds.push(id);
171
- e.metadata = JSON.stringify(patchFn(JSON.parse(e.metadata || "{}"), e));
172
- return e;
173
- },
174
- async patchMetadataBatch(
175
- patches: Array<{ id: string; patchFn: (m: Record<string, unknown>, e: MemoryEntry) => Record<string, unknown> }>,
176
- ) {
177
- let written = 0;
178
- for (const { id, patchFn } of patches) {
179
- const e = stored.find(x => x.id === id);
180
- if (!e) continue;
181
- patchedIds.push(id);
182
- e.metadata = JSON.stringify(patchFn(JSON.parse(e.metadata || "{}"), e));
183
- written++;
184
- }
185
- return written;
186
- },
187
- async vectorSearch(vec: number[], limit = 5, minScore = 0.3, scopeFilter?: string[], scopeMatch: ScopeMatchMode = "family") {
188
- return select(scopeFilter, scopeMatch)
189
- .filter(e => e.vector?.length)
190
- .map(e => ({ entry: e, score: cosineSimilarity(vec, e.vector) }))
191
- .filter(r => r.score >= minScore)
192
- .sort((a, b) => b.score - a.score)
193
- .slice(0, limit);
194
- },
195
- };
196
-
197
- return { store: store as unknown as MemoryStorePort, stored, patchedIds };
198
- }
199
-
200
- function entry(over: Partial<MemoryEntry> & { id: string; scope: string }): MemoryEntry {
201
- return {
202
- text: over.text ?? `text-${over.id}`,
203
- vector: over.vector ?? [1, 0, 0],
204
- category: over.category ?? "events",
205
- importance: over.importance ?? 0.7,
206
- timestamp: over.timestamp ?? Date.now(),
207
- metadata: over.metadata ?? JSON.stringify({ evolution: { status: "active", accessCount: 0 } }),
208
- ...over,
209
- } as MemoryEntry;
210
- }
211
-
212
- const mockLLM = {
213
- async generateL0() { return "an insight"; },
214
- async extractPattern() { return "a pattern"; },
215
- } as unknown as LLMClient;
216
-
217
- const mockEmbedder: Pick<Embedder, "embedPassage"> = {
218
- async embedPassage() { return [1, 0, 0]; },
219
- };
220
-
221
- describe("dream scope isolation (exact mode threading)", () => {
222
- it("exact 模式下 pivot 行既不进 insight 来源,也不被 patch", async () => {
223
- const memRows = Array.from({ length: 4 }, (_, i) =>
224
- entry({ id: `mem-${i}`, scope: "memory", text: `memory rule chunk ${i}`, vector: [1, 0.02 * i, 0] }));
225
- const pivotRows = Array.from({ length: 3 }, (_, i) =>
226
- entry({ id: `pivot-${i}`, scope: "memory:pivot", text: `hand-written pivot ${i}`, vector: [1, 0.01 * i, 0] }));
227
- const { store, stored, patchedIds } = createScopeAwareStore([...memRows, ...pivotRows]);
228
- const stats = tempStatsPath();
229
-
230
- await runDream({
231
- store, llm: mockLLM, embedder: mockEmbedder, scope: "memory",
232
- force: true, scopeMatch: "exact", activityStatsPath: stats.statsPath,
233
- });
234
-
235
- // 任何新写入的派生行,其 sourceMemories 都不许含 pivot id
236
- const derivedSources = stored
237
- .map(e => { try { return JSON.parse(e.metadata || "{}"); } catch { return {}; } })
238
- .filter((m: Record<string, unknown>) => m.cluster_insight === true || m.cross_memory_pattern === true)
239
- .flatMap((m: any) => (m?.evolution?.sourceMemories ?? []) as string[]);
240
- expect(derivedSources.filter(id => id.startsWith("pivot-"))).toEqual([]);
241
-
242
- // 也不许有任何 pivot 行被改写 metadata
243
- expect(patchedIds.filter(id => id.startsWith("pivot-"))).toEqual([]);
244
- });
245
-
246
- it("family 模式下 pivot 行会被卷进来 —— 这是修复前的行为,留作对照", async () => {
247
- const memRows = Array.from({ length: 3 }, (_, i) =>
248
- entry({ id: `mem-${i}`, scope: "memory", vector: [1, 0.01 * i, 0] }));
249
- const pivotRows = Array.from({ length: 3 }, (_, i) =>
250
- entry({ id: `pivot-${i}`, scope: "memory:pivot", vector: [1, 0.01 * i, 0] }));
251
- const { store } = createScopeAwareStore([...memRows, ...pivotRows]);
252
-
253
- // 只验取数面:family 下 gather 能看到 6 行,exact 下只有 3 行。
254
- expect((await store.list(["memory"], undefined, 500, 0)).length).toBe(6);
255
- expect((await store.list(["memory"], undefined, 500, 0, "exact")).length).toBe(3);
256
- });
257
- });
258
-
259
- describe("neverDreamScopes policy gate", () => {
260
- it("memory:pivot 被策略排除,force 也不越过", async () => {
261
- const { store, stored, patchedIds } = createScopeAwareStore(
262
- Array.from({ length: 5 }, (_, i) => entry({ id: `pivot-${i}`, scope: "memory:pivot", vector: [1, 0.01 * i, 0] })),
263
- );
264
- const before = stored.length;
265
-
266
- const result = await runDream({
267
- store, llm: mockLLM, embedder: mockEmbedder,
268
- scope: "memory:pivot", force: true, scopeMatch: "exact",
269
- activityStatsPath: tempStatsPath().statsPath,
270
- });
271
-
272
- expect(result.ran).toBe(false);
273
- expect(result.output.kind).toBe("skipped");
274
- expect(result.output.reason).toBe("policy_excluded");
275
- expect(result.output.effectsWritten).toBe(0);
276
- expect(stored.length).toBe(before); // 一行都没新写
277
- expect(patchedIds).toEqual([]); // 一行都没改
278
- });
279
-
280
- it("默认名单里就有 memory:pivot(改这条要连同 open-loops 的翻盘条件一起改)", () => {
281
- expect(DEFAULT_DREAM_CONFIG.neverDreamScopes).toContain("memory:pivot");
282
- // 与 autoExcludeScopes 是两份名单,语义不同,不许合并
283
- expect(DEFAULT_DREAM_CONFIG.autoExcludeScopes).not.toContain("memory:pivot");
284
- });
285
-
286
- it("不在名单里的 scope 照常跑(策略闸不误伤)", async () => {
287
- const { store } = createScopeAwareStore(
288
- Array.from({ length: 4 }, (_, i) => entry({ id: `p-${i}`, scope: "project:normal", vector: [1, 0.01 * i, 0] })),
289
- );
290
- const result = await runDream({
291
- store, llm: mockLLM, embedder: mockEmbedder,
292
- scope: "project:normal", force: true, scopeMatch: "exact",
293
- activityStatsPath: tempStatsPath().statsPath,
294
- });
295
- expect(result.ran).toBe(true);
296
- expect(result.output.reason).not.toBe("policy_excluded");
297
- });
298
- });
299
-
300
- describe("3a: derived insights never participate in dedupe", () => {
301
- it("LLM 派生行不当 canonical,也不把手写原件合并掉", async () => {
302
- // 复刻 2026-08-16 实测的形状:一条手写原件 + 一条它的 LLM 复述,向量几乎重合、
303
- // importance 与 accessCount 都打平 —— 修复前 canonicalScore 平局,派生行赢过原件。
304
- const original = entry({
305
- id: "hand-written",
306
- scope: "memory:pivot",
307
- text: "决定暂缓提交第二个 PR,坚持一次一个 PR 策略,理由是架构级变更需确认维护者意图。",
308
- vector: [1, 0, 0],
309
- importance: 0.7,
310
- metadata: JSON.stringify({
311
- evolution: { status: "active", accessCount: 0 },
312
- canonicalKey: "pivot-decision-sequential-pr-submission",
313
- tags: ["pinned", "src:c98b9988"],
314
- }),
315
- });
316
- const derived = entry({
317
- id: "llm-restatement",
318
- scope: "memory:pivot",
319
- text: "暂缓提交第二个 PR,等待首个 PR 审查反馈。",
320
- vector: [1, 0.001, 0],
321
- importance: 0.7,
322
- metadata: JSON.stringify({
323
- evolution: { status: "active", accessCount: 0, sourceMemories: ["hand-written"] },
324
- cluster_insight: true,
325
- }),
326
- });
327
- const { store, stored } = createScopeAwareStore([original, derived]);
328
-
329
- const engine = new ConsolidationEngine(store, {
330
- ...DEFAULT_CONSOLIDATION_CONFIG,
331
- scopeMatch: "exact",
332
- });
333
- const result = await engine.run("memory:pivot");
334
-
335
- expect(result.mergedCount).toBe(0);
336
- const statusOf = (id: string) => {
337
- const row = stored.find(e => e.id === id)!;
338
- return (JSON.parse(row.metadata || "{}") as any)?.evolution?.status;
339
- };
340
- expect(statusOf("hand-written")).toBe("active"); // 原件没被顶掉
341
- expect(statusOf("llm-restatement")).toBe("active"); // 派生行也没被动,只是不参与
342
- });
343
-
344
- it("两条同为原始记忆时仍正常合并(排除派生 ≠ 关掉去重)", async () => {
345
- const a = entry({
346
- id: "dup-a", scope: "project:dedupe", text: "构建命令是 bun run build", vector: [1, 0, 0], importance: 0.8,
347
- });
348
- const b = entry({
349
- id: "dup-b", scope: "project:dedupe", text: "构建命令为 bun run build", vector: [1, 0.001, 0], importance: 0.5,
350
- });
351
- const { store, stored } = createScopeAwareStore([a, b]);
352
-
353
- const engine = new ConsolidationEngine(store, { ...DEFAULT_CONSOLIDATION_CONFIG, scopeMatch: "exact" });
354
- const result = await engine.run("project:dedupe");
355
-
356
- expect(result.mergedCount).toBe(1);
357
- const loser = stored.find(e => e.id === "dup-b")!;
358
- expect((JSON.parse(loser.metadata || "{}") as any)?.evolution?.status).toBe("consolidated");
359
- });
360
- });
361
-
362
- describe("--auto 的 activity-stats key 一律 exact(回归锚点)", () => {
363
- it("无冒号 key 在 exact 下不再吞掉整个家族", async () => {
364
- // 实测 activity-stats 里有 11 个无冒号 key(cc / global / memory / manual …)。
365
- // `cc` 一旦达标,family 语义会以 LIKE 'cc%' 把 2026-08-14 刚出队的 transcript
366
- // 原文层整批捞回 —— 这条测试锁住"不会再发生"。
367
- const { store } = createScopeAwareStore([
368
- entry({ id: "cc-durable", scope: "cc" }),
369
- entry({ id: "cc-transcript-1", scope: "cc:019f0f70" }),
370
- entry({ id: "cc-transcript-2", scope: "cc:019f60e2" }),
371
- ]);
372
- expect((await store.list(["cc"], undefined, 100, 0)).length).toBe(3);
373
- expect((await store.list(["cc"], undefined, 100, 0, "exact")).map(r => r.id)).toEqual(["cc-durable"]);
374
- });
375
-
376
- it("写计数按具体 scope 记,pivot 与 memory 各记各的(不碰生产 stats 文件)", async () => {
377
- const cfg = tempStatsPath();
378
- await incrementWriteCount("memory", 3, cfg);
379
- await incrementWriteCount("memory:pivot", 5, cfg);
380
- expect(getWriteCount("memory", cfg)).toBe(3);
381
- expect(getWriteCount("memory:pivot", cfg)).toBe(5);
382
- await resetWriteCount("memory", cfg);
383
- expect(getWriteCount("memory", cfg)).toBe(0);
384
- expect(getWriteCount("memory:pivot", cfg)).toBe(5); // 兄弟 scope 不受牵连
385
- });
386
- });
@@ -1,211 +0,0 @@
1
- /**
2
- * 2026-08-14 dream 写爆库根治的写入层回归:
3
- * - store.patchMetadataBatch:真 LanceDB 实例上的批量读改写语义(锁内最新行起底、
4
- * 按序应用、scope 过滤、单条失败跳过)
5
- * - activity-counter.pruneWriteCounts:transcript 计数清扫(含未达标条目)
6
- * - auto-gc 批量归档:maxArchivePerRun 命中时不弃单(flush 后再停)
7
- */
8
- import { afterAll, describe, expect, it } from "bun:test";
9
- import { mkdtempSync, rmSync } from "node:fs";
10
- import { tmpdir } from "node:os";
11
- import { join } from "node:path";
12
-
13
- import { MemoryStore, type MemoryEntry } from "../store.js";
14
- import { incrementWriteCount, getWriteCount, pruneWriteCounts, listScopesAboveThreshold } from "../activity-counter.js";
15
- import { isTranscriptScope } from "../memory-boundaries.js";
16
- import { maybeRunGc, resetGcTimestamp } from "../auto-gc.js";
17
-
18
- const tmpDirs: string[] = [];
19
- afterAll(() => {
20
- for (const d of tmpDirs) rmSync(d, { recursive: true, force: true });
21
- });
22
-
23
- function makeIsolatedStore(): { store: MemoryStore; statsPath: string } {
24
- const dir = mkdtempSync(join(tmpdir(), "rn-batch-"));
25
- tmpDirs.push(dir);
26
- return { store: new MemoryStore({ dbPath: join(dir, "lancedb"), vectorDim: 3 }), statsPath: join(dir, "activity-stats.json") };
27
- }
28
-
29
- describe("store.patchMetadataBatch(真 LanceDB)", () => {
30
- it("一次调用批量写 N 行,最终态各自生效", async () => {
31
- const { store } = makeIsolatedStore();
32
- const scope = "project:batch";
33
- const a = await store.store({ text: "row a", vector: [1, 0, 0], category: "events", scope, importance: 0.5, metadata: "{}" });
34
- const b = await store.store({ text: "row b", vector: [0, 1, 0], category: "events", scope, importance: 0.5, metadata: "{}" });
35
-
36
- const written = await store.patchMetadataBatch([
37
- { id: a.id, patchFn: meta => ({ ...meta, marker: "A" }) },
38
- { id: b.id, patchFn: meta => ({ ...meta, marker: "B" }) },
39
- ], [scope]);
40
-
41
- expect(written).toBe(2);
42
- expect(JSON.parse((await store.getById(a.id))!.metadata!).marker).toBe("A");
43
- expect(JSON.parse((await store.getById(b.id))!.metadata!).marker).toBe("B");
44
- });
45
-
46
- it("patchFn 以库中最新行起底:批量写不吞前一次独立 update 写入的键", async () => {
47
- const { store } = makeIsolatedStore();
48
- const scope = "project:fresh";
49
- const a = await store.store({ text: "row", vector: [1, 0, 0], category: "events", scope, importance: 0.5, metadata: "{}" });
50
- // 先经独立通道写入一个键(模拟 3a 已提交的 version_group)
51
- await store.update(a.id, { metadata: JSON.stringify({ version_group: "vg-keep" }) }, [scope]);
52
- // 批量 patch 只声明自己的键 —— 旧"预物化整串"接口在这里会把 version_group 抹掉
53
- await store.patchMetadataBatch([
54
- { id: a.id, patchFn: meta => ({ ...meta, consolidatedInto: "x" }) },
55
- ], [scope]);
56
- const final = JSON.parse((await store.getById(a.id))!.metadata!);
57
- expect(final.version_group).toBe("vg-keep");
58
- expect(final.consolidatedInto).toBe("x");
59
- });
60
-
61
- it("同批 patchFn 按数组序执行(闭包可传递决策)", async () => {
62
- const { store } = makeIsolatedStore();
63
- const scope = "project:seq";
64
- const a = await store.store({ text: "first", vector: [1, 0, 0], category: "events", scope, importance: 0.5, metadata: "{}" });
65
- const b = await store.store({ text: "second", vector: [0, 1, 0], category: "events", scope, importance: 0.5, metadata: "{}" });
66
- let decided: string | null = null;
67
- await store.patchMetadataBatch([
68
- { id: a.id, patchFn: meta => { decided = "from-first"; return { ...meta, tag: "lead" }; } },
69
- { id: b.id, patchFn: meta => ({ ...meta, tag: decided }) },
70
- ], [scope]);
71
- expect(JSON.parse((await store.getById(b.id))!.metadata!).tag).toBe("from-first");
72
- });
73
-
74
- it("scope 过滤拒绝越权行、不存在的 id 跳过、空数组返回 0", async () => {
75
- const { store } = makeIsolatedStore();
76
- const a = await store.store({ text: "in scope", vector: [1, 0, 0], category: "events", scope: "project:x", importance: 0.5, metadata: "{}" });
77
- const written = await store.patchMetadataBatch([
78
- { id: a.id, patchFn: meta => ({ ...meta, touched: true }) },
79
- { id: "00000000-0000-4000-8000-000000000000", patchFn: meta => meta },
80
- ], ["project:other"]); // a 不在此 scope
81
- expect(written).toBe(0);
82
- expect(JSON.parse((await store.getById(a.id))!.metadata!).touched).toBeUndefined();
83
- expect(await store.patchMetadataBatch([], ["project:x"])).toBe(0);
84
- });
85
-
86
- it("单条 patchFn 抛错跳过该条,其余照写", async () => {
87
- const { store } = makeIsolatedStore();
88
- const scope = "project:err";
89
- const a = await store.store({ text: "bad", vector: [1, 0, 0], category: "events", scope, importance: 0.5, metadata: "{}" });
90
- const b = await store.store({ text: "good", vector: [0, 1, 0], category: "events", scope, importance: 0.5, metadata: "{}" });
91
- const written = await store.patchMetadataBatch([
92
- { id: a.id, patchFn: () => { throw new Error("boom"); } },
93
- { id: b.id, patchFn: meta => ({ ...meta, ok: true }) },
94
- ], [scope]);
95
- expect(written).toBe(1);
96
- expect(JSON.parse((await store.getById(b.id))!.metadata!).ok).toBe(true);
97
- expect(JSON.parse((await store.getById(a.id))!.metadata!).ok).toBeUndefined();
98
- });
99
- });
100
-
101
- describe("pruneWriteCounts(transcript 计数清扫)", () => {
102
- it("清掉全部 transcript 条目(达标与未达标),durable 一律保留", async () => {
103
- const dir = mkdtempSync(join(tmpdir(), "rn-prune-"));
104
- tmpDirs.push(dir);
105
- const cfg = { statsPath: join(dir, "activity-stats.json") };
106
- await incrementWriteCount("codex:aaaa1111", 15, cfg); // transcript 达标
107
- await incrementWriteCount("kimi:bbbb2222", 3, cfg); // transcript 未达标
108
- await incrementWriteCount("project:harness", 12, cfg); // durable 达标
109
- await incrementWriteCount("memory", 2, cfg); // durable 未达标
110
-
111
- const removed = await pruneWriteCounts(isTranscriptScope, cfg);
112
-
113
- expect(removed.sort()).toEqual(["codex:aaaa1111", "kimi:bbbb2222"]);
114
- expect(getWriteCount("codex:aaaa1111", cfg)).toBe(0);
115
- expect(getWriteCount("kimi:bbbb2222", cfg)).toBe(0);
116
- expect(getWriteCount("project:harness", cfg)).toBe(12);
117
- expect(getWriteCount("memory", cfg)).toBe(2);
118
- expect(listScopesAboveThreshold(10, cfg)).toEqual(["project:harness"]);
119
- });
120
- });
121
-
122
- describe("auto-gc 批量归档的上限语义", () => {
123
- it("maxArchivePerRun 命中时已选中的候选照常提交(flush 不弃单)", async () => {
124
- resetGcTimestamp();
125
- const oldTs = Date.now() - 120 * 86_400_000;
126
- const activeMeta = (): string => JSON.stringify({
127
- evolution: { status: "active", version: 1, accessCount: 0, lastAccessedAt: null, validFrom: oldTs, validUntil: null },
128
- });
129
- const rows: Array<{ id: string; text: string; importance: number; timestamp: number; metadata: string; category: string; scope: string }> = [
130
- { id: "g1", text: "old1", importance: 0.1, timestamp: oldTs, metadata: activeMeta(), category: "events", scope: "project:gc" },
131
- { id: "g2", text: "old2", importance: 0.1, timestamp: oldTs, metadata: activeMeta(), category: "events", scope: "project:gc" },
132
- { id: "g3", text: "old3", importance: 0.1, timestamp: oldTs, metadata: activeMeta(), category: "events", scope: "project:gc" },
133
- ];
134
- const batchCalls: number[] = [];
135
- const store = {
136
- async stats() { return { totalCount: 100, scopeCounts: {}, categoryCounts: {} }; },
137
- async repairSingletonVersionGroups() { return 0; },
138
- async list() { return rows; },
139
- async listPage(opts: { limit?: number; offset?: number } = {}) {
140
- const { limit = 1000, offset = 0 } = opts;
141
- return rows.slice(offset, offset + limit);
142
- },
143
- async update() { return null; },
144
- async patchMetadataBatch(
145
- patches: Array<{ id: string; patchFn: (meta: Record<string, unknown>, entry: unknown) => Record<string, unknown> }>,
146
- ) {
147
- batchCalls.push(patches.length);
148
- for (const { id, patchFn } of patches) {
149
- const row = rows.find(r => r.id === id);
150
- if (!row) continue;
151
- row.metadata = JSON.stringify(patchFn(JSON.parse(row.metadata) as Record<string, unknown>, row));
152
- }
153
- return patches.length;
154
- },
155
- };
156
-
157
- const result = await maybeRunGc(
158
- store as unknown as Parameters<typeof maybeRunGc>[0],
159
- { minMemoryCount: 1, minHoursSinceLastGc: 0, decayScoreThreshold: 0.99, maxArchivePerRun: 2, minAgeDays: 30 },
160
- );
161
-
162
- expect(result.archivedCount).toBe(2); // 上限生效
163
- const archived = rows.filter(r => (JSON.parse(r.metadata) as { evolution: { status: string } }).evolution.status === "archived");
164
- expect(archived.length).toBe(2); // 且真的写进去了(break 前 flush,不弃单)
165
- expect(batchCalls.reduce((a, b) => a + b, 0)).toBe(2);
166
- });
167
-
168
- it("即使低于归档数量门槛也会自愈 singleton version-group", async () => {
169
- resetGcTimestamp();
170
- let repairCalls = 0;
171
- const store = {
172
- async stats() { return { totalCount: 100, scopeCounts: {}, categoryCounts: {} }; },
173
- async repairSingletonVersionGroups() { repairCalls++; return 1; },
174
- async listPage() { return []; },
175
- async patchMetadataBatch() { return 0; },
176
- };
177
-
178
- const result = await maybeRunGc(
179
- store as unknown as Parameters<typeof maybeRunGc>[0],
180
- { minMemoryCount: 1000, minHoursSinceLastGc: 0, decayScoreThreshold: 0.15, maxArchivePerRun: 100, minAgeDays: 30 },
181
- );
182
-
183
- expect(repairCalls).toBe(1);
184
- expect(result.triggered).toBe(false);
185
- expect(result.reason).toBe("below_memory_threshold");
186
- expect(result.dissolvedVersionGroups).toBe(1);
187
- });
188
-
189
- it("单成员组自愈有独立节流,不会被每个 dream scope 重复全库扫描", async () => {
190
- resetGcTimestamp();
191
- let repairCalls = 0;
192
- const store = {
193
- async stats() { return { totalCount: 100, scopeCounts: {}, categoryCounts: {} }; },
194
- async repairSingletonVersionGroups() { repairCalls++; return 0; },
195
- async listPage() { return []; },
196
- async patchMetadataBatch() { return 0; },
197
- };
198
- const config = {
199
- minMemoryCount: 1000,
200
- minHoursSinceLastGc: 24,
201
- decayScoreThreshold: 0.15,
202
- maxArchivePerRun: 100,
203
- minAgeDays: 30,
204
- };
205
-
206
- await maybeRunGc(store as unknown as Parameters<typeof maybeRunGc>[0], config);
207
- await maybeRunGc(store as unknown as Parameters<typeof maybeRunGc>[0], config);
208
-
209
- expect(repairCalls).toBe(1);
210
- });
211
- });