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,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
- });