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,315 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
-
3
- import {
4
- scanMemoryPromotions,
5
- type PromoteScanDeps,
6
- type PromoteRequest,
7
- } from "../memory-promotion.js";
8
- import type { MemoryEntry } from "../store.js";
9
- import type { StoredPromotedMemoryRecord } from "../memory-schema.js";
10
-
11
- // ---------------------------------------------------------------------------
12
- // Mock helpers
13
- // ---------------------------------------------------------------------------
14
-
15
- let idSeq = 0;
16
-
17
- const ACTIVE_EVOLUTION = {
18
- status: "active",
19
- version: 1,
20
- accessCount: 0,
21
- lastAccessedAt: null,
22
- supersededBy: null,
23
- consolidatedInto: null,
24
- sourceMemories: [],
25
- validFrom: 1_700_000_000_000,
26
- validUntil: null,
27
- };
28
-
29
- /**
30
- * Build a transcript-downgraded evidence entry (category=events) with an EMPTY
31
- * vector, faithfully reproducing store.list — which never returns vectors.
32
- */
33
- function makeDowngradedEvent(
34
- downgradedFrom: "profile" | "preferences",
35
- overrides: Partial<MemoryEntry> = {},
36
- ): MemoryEntry {
37
- idSeq += 1;
38
- return {
39
- id: `ev-${String(idSeq).padStart(4, "0")}`,
40
- text: `${downgradedFrom} hint #${idSeq}`,
41
- vector: [], // list() never returns vectors
42
- category: "events",
43
- scope: "cc:project:test",
44
- importance: 0.7,
45
- timestamp: 1_700_000_000_000 + idSeq,
46
- metadata: JSON.stringify({
47
- boundary: {
48
- layer: "evidence",
49
- authority: "transcript-ingest",
50
- conflictPolicy: "append-only",
51
- originalCategory: downgradedFrom,
52
- downgradedFrom,
53
- },
54
- evolution: ACTIVE_EVOLUTION,
55
- }),
56
- ...overrides,
57
- };
58
- }
59
-
60
- interface MockOptions {
61
- /** Per-id vector overrides; ids absent from the map get no vector. */
62
- vectorMap?: Map<string, number[]>;
63
- /** Fallback vector handed to every listed id when vectorMap is omitted. */
64
- defaultVector?: number[];
65
- }
66
-
67
- function createScanDeps(entries: MemoryEntry[], options: MockOptions = {}) {
68
- const promoteCalls: PromoteRequest[] = [];
69
- const deps: PromoteScanDeps = {
70
- store: {
71
- async list(
72
- _scopeFilter?: string[],
73
- category?: string,
74
- _limit?: number,
75
- _offset?: number,
76
- ): Promise<MemoryEntry[]> {
77
- return category ? entries.filter((e) => e.category === category) : entries;
78
- },
79
- async getVectors(ids: string[]): Promise<Map<string, number[]>> {
80
- const map = new Map<string, number[]>();
81
- for (const id of ids) {
82
- if (options.vectorMap?.has(id)) {
83
- map.set(id, options.vectorMap.get(id)!);
84
- } else if (options.defaultVector && !options.vectorMap) {
85
- map.set(id, options.defaultVector);
86
- }
87
- }
88
- return map;
89
- },
90
- },
91
- async promote(req: PromoteRequest): Promise<StoredPromotedMemoryRecord> {
92
- promoteCalls.push(req);
93
- return {
94
- text: "promoted text",
95
- category: req.category,
96
- importance: req.importance,
97
- scope: req.scope,
98
- source: "agent",
99
- tags: [],
100
- canonicalKey: `${req.category}:k${promoteCalls.length}`,
101
- id: `durable-${req.memoryId}`,
102
- resolvedScope: req.scope,
103
- storedAt: "2026-05-29T00:00:00.000Z",
104
- disposition: "promoted",
105
- sourceMemoryId: req.memoryId,
106
- sourceCategory: "events",
107
- };
108
- },
109
- };
110
- return { deps, promoteCalls };
111
- }
112
-
113
- // ---------------------------------------------------------------------------
114
- // Tests
115
- // ---------------------------------------------------------------------------
116
-
117
- describe("scanMemoryPromotions", () => {
118
- it("promotes a cluster once it has >= minOccurrences similar members", async () => {
119
- const entries = [
120
- makeDowngradedEvent("preferences"),
121
- makeDowngradedEvent("preferences"),
122
- makeDowngradedEvent("preferences"),
123
- ];
124
- const { deps, promoteCalls } = createScanDeps(entries, { defaultVector: [1, 0, 0] });
125
-
126
- const result = await scanMemoryPromotions(deps, "cc:project:test", { dryRun: false });
127
-
128
- expect(result.candidates.length).toBe(1);
129
- expect(result.candidates[0].occurrences).toBe(3);
130
- expect(result.candidates[0].downgradedFrom).toBe("preferences");
131
- expect(result.promoted).toBe(1);
132
- expect(promoteCalls.length).toBe(1);
133
- expect(promoteCalls[0].category).toBe("preferences");
134
- expect(promoteCalls[0].scope).toBe("cc:project:test");
135
- });
136
-
137
- it("does not promote when occurrences are below minOccurrences", async () => {
138
- const entries = [makeDowngradedEvent("preferences"), makeDowngradedEvent("preferences")];
139
- const { deps, promoteCalls } = createScanDeps(entries, { defaultVector: [1, 0, 0] });
140
-
141
- const result = await scanMemoryPromotions(deps, "cc:project:test", { dryRun: false });
142
-
143
- expect(result.candidates.length).toBe(0);
144
- expect(promoteCalls.length).toBe(0);
145
- });
146
-
147
- it("does not promote a cluster whose average importance is below minImportance", async () => {
148
- const entries = [
149
- makeDowngradedEvent("preferences", { importance: 0.4 }),
150
- makeDowngradedEvent("preferences", { importance: 0.4 }),
151
- makeDowngradedEvent("preferences", { importance: 0.4 }),
152
- ];
153
- const { deps, promoteCalls } = createScanDeps(entries, { defaultVector: [1, 0, 0] });
154
-
155
- const result = await scanMemoryPromotions(deps, "cc:project:test", { dryRun: false });
156
-
157
- expect(result.candidates.length).toBe(0);
158
- expect(promoteCalls.length).toBe(0);
159
- });
160
-
161
- it("promotes with the target category equal to downgradedFrom (profile)", async () => {
162
- const entries = [
163
- makeDowngradedEvent("profile"),
164
- makeDowngradedEvent("profile"),
165
- makeDowngradedEvent("profile"),
166
- ];
167
- const { deps, promoteCalls } = createScanDeps(entries, { defaultVector: [1, 0, 0] });
168
-
169
- await scanMemoryPromotions(deps, "cc:project:test", { dryRun: false });
170
-
171
- expect(promoteCalls.length).toBe(1);
172
- expect(promoteCalls[0].category).toBe("profile");
173
- });
174
-
175
- it("finds candidates but does NOT call promote in dryRun mode", async () => {
176
- const entries = [
177
- makeDowngradedEvent("preferences"),
178
- makeDowngradedEvent("preferences"),
179
- makeDowngradedEvent("preferences"),
180
- ];
181
- const { deps, promoteCalls } = createScanDeps(entries, { defaultVector: [1, 0, 0] });
182
-
183
- const result = await scanMemoryPromotions(deps, "cc:project:test", { dryRun: true });
184
-
185
- expect(result.candidates.length).toBe(1);
186
- expect(result.dryRun).toBe(true);
187
- expect(result.promoted).toBe(0);
188
- expect(result.candidates[0].promoted).toBeNull();
189
- expect(promoteCalls.length).toBe(0);
190
- });
191
-
192
- it("keeps profile and preferences as separate candidates with their own category", async () => {
193
- const entries = [
194
- makeDowngradedEvent("preferences"),
195
- makeDowngradedEvent("preferences"),
196
- makeDowngradedEvent("preferences"),
197
- makeDowngradedEvent("profile"),
198
- makeDowngradedEvent("profile"),
199
- makeDowngradedEvent("profile"),
200
- ];
201
- const { deps, promoteCalls } = createScanDeps(entries, { defaultVector: [1, 0, 0] });
202
-
203
- const result = await scanMemoryPromotions(deps, "cc:project:test", { dryRun: false });
204
-
205
- expect(result.candidates.length).toBe(2);
206
- expect(promoteCalls.length).toBe(2);
207
- expect(promoteCalls.map((c) => c.category).sort()).toEqual(["preferences", "profile"]);
208
- });
209
-
210
- it("skips entries that have no vector available", async () => {
211
- const entries = [
212
- makeDowngradedEvent("preferences"),
213
- makeDowngradedEvent("preferences"),
214
- makeDowngradedEvent("preferences"),
215
- ];
216
- // Only 2 of 3 receive vectors -> the cluster has 2 members < minOccurrences.
217
- const vectorMap = new Map<string, number[]>();
218
- vectorMap.set(entries[0].id, [1, 0, 0]);
219
- vectorMap.set(entries[1].id, [1, 0, 0]);
220
- const { deps, promoteCalls } = createScanDeps(entries, { vectorMap });
221
-
222
- const result = await scanMemoryPromotions(deps, "cc:project:test", { dryRun: false });
223
-
224
- expect(result.candidates.length).toBe(0);
225
- expect(promoteCalls.length).toBe(0);
226
- });
227
-
228
- it("ignores events that are not downgraded from profile/preferences", async () => {
229
- const plain = (): MemoryEntry =>
230
- makeDowngradedEvent("preferences", {
231
- metadata: JSON.stringify({
232
- boundary: {
233
- layer: "evidence",
234
- authority: "transcript-ingest",
235
- conflictPolicy: "append-only",
236
- },
237
- evolution: ACTIVE_EVOLUTION,
238
- }),
239
- });
240
- const { deps, promoteCalls } = createScanDeps([plain(), plain(), plain()], {
241
- defaultVector: [1, 0, 0],
242
- });
243
-
244
- const result = await scanMemoryPromotions(deps, "cc:project:test", { dryRun: false });
245
-
246
- expect(result.candidates.length).toBe(0);
247
- expect(promoteCalls.length).toBe(0);
248
- });
249
-
250
- it("skips evidence already promoted (promotedTo marker present)", async () => {
251
- const promotedAlready = (): MemoryEntry =>
252
- makeDowngradedEvent("preferences", {
253
- metadata: JSON.stringify({
254
- boundary: {
255
- layer: "evidence",
256
- authority: "transcript-ingest",
257
- conflictPolicy: "append-only",
258
- originalCategory: "preferences",
259
- downgradedFrom: "preferences",
260
- },
261
- promotedTo: "existing-durable-id",
262
- evolution: ACTIVE_EVOLUTION,
263
- }),
264
- });
265
- const { deps, promoteCalls } = createScanDeps(
266
- [promotedAlready(), promotedAlready(), promotedAlready()],
267
- { defaultVector: [1, 0, 0] },
268
- );
269
-
270
- const result = await scanMemoryPromotions(deps, "cc:project:test", { dryRun: false });
271
-
272
- expect(result.candidates.length).toBe(0);
273
- expect(promoteCalls.length).toBe(0);
274
- });
275
-
276
- it("excludes inactive (superseded) evidence", async () => {
277
- const superseded = (): MemoryEntry =>
278
- makeDowngradedEvent("preferences", {
279
- metadata: JSON.stringify({
280
- boundary: {
281
- layer: "evidence",
282
- authority: "transcript-ingest",
283
- conflictPolicy: "append-only",
284
- originalCategory: "preferences",
285
- downgradedFrom: "preferences",
286
- },
287
- evolution: { ...ACTIVE_EVOLUTION, status: "superseded", supersededBy: "x" },
288
- }),
289
- });
290
- const { deps, promoteCalls } = createScanDeps(
291
- [superseded(), superseded(), superseded()],
292
- { defaultVector: [1, 0, 0] },
293
- );
294
-
295
- const result = await scanMemoryPromotions(deps, "cc:project:test", { dryRun: false });
296
-
297
- expect(result.candidates.length).toBe(0);
298
- expect(promoteCalls.length).toBe(0);
299
- });
300
-
301
- it("excludes evidence whose text is a bridge-injected prompt (noise)", async () => {
302
- const promptText = "# CC Reply Prompt\n\n你是 CC。你不是 Alice 的数字分身,也不是 E 化镜像。";
303
- const entries = [
304
- makeDowngradedEvent("preferences", { text: promptText }),
305
- makeDowngradedEvent("preferences", { text: promptText }),
306
- makeDowngradedEvent("preferences", { text: promptText }),
307
- ];
308
- const { deps, promoteCalls } = createScanDeps(entries, { defaultVector: [1, 0, 0] });
309
-
310
- const result = await scanMemoryPromotions(deps, "cc:project:test", { dryRun: false });
311
-
312
- expect(result.candidates.length).toBe(0);
313
- expect(promoteCalls.length).toBe(0);
314
- });
315
- });
@@ -1,256 +0,0 @@
1
- import { describe, expect, it, beforeEach } from "bun:test";
2
- import { mkdtempSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
-
6
- import {
7
- applyUtilityToMeta,
8
- computeMemoryUtility,
9
- formatMemoryUtility,
10
- JOIN_WEIGHT,
11
- linkMemoryToObservations,
12
- OUTCOME_REWARD,
13
- readUtility,
14
- summarizeMemoryOutcomes,
15
- UTILITY_FIELD,
16
- } from "../memory-utility.js";
17
- import { buildWorkflowObservationRecord } from "../workflow-observation-engine.js";
18
- import { WorkflowObservationStore } from "../workflow-observation-store.js";
19
- import {
20
- PROCESS_READER_ID,
21
- RECALLED_IDS_CAP,
22
- recallLedgerSize,
23
- recentRecallHits,
24
- recordRecallHits,
25
- resetRecallLedger,
26
- } from "../recall-ledger.js";
27
- import { AccessTracker } from "../access-tracker.js";
28
- import { isDecayExempt, resolveTier } from "../decay-engine.js";
29
-
30
- const MEM_A = "aaaaaaaa-0000-4000-8000-000000000001";
31
- const MEM_B = "bbbbbbbb-0000-4000-8000-000000000002";
32
-
33
- function obs(overrides: Record<string, unknown>) {
34
- return buildWorkflowObservationRecord({
35
- workflowId: "resume_context",
36
- summary: "test observation",
37
- ...overrides,
38
- });
39
- }
40
-
41
- describe("recall ledger (P0 join key)", () => {
42
- beforeEach(() => resetRecallLedger());
43
-
44
- it("records hits and returns them newest-first within the window", () => {
45
- const t0 = 1_000_000;
46
- recordRecallHits([MEM_A], t0);
47
- recordRecallHits([MEM_B], t0 + 1000);
48
- expect(recentRecallHits(60_000, 10, t0 + 2000)).toEqual([MEM_B, MEM_A]);
49
- });
50
-
51
- it("drops hits older than the window", () => {
52
- const t0 = 1_000_000;
53
- recordRecallHits([MEM_A], t0);
54
- recordRecallHits([MEM_B], t0 + 60 * 60_000);
55
- expect(recentRecallHits(30 * 60_000, 10, t0 + 60 * 60_000)).toEqual([MEM_B]);
56
- });
57
-
58
- it("caps the returned list", () => {
59
- const ids = Array.from({ length: RECALLED_IDS_CAP + 20 }, (_, i) => `id-${i}`);
60
- recordRecallHits(ids, 5_000);
61
- expect(recentRecallHits(60_000, RECALLED_IDS_CAP, 5_000).length).toBe(RECALLED_IDS_CAP);
62
- });
63
-
64
- it("gives every AccessTracker the same process-level reader id", () => {
65
- // 回归防线:readerId 曾是 per-instance 随机的,而 components 按 profile 缓存 —— 同一个
66
- // 会话开两个 profile 就伪造出两个 reader,join 直接失效。
67
- const fakeStore = {} as never;
68
- expect(new AccessTracker(fakeStore).readerId).toBe(PROCESS_READER_ID);
69
- expect(new AccessTracker(fakeStore).readerId).toBe(new AccessTracker(fakeStore).readerId);
70
- expect(new AccessTracker(fakeStore, undefined, "r-explicit").readerId).toBe("r-explicit");
71
- });
72
-
73
- it("records into the ledger before the novelty/cooldown gate", () => {
74
- // 高相似度 + 严格 novelty 闸:强化会被挡下,但 agent 确实看到了这条 —— 账本必须记。
75
- const fakeStore = {} as never;
76
- const tracker = new AccessTracker(fakeStore, {
77
- flushIntervalMs: 5000,
78
- reinforcementFactor: 0.5,
79
- maxMultiplier: 3,
80
- accessFreshnessHalfLifeDays: 30,
81
- noveltyThreshold: 0.9,
82
- cooldownMs: 0,
83
- });
84
- tracker.recordAccess([MEM_A], [0.99]);
85
- expect(tracker.pendingCount).toBe(0);
86
- expect(recentRecallHits()).toContain(MEM_A);
87
- tracker.destroy();
88
- });
89
- });
90
-
91
- describe("memory ↔ observation join", () => {
92
- it("joins exactly when the observation names the memory", () => {
93
- const links = linkMemoryToObservations(MEM_A, "{}", [
94
- obs({ recalledIds: [MEM_A, MEM_B], outcome: "success" }),
95
- obs({ recalledIds: [MEM_B], outcome: "failure" }),
96
- ]);
97
- expect(links.length).toBe(1);
98
- expect(links[0].join).toBe("exact");
99
- });
100
-
101
- it("falls back to session join through readerId ∩ metadata.readerIds", () => {
102
- const meta = JSON.stringify({ readerIds: ["r-1234abcd"] });
103
- const links = linkMemoryToObservations(MEM_A, meta, [
104
- obs({ readerId: "r-1234abcd", outcome: "corrected" }),
105
- obs({ readerId: "r-otherxx", outcome: "failure" }),
106
- ]);
107
- expect(links.length).toBe(1);
108
- expect(links[0].join).toBe("session");
109
- expect(links[0].outcome).toBe("corrected");
110
- });
111
-
112
- it("prefers exact over session for the same observation", () => {
113
- const meta = JSON.stringify({ readerIds: ["r-1234abcd"] });
114
- const links = linkMemoryToObservations(MEM_A, meta, [
115
- obs({ readerId: "r-1234abcd", recalledIds: [MEM_A], outcome: "success" }),
116
- ]);
117
- expect(links.length).toBe(1);
118
- expect(links[0].join).toBe("exact");
119
- });
120
-
121
- it("returns nothing when neither key is present (the pre-P0 state)", () => {
122
- expect(linkMemoryToObservations(MEM_A, "{}", [obs({ outcome: "success" })])).toEqual([]);
123
- });
124
- });
125
-
126
- describe("utility computation", () => {
127
- const now = Date.parse("2026-08-23T00:00:00.000Z");
128
- const at = (daysAgo: number) => new Date(now - daysAgo * 86_400_000).toISOString();
129
-
130
- it("answers 「这条 memory 参与过的任务成功率是多少」", () => {
131
- const summary = summarizeMemoryOutcomes(MEM_A, [
132
- { observationId: "o1", workflowId: "w", outcome: "success", recordedAt: at(1), join: "exact", summary: "" },
133
- { observationId: "o2", workflowId: "w", outcome: "success", recordedAt: at(2), join: "exact", summary: "" },
134
- { observationId: "o3", workflowId: "w", outcome: "failure", recordedAt: at(3), join: "exact", summary: "" },
135
- ], now);
136
- expect(summary.total).toBe(3);
137
- expect(summary.successRate).toBeCloseTo(2 / 3, 5);
138
- expect(summary.exactSuccessRate).toBeCloseTo(2 / 3, 5);
139
- });
140
-
141
- it("goes negative when the tasks mostly failed — importance could never hold this", () => {
142
- const summary = summarizeMemoryOutcomes(MEM_A, [
143
- { observationId: "o1", workflowId: "w", outcome: "failure", recordedAt: at(0), join: "exact", summary: "" },
144
- { observationId: "o2", workflowId: "w", outcome: "failure", recordedAt: at(1), join: "exact", summary: "" },
145
- { observationId: "o3", workflowId: "w", outcome: "missed", recordedAt: at(2), join: "exact", summary: "" },
146
- ], now);
147
- expect(summary.utility).not.toBeNull();
148
- expect(summary.utility!).toBeLessThan(0);
149
- });
150
-
151
- it("weights session joins below exact ones", () => {
152
- const exact = summarizeMemoryOutcomes(MEM_A, [
153
- { observationId: "o1", workflowId: "w", outcome: "success", recordedAt: at(0), join: "exact", summary: "" },
154
- ], now);
155
- const session = summarizeMemoryOutcomes(MEM_A, [
156
- { observationId: "o1", workflowId: "w", outcome: "success", recordedAt: at(0), join: "session", summary: "" },
157
- ], now);
158
- expect(exact.effectiveSamples).toBeCloseTo(JOIN_WEIGHT.exact, 5);
159
- expect(session.effectiveSamples).toBeCloseTo(JOIN_WEIGHT.session, 5);
160
- // 同一个 outcome,加权平均的值一样,可信度不同 —— 差别落在 effectiveSamples 上。
161
- expect(exact.utility).toBeCloseTo(OUTCOME_REWARD.success, 5);
162
- });
163
-
164
- it("decays old outcomes by the 30-day half-life", () => {
165
- const fresh = summarizeMemoryOutcomes(MEM_A, [
166
- { observationId: "o1", workflowId: "w", outcome: "success", recordedAt: at(0), join: "exact", summary: "" },
167
- ], now);
168
- const old = summarizeMemoryOutcomes(MEM_A, [
169
- { observationId: "o1", workflowId: "w", outcome: "success", recordedAt: at(30), join: "exact", summary: "" },
170
- ], now);
171
- expect(old.effectiveSamples).toBeCloseTo(fresh.effectiveSamples * 0.5, 3);
172
- });
173
-
174
- it("ignores session-join noise once exact samples exist", () => {
175
- // 2026-08-23 落地当天的实测:同一会话跑多个任务时,readerId join 会把无关任务的
176
- // 成败摊到每条被读过的记忆上——那次 5 条记忆的会话级成功率全是 44%(常数,零信息),
177
- // 精确成功率却是 100%/33%/0%。所以有精确样本时不掺会话级。
178
- const links = [
179
- { observationId: "e1", workflowId: "w", outcome: "success" as const, recordedAt: at(0), join: "exact" as const, summary: "" },
180
- { observationId: "e2", workflowId: "w", outcome: "success" as const, recordedAt: at(0), join: "exact" as const, summary: "" },
181
- { observationId: "s1", workflowId: "w", outcome: "failure" as const, recordedAt: at(0), join: "session" as const, summary: "" },
182
- { observationId: "s2", workflowId: "w", outcome: "failure" as const, recordedAt: at(0), join: "session" as const, summary: "" },
183
- ];
184
- const preferred = summarizeMemoryOutcomes(MEM_A, links, now);
185
- expect(preferred.utilityBasis).toBe("exact");
186
- expect(preferred.utility).toBeCloseTo(OUTCOME_REWARD.success, 5);
187
-
188
- const mixed = summarizeMemoryOutcomes(MEM_A, links, now, { preferExact: false });
189
- expect(mixed.utility!).toBeLessThan(preferred.utility!);
190
- });
191
-
192
- it("falls back to session join when nothing named this memory", () => {
193
- const links = Array.from({ length: 4 }, (_, i) => ({
194
- observationId: `s${i}`, workflowId: "w", outcome: "success" as const,
195
- recordedAt: at(0), join: "session" as const, summary: "",
196
- }));
197
- const summary = summarizeMemoryOutcomes(MEM_A, links, now);
198
- expect(summary.utilityBasis).toBe("session");
199
- expect(summary.utility).toBeCloseTo(OUTCOME_REWARD.success, 5);
200
- });
201
-
202
- it("returns null (not 0) when there is no evidence", () => {
203
- const summary = summarizeMemoryOutcomes(MEM_A, [], now);
204
- expect(summary.utility).toBeNull();
205
- expect(summary.successRate).toBeNull();
206
- expect(summary.utilityBasis).toBe("none");
207
- });
208
- });
209
-
210
- describe("utility metadata write-back", () => {
211
- it("writes utility without touching importance-derived behaviour", () => {
212
- const summary = summarizeMemoryOutcomes(MEM_A, [
213
- { observationId: "o1", workflowId: "w", outcome: "failure", recordedAt: new Date().toISOString(), join: "exact", summary: "" },
214
- ]);
215
- const before = { tier: "peripheral", accessCount: 2 };
216
- const after = applyUtilityToMeta(before, summary);
217
- expect(after[UTILITY_FIELD]).toBeLessThan(0);
218
- // 硬约束:utility 不进 tier、不进 decay 豁免。
219
- expect(resolveTier(JSON.stringify(after), 0.7)).toBe(resolveTier(JSON.stringify(before), 0.7));
220
- expect(isDecayExempt(JSON.stringify(after), 0.7, "cases"))
221
- .toBe(isDecayExempt(JSON.stringify(before), 0.7, "cases"));
222
- expect((after as Record<string, unknown>).importance).toBeUndefined();
223
- });
224
-
225
- it("clears utility instead of writing 0 when the evidence is gone", () => {
226
- const meta = { utility: -0.5, utilitySamples: 1, utilityUpdatedAt: 1 };
227
- const cleared = applyUtilityToMeta(meta, summarizeMemoryOutcomes(MEM_A, []));
228
- expect(UTILITY_FIELD in cleared).toBe(false);
229
- expect(readUtility(JSON.stringify(cleared))).toBeNull();
230
- });
231
- });
232
-
233
- describe("end-to-end through the observation store", () => {
234
- it("round-trips join keys and answers the success-rate query", async () => {
235
- const dir = mkdtempSync(join(tmpdir(), "recallnest-utility-obs-"));
236
- const store = new WorkflowObservationStore(dir);
237
- await store.save(obs({ recalledIds: [MEM_A], outcome: "success", readerId: "r-sess0001" }));
238
- await store.save(obs({ recalledIds: [MEM_A], outcome: "failure", readerId: "r-sess0001", workflowId: "checkpoint_session" }));
239
- await store.save(obs({ recalledIds: [MEM_B], outcome: "success", readerId: "r-sess0001" }));
240
-
241
- const records = await store.listRecent({ limit: 100 });
242
- const summary = computeMemoryUtility({ id: MEM_A, metadata: "{}" }, records);
243
- expect(summary.total).toBe(2);
244
- expect(summary.successRate).toBeCloseTo(0.5, 5);
245
- expect(formatMemoryUtility(summary, { showLinks: 2 })).toContain("成功率");
246
- });
247
-
248
- it("still parses observations written before the join-key fields existed", async () => {
249
- const dir = mkdtempSync(join(tmpdir(), "recallnest-utility-legacy-"));
250
- const store = new WorkflowObservationStore(dir);
251
- const legacy = obs({ outcome: "success" });
252
- expect((legacy as Record<string, unknown>).recalledIds).toBeUndefined();
253
- await store.save(legacy);
254
- expect((await store.listRecent({ limit: 10 })).length).toBe(1);
255
- });
256
- });
@@ -1,65 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
-
3
- import { planDedup, type DedupRow } from "../../scripts/migrate-dedup-ids.js";
4
-
5
- /**
6
- * P0-2 迁移脚本核心逻辑回归。LanceDB I/O 部分(main)由「在备份副本上实跑」做集成验证;
7
- * 这里只测纯 keeper 选择:同 id 保留最新 timestamp(importance 破平)、每 id 唯一、不丢文本。
8
- */
9
-
10
- function row(id: string, ts: number, importance: number, text: string): DedupRow {
11
- return { id, timestamp: ts, importance, text, vector: [0, 0, 0], scope: "s", category: "entities", metadata: "{}" };
12
- }
13
-
14
- describe("planDedup (dup-id migration keeper selection)", () => {
15
- it("keeps the latest-timestamp row per dup id and counts extras", () => {
16
- const rows = [
17
- row("dup1", 100, 0.5, "A"),
18
- row("dup1", 300, 0.6, "A"), // latest ts → keeper
19
- row("dup1", 200, 0.9, "A"),
20
- row("uniq", 150, 0.5, "B"),
21
- ];
22
- const plan = planDedup(rows);
23
- expect(plan.dupIds).toEqual(["dup1"]);
24
- expect(plan.extraRows).toBe(2);
25
- expect(plan.keepers.length).toBe(1);
26
- expect(Number(plan.keepers[0].timestamp)).toBe(300);
27
- });
28
-
29
- it("breaks timestamp ties by highest importance", () => {
30
- const rows = [
31
- row("d", 500, 0.4, "X"),
32
- row("d", 500, 0.95, "X"), // same ts, higher importance → keeper
33
- row("d", 500, 0.7, "X"),
34
- ];
35
- const plan = planDedup(rows);
36
- expect(plan.keepers.length).toBe(1);
37
- expect(plan.keepers[0].importance).toBe(0.95);
38
- });
39
-
40
- it("leaves unique ids untouched and preserves every distinct id's text", () => {
41
- const rows = [
42
- row("a", 1, 0.5, "textA"),
43
- row("a", 2, 0.5, "textA"), // dup
44
- row("b", 1, 0.5, "textB"), // unique
45
- row("c", 1, 0.5, "textC"),
46
- row("c", 5, 0.5, "textC"), // dup
47
- ];
48
- const plan = planDedup(rows);
49
- const dupSet = new Set(plan.dupIds);
50
- // Simulate the migration result: untouched unique-id rows + one keeper per dup id.
51
- const survivors = [...rows.filter((r) => !dupSet.has(r.id)), ...plan.keepers];
52
- const ids = survivors.map((r) => r.id).sort();
53
- expect(ids).toEqual(["a", "b", "c"]); // every distinct id survives
54
- expect(new Set(ids).size).toBe(ids.length); // each id appears exactly once
55
- expect(survivors.find((r) => r.id === "a")!.text).toBe("textA");
56
- expect(survivors.find((r) => r.id === "c")!.text).toBe("textC");
57
- });
58
-
59
- it("returns an empty plan when there are no duplicates", () => {
60
- const plan = planDedup([row("x", 1, 0.5, "1"), row("y", 2, 0.5, "2")]);
61
- expect(plan.dupIds).toEqual([]);
62
- expect(plan.extraRows).toBe(0);
63
- expect(plan.keepers).toEqual([]);
64
- });
65
- });