recallnest 2.6.1 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/CHANGELOG.md +292 -0
  2. package/README.md +280 -195
  3. package/README_CN.md +266 -199
  4. package/config.json.example +8 -1
  5. package/docs/api-reference.md +1 -1
  6. package/integrations/README.md +1 -1
  7. package/integrations/agy/README.md +73 -0
  8. package/integrations/agy/setup.sh +75 -0
  9. package/integrations/setup-all.sh +1 -1
  10. package/package.json +7 -5
  11. package/src/access-tracker.ts +10 -1
  12. package/src/activity-counter.ts +69 -11
  13. package/src/age-label.ts +29 -0
  14. package/src/audit-log.ts +35 -0
  15. package/src/auto-gc.ts +97 -30
  16. package/src/capture-engine.ts +26 -4
  17. package/src/cli.ts +224 -10
  18. package/src/consolidation-engine.ts +317 -123
  19. package/src/context-composer-stable.ts +5 -1
  20. package/src/data-checkup.ts +17 -3
  21. package/src/dream-pipeline.ts +90 -12
  22. package/src/embedder.ts +44 -5
  23. package/src/env-config.ts +71 -0
  24. package/src/forget-engine.ts +8 -2
  25. package/src/gateway-server.ts +272 -0
  26. package/src/ingest.ts +248 -22
  27. package/src/kg-extractor.ts +17 -0
  28. package/src/llm-client.ts +86 -25
  29. package/src/mcp-server.ts +2 -1
  30. package/src/mcp-tools-advanced.ts +7 -2
  31. package/src/mcp-tools-core.ts +11 -1
  32. package/src/mcp-tools-governance.ts +45 -3
  33. package/src/memory-evolution.ts +16 -0
  34. package/src/memory-output.ts +47 -8
  35. package/src/memory-promotion.ts +280 -0
  36. package/src/memory-store-port.ts +32 -3
  37. package/src/memory-utility.ts +314 -0
  38. package/src/pivot-apply.ts +52 -1
  39. package/src/pivot-evidence.ts +128 -0
  40. package/src/ppr-traversal.ts +25 -1
  41. package/src/recall-ledger.ts +76 -0
  42. package/src/retriever.ts +82 -9
  43. package/src/runtime-config.ts +9 -0
  44. package/src/scope-policy.ts +22 -1
  45. package/src/session-engine.ts +11 -4
  46. package/src/session-output.ts +13 -4
  47. package/src/session-store.ts +2 -3
  48. package/src/skill-promotion.ts +218 -1
  49. package/src/store.ts +231 -71
  50. package/src/synthesis-contract.ts +286 -0
  51. package/src/version-manager.ts +83 -0
  52. package/src/workflow-observation-schema.ts +26 -0
  53. package/integrations/gemini-cli/README.md +0 -57
  54. package/integrations/gemini-cli/setup.sh +0 -64
  55. package/src/__tests__/access-tracker-novelty.test.ts +0 -304
  56. package/src/__tests__/activity-counter.test.ts +0 -125
  57. package/src/__tests__/adaptive-fidelity.test.ts +0 -102
  58. package/src/__tests__/admission-control.test.ts +0 -206
  59. package/src/__tests__/anchor-generator.test.ts +0 -67
  60. package/src/__tests__/api-server-security.test.ts +0 -102
  61. package/src/__tests__/auto-consolidation.test.ts +0 -168
  62. package/src/__tests__/auto-recall.test.ts +0 -131
  63. package/src/__tests__/babel-integration.test.ts +0 -144
  64. package/src/__tests__/batch-dedup.test.ts +0 -65
  65. package/src/__tests__/belief-history.test.ts +0 -220
  66. package/src/__tests__/canary-eval.test.ts +0 -114
  67. package/src/__tests__/capture-engine.test.ts +0 -1328
  68. package/src/__tests__/capture-heuristic.test.ts +0 -171
  69. package/src/__tests__/cascade-forget.test.ts +0 -210
  70. package/src/__tests__/category-min-scores.test.ts +0 -116
  71. package/src/__tests__/cc8-cc1-essential-context.test.ts +0 -298
  72. package/src/__tests__/circuit-breaker.test.ts +0 -99
  73. package/src/__tests__/cluster-consolidation.test.ts +0 -945
  74. package/src/__tests__/confidence-metadata.test.ts +0 -125
  75. package/src/__tests__/confidence-tracker.test.ts +0 -261
  76. package/src/__tests__/conflict-advisor.test.ts +0 -206
  77. package/src/__tests__/conflict-escalation.test.ts +0 -149
  78. package/src/__tests__/conflict-lifecycle.test.ts +0 -83
  79. package/src/__tests__/conflict-output.test.ts +0 -85
  80. package/src/__tests__/conflict-store.test.ts +0 -418
  81. package/src/__tests__/connector-v1.test.ts +0 -181
  82. package/src/__tests__/consolidation-engine.test.ts +0 -430
  83. package/src/__tests__/constructive-retrieval.test.ts +0 -509
  84. package/src/__tests__/context-collapse-renderer.test.ts +0 -214
  85. package/src/__tests__/context-composer-pins.test.ts +0 -64
  86. package/src/__tests__/context-composer-task-selection.test.ts +0 -24
  87. package/src/__tests__/context-reconstructor.test.ts +0 -65
  88. package/src/__tests__/context-renderer.test.ts +0 -91
  89. package/src/__tests__/continuity-eval.test.ts +0 -275
  90. package/src/__tests__/continuity-schema.test.ts +0 -97
  91. package/src/__tests__/contracts.test.ts +0 -262
  92. package/src/__tests__/conversation-importer.test.ts +0 -692
  93. package/src/__tests__/core-recall-eval.test.ts +0 -235
  94. package/src/__tests__/core-summary.test.ts +0 -109
  95. package/src/__tests__/data-checkup.test.ts +0 -276
  96. package/src/__tests__/decay-exemption.test.ts +0 -129
  97. package/src/__tests__/dedup-actions.test.ts +0 -58
  98. package/src/__tests__/dedup-guard.test.ts +0 -39
  99. package/src/__tests__/deterministic-id.test.ts +0 -39
  100. package/src/__tests__/distill-lock.test.ts +0 -305
  101. package/src/__tests__/distill-session-argshape.test.ts +0 -79
  102. package/src/__tests__/doctor.test.ts +0 -319
  103. package/src/__tests__/dream-pipeline.test.ts +0 -780
  104. package/src/__tests__/drill-down.test.ts +0 -108
  105. package/src/__tests__/embedder.test.ts +0 -122
  106. package/src/__tests__/emotion-decay.test.ts +0 -46
  107. package/src/__tests__/emotion-detector.test.ts +0 -56
  108. package/src/__tests__/emotion-phase1-integration.test.ts +0 -153
  109. package/src/__tests__/emotion-retrieval.test.ts +0 -56
  110. package/src/__tests__/entity-resolver.test.ts +0 -58
  111. package/src/__tests__/env-config.test.ts +0 -151
  112. package/src/__tests__/error-signature-boost.test.ts +0 -87
  113. package/src/__tests__/error-signature.test.ts +0 -95
  114. package/src/__tests__/error-taxonomy.test.ts +0 -129
  115. package/src/__tests__/ethics-layer.test.ts +0 -476
  116. package/src/__tests__/event-segmenter.test.ts +0 -153
  117. package/src/__tests__/feature-flags-integration.test.ts +0 -54
  118. package/src/__tests__/frequency-tracker.test.ts +0 -113
  119. package/src/__tests__/freshness.test.ts +0 -186
  120. package/src/__tests__/graph-export.test.ts +0 -489
  121. package/src/__tests__/hotness-blend.test.ts +0 -283
  122. package/src/__tests__/ingest-chunking.test.ts +0 -44
  123. package/src/__tests__/ingest.test.ts +0 -360
  124. package/src/__tests__/interference-detector.test.ts +0 -203
  125. package/src/__tests__/kg-extractor.test.ts +0 -192
  126. package/src/__tests__/kg-store.test.ts +0 -300
  127. package/src/__tests__/llm-client-pivot.test.ts +0 -628
  128. package/src/__tests__/llm-consolidation.test.ts +0 -226
  129. package/src/__tests__/maintenance-locks.test.ts +0 -189
  130. package/src/__tests__/mcp-efficiency.test.ts +0 -288
  131. package/src/__tests__/mcp-related-scopes.test.ts +0 -151
  132. package/src/__tests__/memory-boundaries.test.ts +0 -282
  133. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +0 -345
  134. package/src/__tests__/memory-evolution.test.ts +0 -271
  135. package/src/__tests__/memory-health-rebalance.test.ts +0 -144
  136. package/src/__tests__/memory-lint.test.ts +0 -421
  137. package/src/__tests__/memory-output.test.ts +0 -215
  138. package/src/__tests__/memory-promotion.test.ts +0 -315
  139. package/src/__tests__/migrate-dedup-ids.test.ts +0 -65
  140. package/src/__tests__/multi-vector.test.ts +0 -242
  141. package/src/__tests__/narrative-architecture.test.ts +0 -456
  142. package/src/__tests__/noise-filter.test.ts +0 -205
  143. package/src/__tests__/noise-prototype-bank.test.ts +0 -280
  144. package/src/__tests__/obsidian-connector.test.ts +0 -363
  145. package/src/__tests__/omitted-reporting.test.ts +0 -98
  146. package/src/__tests__/pending-review.test.ts +0 -77
  147. package/src/__tests__/pii-redact.test.ts +0 -333
  148. package/src/__tests__/pivot-apply.test.ts +0 -373
  149. package/src/__tests__/pivot-distill-supervisor.test.ts +0 -457
  150. package/src/__tests__/pivot-distill.test.ts +0 -1178
  151. package/src/__tests__/post-retrieval-filter.test.ts +0 -178
  152. package/src/__tests__/ppr-traversal.test.ts +0 -256
  153. package/src/__tests__/prediction-engine.test.ts +0 -349
  154. package/src/__tests__/predictive-prospective-memory.test.ts +0 -416
  155. package/src/__tests__/preference-guard-smoke.test.ts +0 -211
  156. package/src/__tests__/preference-matcher.test.ts +0 -282
  157. package/src/__tests__/preference-slots.test.ts +0 -205
  158. package/src/__tests__/prospective-memory.test.ts +0 -283
  159. package/src/__tests__/pull-from-macbook-sync-only.test.ts +0 -679
  160. package/src/__tests__/query-entity-detector.test.ts +0 -171
  161. package/src/__tests__/query-expander-alias.test.ts +0 -146
  162. package/src/__tests__/query-expander.test.ts +0 -34
  163. package/src/__tests__/recall-gate.test.ts +0 -266
  164. package/src/__tests__/recall-governor.test.ts +0 -227
  165. package/src/__tests__/recall-mode.test.ts +0 -64
  166. package/src/__tests__/resolve-db-path.test.ts +0 -130
  167. package/src/__tests__/resolve-tier.test.ts +0 -64
  168. package/src/__tests__/result-synthesizer.test.ts +0 -198
  169. package/src/__tests__/resume-context.test.ts +0 -4898
  170. package/src/__tests__/retrieval-eval.test.ts +0 -163
  171. package/src/__tests__/retrieval-trace.test.ts +0 -213
  172. package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
  173. package/src/__tests__/retriever-lazy-init-dispatch.test.ts +0 -123
  174. package/src/__tests__/retroactive-boost.test.ts +0 -223
  175. package/src/__tests__/salience-scoring.test.ts +0 -106
  176. package/src/__tests__/scope-inventory.test.ts +0 -200
  177. package/src/__tests__/scope-policy.test.ts +0 -69
  178. package/src/__tests__/scope-suggester.test.ts +0 -80
  179. package/src/__tests__/session-checkpoint.test.ts +0 -291
  180. package/src/__tests__/session-distiller.test.ts +0 -640
  181. package/src/__tests__/session-gc.test.ts +0 -215
  182. package/src/__tests__/skill-engine.test.ts +0 -552
  183. package/src/__tests__/skill-promotion.test.ts +0 -654
  184. package/src/__tests__/skill-verifier.test.ts +0 -122
  185. package/src/__tests__/source-heartbeat.test.ts +0 -250
  186. package/src/__tests__/stable-context-length-contract.test.ts +0 -62
  187. package/src/__tests__/store-patch-metadata.test.ts +0 -205
  188. package/src/__tests__/store-read-consistency.test.ts +0 -122
  189. package/src/__tests__/store-schema-migration.test.ts +0 -87
  190. package/src/__tests__/store-scope-audit.test.ts +0 -81
  191. package/src/__tests__/store-upsert.test.ts +0 -201
  192. package/src/__tests__/supersede-chain.test.ts +0 -154
  193. package/src/__tests__/temporal-parser.test.ts +0 -241
  194. package/src/__tests__/temporal-validity.test.ts +0 -160
  195. package/src/__tests__/term-registry-terminal-roster.test.ts +0 -127
  196. package/src/__tests__/term-registry.test.ts +0 -121
  197. package/src/__tests__/tool-error-surfacing.test.ts +0 -98
  198. package/src/__tests__/tool-output-compressor.test.ts +0 -160
  199. package/src/__tests__/topic-tag.test.ts +0 -113
  200. package/src/__tests__/trusted-memory.test.ts +0 -839
  201. package/src/__tests__/ui-server-security.test.ts +0 -93
  202. package/src/__tests__/usage-tracker.test.ts +0 -287
  203. package/src/__tests__/version-manager.test.ts +0 -213
  204. package/src/__tests__/workflow-observation-skill-outcome.test.ts +0 -352
  205. package/src/__tests__/workflow-observation.test.ts +0 -433
  206. package/src/__tests__/write-verifier.test.ts +0 -122
  207. /package/integrations/{gemini-cli/gemini-md-snippet.md → agy/agy-md-snippet.md} +0 -0
@@ -20,12 +20,16 @@
20
20
  import type { MemoryEntry, MemorySearchResult } from "./store.js";
21
21
  import { deterministicId } from "./store.js";
22
22
  import type { MemoryStorePort } from "./memory-store-port.js";
23
- import { createVersionGroup } from "./version-manager.js";
24
- import { isActiveMemory, parseEvolution, buildSupersedeMetadata, buildConsolidatedMetadata, patchEvolution } from "./memory-evolution.js";
23
+ import type { ScopeMatchMode } from "./scope-policy.js";
24
+ import { computeVersionGroupPatch, resolveGroupId } from "./version-manager.js";
25
+ import { isActiveMemory, parseEvolution, buildSupersedeMetadata, patchEvolutionOnMeta } from "./memory-evolution.js";
25
26
  import { cosineSimilarity } from "./multi-vector.js";
26
27
  import type { LLMClient } from "./llm-client.js";
27
28
  import type { Embedder } from "./embedder.js";
28
29
  import { logWarn } from "./stderr-log.js";
30
+ import { DURABLE_MEMORY_CATEGORIES, type DurableMemoryCategory } from "./memory-schema.js";
31
+ import { getConflictPolicyForCategory, type MemoryBoundaryMetadata } from "./memory-boundaries.js";
32
+ import type { SynthesisVerdict } from "./synthesis-contract.js";
29
33
 
30
34
  // ---------------------------------------------------------------------------
31
35
  // Config & Types
@@ -46,6 +50,12 @@ export interface ConsolidationConfig {
46
50
  tripleJaccardThreshold?: number;
47
51
  /** KG evidence: both sides need at least this many triples to qualify (default 2) */
48
52
  minTriplesForEvidence?: number;
53
+ /**
54
+ * scope 过滤模式(2026-08-16 加)。默认 "family" = store 历史语义(无冒号按前缀)。
55
+ * dream 传 "exact":它拿到的是一个具体 scope 名,前缀语义会把兄弟 scope
56
+ * (`memory` → `memory:pivot`)卷进同一批候选与 vectorSearch 结果。
57
+ */
58
+ scopeMatch?: ScopeMatchMode;
49
59
  }
50
60
 
51
61
  export const DEFAULT_CONSOLIDATION_CONFIG: ConsolidationConfig = {
@@ -122,6 +132,53 @@ function parseMetadata(entry: MemoryEntry): Record<string, unknown> {
122
132
  try { return JSON.parse(entry.metadata); } catch { return {}; }
123
133
  }
124
134
 
135
+ /**
136
+ * 合成契约版本号。写进每条派生物的 metadata,用来把「新契约的产物」和历史存量分开。
137
+ *
138
+ * 存在的理由是可评估性:两把确定性判据(结论连接词率 / 性向归因率)要在改动前后
139
+ * 做对照,而库里 4,500+ 条老产物和新产物混在一张表里、`cluster_insight` 标记完全
140
+ * 相同。没有这个标记,"改完之后的产物质量"这个问题就只能靠时间戳去猜。
141
+ *
142
+ * **提示词属于契约,改了就要推版本号**——否则前后两批产物混在同一个标记下,
143
+ * 「这个数字是哪版代码跑出来的」又变成了猜。v2(2026-08-23 当日)= v1 加上
144
+ * 「只输出一个 JSON 对象」的约束:v1 实测有 3.3% 的调用会一口气吐出多个 JSON
145
+ * 对象、整条判 unparsable(finish_reason 全为 stop,与 token 截断无关),
146
+ * 补这一句后同口径实测降到 0.0%。
147
+ */
148
+ export const SYNTHESIS_CONTRACT_VERSION = 2;
149
+
150
+ /**
151
+ * 派生物(cluster insight / cross-memory pattern)的边界声明。
152
+ *
153
+ * 为什么必须显式写(2026-08-22 查实):4,749 条历史派生物**全部没有 boundary 字段**。
154
+ * 检索侧 `retriever.ts` 的 `isEvidenceLayer` 在缺字段时回退到 `isTranscriptScope(scope)`,
155
+ * 于是其中 96.4% 因为恰好落在 `cc:` / `codex:` scope 而被判成 evidence —— **靠 scope
156
+ * 命名侥幸判对,不是靠设计**。剩下 173 条落在 `memory` / `project:*` 这类非 transcript
157
+ * scope 上,静默拿到了 durable 权威:一句 LLM 复述,在检索里和手写结论平起平坐。
158
+ *
159
+ * 定成 evidence 而不是 durable:这是模型对既有记忆的再加工,不是从外部世界捕获的
160
+ * 事实,也不是人拍板的结论。它可以作为线索被检索到,但不该压过它自己的源。
161
+ * authority 取 `distillation`(它确实是提炼产物),与 layer 一起才说得全 ——
162
+ * 「是什么来源」和「有多大权威」是两个问题。
163
+ */
164
+ export function buildDerivedBoundary(category: string): MemoryBoundaryMetadata {
165
+ const durable = toDurableCategory(category);
166
+ return {
167
+ layer: "evidence",
168
+ authority: "distillation",
169
+ conflictPolicy: getConflictPolicyForCategory(durable),
170
+ originalCategory: durable,
171
+ note: "Dream-synthesized derivative: a lead to its sources, never authority over them.",
172
+ };
173
+ }
174
+
175
+ /** 簇的多数票类目可能落在 legacy 值上;边界字段只认 durable 六类,兜底到 events。 */
176
+ function toDurableCategory(category: string): DurableMemoryCategory {
177
+ return (DURABLE_MEMORY_CATEGORIES as readonly string[]).includes(category)
178
+ ? category as DurableMemoryCategory
179
+ : "events";
180
+ }
181
+
125
182
  function isActive(entry: MemoryEntry): boolean {
126
183
  // Unified lifecycle check via evolution.status (no legacy meta.state fallback)
127
184
  return isActiveMemory(entry.metadata);
@@ -163,7 +220,7 @@ export function detectHeuristicContradiction(textA: string, textB: string): bool
163
220
  // Store interface (duck-typed to avoid hard dependency)
164
221
  // ---------------------------------------------------------------------------
165
222
 
166
- type ConsolidationStore = Pick<MemoryStorePort, "list" | "getById" | "vectorSearch" | "update">;
223
+ type ConsolidationStore = Pick<MemoryStorePort, "list" | "getById" | "vectorSearch" | "update" | "patchMetadataBatch">;
167
224
 
168
225
  // ---------------------------------------------------------------------------
169
226
  // Engine
@@ -184,11 +241,18 @@ export class ConsolidationEngine {
184
241
  maxEntriesPerRun,
185
242
  tripleJaccardThreshold = 0.5,
186
243
  minTriplesForEvidence = 2,
244
+ scopeMatch = "family",
187
245
  } = this.config;
188
246
 
189
247
  // 1. Fetch entries in scope
190
- const entries = await this.store.list([scope], undefined, maxEntriesPerRun, 0);
191
- const active = entries.filter(isActive);
248
+ const entries = await this.store.list([scope], undefined, maxEntriesPerRun, 0, scopeMatch);
249
+ // 派生 insight 不参与去重(2026-08-16 加,与 dream gather 的既有排除对齐)。
250
+ // 此前只有 gather(dream-pipeline.ts)排除派生物,3a 自己重新取数时不排除 ——
251
+ // 实测后果:`memory:pivot` 里 3 条手写原件被 3a 合并进 LLM 复述、复述当了 canonical
252
+ // (双方 importance 0.7 / accessCount 0,canonicalScore 打平时任取其一),
253
+ // 其中两条带 canonicalKey 与来源 tag,赢家是零来源的短复述。
254
+ // 派生物既不做 seed 也不做 member —— 摘要不该有资格顶掉它自己的原文。
255
+ const active = entries.filter(e => isActive(e) && !isDerivedInsight(e.metadata));
192
256
 
193
257
  if (active.length === 0) {
194
258
  return { originalCount: 0, clustersFound: 0, mergedCount: 0, relationsAdded: 0, tripleEvidenceMerges: 0, conflictsDetected: [], scope };
@@ -222,7 +286,7 @@ export class ConsolidationEngine {
222
286
  if (!full?.vector?.length) continue;
223
287
 
224
288
  const similar = await this.store.vectorSearch(
225
- full.vector, 10, clusterThreshold, [scope],
289
+ full.vector, 10, clusterThreshold, [scope], scopeMatch,
226
290
  );
227
291
 
228
292
  // Seeds are active-only (above), but vectorSearch filters by scope alone, so
@@ -236,7 +300,10 @@ export class ConsolidationEngine {
236
300
  s => s.entry.id !== entry.id
237
301
  && !clustered.has(s.entry.id)
238
302
  && s.entry.category === entry.category
239
- && isActive(s.entry),
303
+ && isActive(s.entry)
304
+ // vectorSearch 只按 scope 过滤,派生 insight 会从这里回到候选集里 ——
305
+ // seed 侧已在上面滤掉,member 侧必须同样滤(2026-08-16)。
306
+ && !isDerivedInsight(s.entry.metadata),
240
307
  );
241
308
 
242
309
  if (members.length === 0) continue;
@@ -284,11 +351,27 @@ export class ConsolidationEngine {
284
351
 
285
352
  // Get similarity scores relative to canonical
286
353
  const pairResults = await this.store.vectorSearch(
287
- canonical.vector, memberEntries.length + 5, clusterThreshold, [scope],
354
+ canonical.vector, memberEntries.length + 5, clusterThreshold, [scope], scopeMatch,
288
355
  );
289
356
  const scoreMap = new Map<string, number>();
290
357
  for (const r of pairResults) scoreMap.set(r.entry.id, r.score);
291
358
 
359
+ // 2026-08-14 批量化重构(dream 写爆库根治 + Bug-1 修复):
360
+ //
361
+ // 旧路径对每个 member 逐条写库 2-4 次(merge: createVersionGroup 2 + member 1
362
+ // + canonical 1;link: 2),一个 cluster 写 canonical N 遍 —— 且每次 patch 都
363
+ // 基于循环里从不刷新的旧内存串,后写的把先写的键抹掉:member1 经
364
+ // createVersionGroup 落库的 version_group 被随后的 sourceMemories 更新(旧串
365
+ // 起底)覆盖,member2 又因读到无组的旧串新造一个 groupId。最终态是 canonical
366
+ // 与全部 member 通常都不带组、sourceMemories 只剩最后一个 member(2 成员的
367
+ // cluster 同样全丢)。
368
+ //
369
+ // 新路径:先纯计算把 member 分进 merge/link 两组,cluster 末尾一次
370
+ // patchMetadataBatch —— patchFn 在锁内以库中最新 meta 为底座应用,canonical
371
+ // 只写一次(version-group 键 + sourceMemories 累积 + cluster_members 累积),
372
+ // groupId 在 cluster 级决策一次全组共享。每 cluster 1 个 commit(原 2-4×N)。
373
+ const mergeMembers: MemoryEntry[] = [];
374
+ const linkMembers: MemoryEntry[] = [];
292
375
  for (const member of sorted.slice(1)) {
293
376
  const sim = scoreMap.get(member.id) ?? clusterThreshold;
294
377
 
@@ -302,37 +385,78 @@ export class ConsolidationEngine {
302
385
  );
303
386
 
304
387
  if (sim >= mergeThreshold || jaccard >= tripleJaccardThreshold) {
305
- // Tier 3.3: Version coexistence — both stay but grouped.
306
- await createVersionGroup(this.store, canonical, member, scope);
307
- // C-1: Also mark the weaker entry as consolidated via evolution metadata
308
- const consolidatedMeta = buildConsolidatedMetadata(member.metadata, canonical.id);
309
- await this.store.update(member.id, { metadata: consolidatedMeta }, [scope]);
310
- // Mark canonical as having source memories
311
- const canonEvo = parseEvolution(canonical.metadata, canonical.timestamp);
312
- if (!canonEvo.sourceMemories.includes(member.id)) {
313
- const updatedCanon = patchEvolution(canonical.metadata, {
314
- sourceMemories: [...canonEvo.sourceMemories, member.id],
315
- });
316
- await this.store.update(canonical.id, { metadata: updatedCanon }, [scope]);
317
- }
388
+ mergeMembers.push(member);
318
389
  mergedCount++;
319
390
  if (sim < mergeThreshold) tripleEvidenceMerges++;
320
391
  } else {
321
- // Link: add clustered_with relation
322
- const memberMeta = parseMetadata(member);
323
- memberMeta.clustered_with = canonical.id;
324
- await this.store.update(member.id, { metadata: JSON.stringify(memberMeta) }, [scope]);
325
-
326
- const canonMeta = parseMetadata(canonical);
327
- if (!Array.isArray(canonMeta.cluster_members)) canonMeta.cluster_members = [];
328
- if (!(canonMeta.cluster_members as string[]).includes(member.id)) {
329
- (canonMeta.cluster_members as string[]).push(member.id);
330
- }
331
- await this.store.update(canonical.id, { metadata: JSON.stringify(canonMeta) }, [scope]);
392
+ linkMembers.push(member);
332
393
  relationsAdded++;
333
394
  }
334
395
  }
335
396
 
397
+ if (mergeMembers.length > 0 || linkMembers.length > 0) {
398
+ // groupId 决策:canonical 已有组沿用,否则 cluster 级新生成一次。
399
+ // 决策基于本 cluster 开头刚 getById 的 canonical(毫秒级新鲜);组字段只有
400
+ // dream 自己写、且本 scope 有 per-scope dream 锁,锁外决策的竞态面趋零。
401
+ const groupId = mergeMembers.length > 0 ? resolveGroupId(canonical) : null;
402
+ const nowIso = new Date().toISOString();
403
+ const mergeIds = mergeMembers.map(m => m.id);
404
+ const linkIds = linkMembers.map(m => m.id);
405
+
406
+ const patches: Array<{
407
+ id: string;
408
+ patchFn: (meta: Record<string, unknown>, entry: MemoryEntry) => Record<string, unknown>;
409
+ }> = [];
410
+
411
+ patches.push({
412
+ id: canonical.id,
413
+ patchFn: (meta, entry) => {
414
+ if (groupId) computeVersionGroupPatch(meta, entry, groupId, true, nowIso);
415
+ if (mergeIds.length > 0) {
416
+ const evo = meta.evolution !== null && typeof meta.evolution === "object" && !Array.isArray(meta.evolution)
417
+ ? (meta.evolution as Record<string, unknown>)
418
+ : {};
419
+ const existing = Array.isArray(evo.sourceMemories) ? (evo.sourceMemories as string[]) : [];
420
+ const accumulated = [...existing];
421
+ for (const id of mergeIds) if (!accumulated.includes(id)) accumulated.push(id);
422
+ patchEvolutionOnMeta(meta, { sourceMemories: accumulated });
423
+ }
424
+ if (linkIds.length > 0) {
425
+ const members = Array.isArray(meta.cluster_members) ? (meta.cluster_members as string[]) : [];
426
+ for (const id of linkIds) if (!members.includes(id)) members.push(id);
427
+ meta.cluster_members = members;
428
+ }
429
+ return meta;
430
+ },
431
+ });
432
+
433
+ for (const member of mergeMembers) {
434
+ patches.push({
435
+ id: member.id,
436
+ patchFn: (meta, entry) => {
437
+ // Tier 3.3 version coexistence + C-1 consolidated 标记,一次写齐
438
+ computeVersionGroupPatch(meta, entry, groupId as string, false, nowIso);
439
+ return patchEvolutionOnMeta(meta, {
440
+ status: "consolidated",
441
+ consolidatedInto: canonical.id,
442
+ });
443
+ },
444
+ });
445
+ }
446
+
447
+ for (const member of linkMembers) {
448
+ patches.push({
449
+ id: member.id,
450
+ patchFn: (meta) => {
451
+ meta.clustered_with = canonical.id;
452
+ return meta;
453
+ },
454
+ });
455
+ }
456
+
457
+ await this.store.patchMetadataBatch(patches, [scope]);
458
+ }
459
+
336
460
  // Conflict detection within cluster
337
461
  for (let i = 0; i < memberEntries.length; i++) {
338
462
  for (let j = i + 1; j < memberEntries.length; j++) {
@@ -373,6 +497,21 @@ export interface ClusterConsolidationResult {
373
497
  * 「LLM 全程不可用」和「今天没料可炼」会长得一模一样。
374
498
  */
375
499
  llmFailures: number;
500
+ /**
501
+ * 模型自己判定「这簇没有可提炼结论」的次数(insight + pattern 合计)。
502
+ *
503
+ * 2026-08-23 加。在此之前**这个数按构造只能是 0** —— 老 `generateL0` 没有弃权路径,
504
+ * 每个合格簇必写一条。所以它不是锦上添花的计量,而是新契约是否真在生效的直接读数:
505
+ * 长期为 0 意味着模型又在硬凑了。诊断实测同批簇上一个合格提示词弃权约 22.5%。
506
+ */
507
+ synthesisAbstained: number;
508
+ /**
509
+ * 写库前校验拦下的次数(提示词回声 / evidence 不合法 / 长度越界 / JSON 解析不出)。
510
+ *
511
+ * 与 `llmFailures` 分开:那个是「没拿到回复」,这个是「拿到了但不合契约」。
512
+ * 老路径两者都不计 —— 于是 28 条系统提示词原文安静地进了库。
513
+ */
514
+ synthesisRejected: number;
376
515
  }
377
516
 
378
517
  /**
@@ -389,7 +528,7 @@ export async function clusterAndConsolidate(params: {
389
528
  entries: MemoryEntry[];
390
529
  embedder: Pick<Embedder, "embedPassage">;
391
530
  llm: LLMClient;
392
- store: Pick<MemoryStorePort, "store" | "update">;
531
+ store: Pick<MemoryStorePort, "store" | "update" | "patchMetadataBatch">;
393
532
  scope: string;
394
533
  /** Minimum cluster size to trigger consolidation (default: 3) */
395
534
  minClusterSize?: number;
@@ -418,6 +557,8 @@ export async function clusterAndConsolidate(params: {
418
557
  patternsExtracted: 0,
419
558
  entriesLinked: 0,
420
559
  llmFailures: 0,
560
+ synthesisAbstained: 0,
561
+ synthesisRejected: 0,
421
562
  };
422
563
 
423
564
  // Filter to active entries with vectors
@@ -481,34 +622,11 @@ export async function clusterAndConsolidate(params: {
481
622
  break;
482
623
  }
483
624
 
484
- // Generate insight via LLM from cluster member texts
485
- //
486
- // 单个 cluster 的 LLM 失败不该炸掉整个 scope。llm-client 的 chat() 在 catch 里是
487
- // `throw err`(记完熔断计数后原样抛),所以 15s AbortController 超时会一路冒到
488
- // runDream 之外 —— 2026-08-09 memory 轮次连挂两次正是这条路径:memory 是最大
489
- // scope、cluster 最多,撞上一次超时的概率接近 1,且两次挂的位置不同(不是某对
490
- // 内容有毒,是这一阶段本来就没有防护)。隔壁 llm-consolidation.ts:68 的同类调用
491
- // 一直有 try/catch,本处缺的是同一层保护。
492
- //
493
- // 吸收成 null 而不是新开一条分支:generateL0 返回 null(熔断打开 / 空响应)本来
494
- // 就是合法路径,下面 `if (!insight)` 已经按「零产出轮次」处理。失败另计 llmFailures,
495
- // 避免吸收变静默。
496
- const combinedText = cluster.map(m => m.text).join("\n---\n");
497
- let insight: string | null;
498
- try {
499
- insight = await llm.generateL0(combinedText);
500
- } catch (err) {
501
- result.llmFailures++;
502
- logWarn(`[WARN] cluster insight LLM 调用失败,本簇跳过: ${err instanceof Error ? err.message : String(err)}`);
503
- insight = null;
504
- }
505
-
506
- if (!insight) {
507
- // CC-9: No insight produced — this is a zero-yield round
508
- consecutiveLowYield++;
509
- result.clustersConsolidated++;
510
- continue;
511
- }
625
+ // 每簇共用的三个量,2026-08-23 从 insight 分支里提上来。
626
+ // 提上来是 pattern 解耦的前置条件:以前 bestCat / maxImportance / sortedSourceIds
627
+ // 都算在 `if (!insight) continue` 之后,pattern 想独立跑就拿不到它们。
628
+ const memberTexts = cluster.map(m => m.text);
629
+ const sortedSourceIds = cluster.map(m => m.id).slice().sort();
512
630
 
513
631
  // Determine category (majority vote from cluster members)
514
632
  const catCounts = new Map<string, number>();
@@ -527,63 +645,122 @@ export async function clusterAndConsolidate(params: {
527
645
  // Importance = max of cluster members
528
646
  const maxImportance = Math.max(...cluster.map(m => m.importance));
529
647
 
530
- // Embed the insight text
531
- const insightVector = await params.embedder.embedPassage(insight);
532
-
533
- // Store the insight as a new memory. P0-2/P1-2: derive a deterministic id from the
534
- // (sorted) source member ids so re-running dream/consolidate on the same cluster
535
- // upserts the same insight row instead of appending a near-duplicate each run
536
- // (the highest-frequency dup-id source). Cross-run stable because sourceIds are stable.
537
- const sortedSourceIds = cluster.map(m => m.id).slice().sort();
538
- const insightEntry = await store.store({
539
- id: deterministicId(scope, `cluster-insight:${sortedSourceIds.join(",")}`),
540
- text: insight,
541
- vector: insightVector,
542
- category: bestCat,
543
- scope,
544
- importance: maxImportance,
545
- metadata: JSON.stringify({
546
- evolution: {
547
- status: "active",
548
- version: 1,
549
- accessCount: 0,
550
- lastAccessedAt: null,
551
- supersededBy: null,
552
- consolidatedInto: null,
553
- sourceMemories: cluster.map(m => m.id),
554
- validFrom: Date.now(),
555
- validUntil: null,
556
- },
557
- cluster_insight: true,
558
- }),
559
- });
560
-
561
- result.insightsGenerated++;
562
-
563
- // Mark source memories as consolidated_into (but keep them active)
564
- for (const member of cluster) {
565
- const patched = patchEvolution(member.metadata, {
566
- consolidatedInto: insightEntry.id,
567
- // Keep status active — still individually searchable
648
+ /**
649
+ * 一次合成调用的统一入口:吸收异常、把三种结局(产出 / 弃权 / 校验不过)分别计数。
650
+ *
651
+ * 单个 cluster 的 LLM 失败不该炸掉整个 scope。llm-client 的 chat() 在 catch 里是
652
+ * `throw err`(记完熔断计数后原样抛),所以 AbortController 超时会一路冒到
653
+ * runDream 之外 —— 2026-08-09 memory 轮次连挂两次正是这条路径:memory 是最大
654
+ * scope、cluster 最多,撞上一次超时的概率接近 1,且两次挂的位置不同(不是某对
655
+ * 内容有毒,是这一阶段本来就没有防护)。
656
+ *
657
+ * 吸收成 `abstained` 而不是新开一条分支:调用方对「本簇这一侧没有产出」的处理
658
+ * 本来就一样。失败另计 llmFailures、校验不过另计 synthesisRejected,避免吸收变静默。
659
+ */
660
+ const runSynthesis = async (
661
+ label: string,
662
+ call: () => Promise<SynthesisVerdict>,
663
+ ): Promise<SynthesisVerdict> => {
664
+ let verdict: SynthesisVerdict;
665
+ try {
666
+ verdict = await call();
667
+ } catch (err) {
668
+ result.llmFailures++;
669
+ logWarn(`[WARN] ${label} LLM 调用失败,本簇跳过: ${err instanceof Error ? err.message : String(err)}`);
670
+ return { status: "abstained" };
671
+ }
672
+ if (verdict.status === "abstained") {
673
+ result.synthesisAbstained++;
674
+ } else if (verdict.status === "rejected") {
675
+ result.synthesisRejected++;
676
+ // 打出来的是**原因**不是「失败了」:`prompt-echo` 说明模型在回声提示词,
677
+ // `evidence-out-of-range` 说明它在编造编号,两者要采取的动作完全不同。
678
+ logWarn(`[WARN] ${label} 输出未通过写库前校验(${verdict.reason}),本簇该侧不写库`);
679
+ }
680
+ return verdict;
681
+ };
682
+
683
+ /** 派生物统一的边界声明。见下方 buildDerivedBoundary 的注释。 */
684
+ const derivedBoundary = buildDerivedBoundary(bestCat);
685
+
686
+ let effectsThisCluster = 0;
687
+
688
+ // ---- cluster insight ---------------------------------------------------
689
+ const insightVerdict = await runSynthesis("cluster insight", () =>
690
+ llm.synthesizeClusterInsight(memberTexts));
691
+
692
+ if (insightVerdict.status === "ok") {
693
+ const { text: insight, evidence } = insightVerdict.output;
694
+
695
+ // Embed the insight text
696
+ const insightVector = await params.embedder.embedPassage(insight);
697
+
698
+ // Store the insight as a new memory. P0-2/P1-2: derive a deterministic id from the
699
+ // (sorted) source member ids so re-running dream/consolidate on the same cluster
700
+ // upserts the same insight row instead of appending a near-duplicate each run
701
+ // (the highest-frequency dup-id source). Cross-run stable because sourceIds are stable.
702
+ const insightEntry = await store.store({
703
+ id: deterministicId(scope, `cluster-insight:${sortedSourceIds.join(",")}`),
704
+ text: insight,
705
+ vector: insightVector,
706
+ category: bestCat,
707
+ scope,
708
+ importance: maxImportance,
709
+ metadata: JSON.stringify({
710
+ evolution: {
711
+ status: "active",
712
+ version: 1,
713
+ accessCount: 0,
714
+ lastAccessedAt: null,
715
+ supersededBy: null,
716
+ consolidatedInto: null,
717
+ sourceMemories: cluster.map(m => m.id),
718
+ validFrom: Date.now(),
719
+ validUntil: null,
720
+ },
721
+ cluster_insight: true,
722
+ boundary: derivedBoundary,
723
+ synthesis_contract: SYNTHESIS_CONTRACT_VERSION,
724
+ /** 模型自己指认的支撑源(sourceMemories 的子集)。空集进不来——校验挡在前面。 */
725
+ evidenceMemories: evidence.map(i => cluster[i - 1]?.id).filter((id): id is string => Boolean(id)),
726
+ }),
568
727
  });
569
- await store.update(member.id, { metadata: patched }, [scope]);
570
- result.entriesLinked++;
728
+
729
+ result.insightsGenerated++;
730
+ effectsThisCluster++;
731
+
732
+ // Mark source memories as consolidated_into (but keep them active).
733
+ // 2026-08-14 批量化:N 次逐条 update → 1 次 patchMetadataBatch。patchFn 在锁内
734
+ // 以最新 meta 为底座,pattern 段随后的 contributedToPattern 批不会再把这里写的
735
+ // consolidatedInto 抹掉(旧路径的 Bug-2:两段都从同一个旧内存串起底 patch)。
736
+ // 本批独立于 pattern 批提交:pattern 路径失败时 insight 的源链接已落库(保持
737
+ // 旧异常语义 —— insight 源链接从来不等 pattern)。
738
+ result.entriesLinked += await store.patchMetadataBatch(
739
+ cluster.map(member => ({
740
+ id: member.id,
741
+ patchFn: (meta: Record<string, unknown>) => patchEvolutionOnMeta(meta, {
742
+ consolidatedInto: insightEntry.id,
743
+ // Keep status active — still individually searchable
744
+ }),
745
+ })),
746
+ [scope],
747
+ );
571
748
  }
572
749
 
573
750
  result.clustersConsolidated++;
574
751
 
575
- // HP-5: Cross-memory pattern extraction
752
+ // ---- HP-5: Cross-memory pattern extraction ------------------------------
753
+ //
754
+ // 2026-08-23 解耦:这一段不再挂在 insight 成功之后。原路径是
755
+ // `if (!insight) { ...; continue; }`,于是 insight 一旦弃权或失败,pattern 抽取
756
+ // 根本不执行 —— 两个目标概念不同的合成被绑成一条命。改后两者独立失败、独立弃权:
757
+ // 「这簇没有单条结论」和「这簇没有跨条目模式」是两个判断,没有理由互相代表。
576
758
  if (extractPatterns && cluster.length >= 3) {
577
- // 同上:pattern 抽取失败也只丢这一簇的 pattern,不连坐已经写好的 insight。
578
- let patternText: string | null;
579
- try {
580
- patternText = await llm.extractPattern(cluster.map(m => m.text));
581
- } catch (err) {
582
- result.llmFailures++;
583
- logWarn(`[WARN] pattern 抽取 LLM 调用失败,本簇跳过: ${err instanceof Error ? err.message : String(err)}`);
584
- patternText = null;
585
- }
586
- if (patternText) {
759
+ const patternVerdict = await runSynthesis("cross-memory pattern", () =>
760
+ llm.synthesizeClusterPattern(memberTexts));
761
+
762
+ if (patternVerdict.status === "ok") {
763
+ const { text: patternText, evidence } = patternVerdict.output;
587
764
  // 与同函数上方的 cluster insight 取同一个值:派生物继承源里最高的 importance,
588
765
  // 不额外加成。原本这里是 maxImportance + 0.1,会让 LLM 抽出的 pattern 比它的
589
766
  // 任何一条源记忆都重要——而 importance >= 0.95 在本仓库里是「人工 pin」的专用
@@ -615,23 +792,38 @@ export async function clusterAndConsolidate(params: {
615
792
  },
616
793
  cross_memory_pattern: true,
617
794
  source_cluster_size: cluster.length,
795
+ // pattern 恒为 patterns 类目,边界单独构造,不跟 insight 的多数票类目走。
796
+ boundary: buildDerivedBoundary("patterns"),
797
+ synthesis_contract: SYNTHESIS_CONTRACT_VERSION,
798
+ evidenceMemories: evidence.map(i => cluster[i - 1]?.id).filter((id): id is string => Boolean(id)),
618
799
  }),
619
800
  });
620
801
 
621
- // Mark source memories with contributedToPattern
622
- for (const member of cluster) {
623
- const patched = patchEvolution(member.metadata, {
624
- contributedToPattern: patternEntry.id,
625
- });
626
- await store.update(member.id, { metadata: patched }, [scope]);
627
- }
802
+ // Mark source memories with contributedToPattern(同上:一批提交,锁内最新
803
+ // meta 起底 —— 与 insight 批叠加而非互抹)
804
+ await store.patchMetadataBatch(
805
+ cluster.map(member => ({
806
+ id: member.id,
807
+ patchFn: (meta: Record<string, unknown>) => patchEvolutionOnMeta(meta, {
808
+ contributedToPattern: patternEntry.id,
809
+ }),
810
+ })),
811
+ [scope],
812
+ );
628
813
 
629
814
  result.patternsExtracted++;
815
+ effectsThisCluster++;
630
816
  }
631
817
  }
632
818
 
633
- // CC-9: Successful insight generated — reset diminishing returns counter
634
- consecutiveLowYield = 0;
819
+ // CC-9: 本簇两侧都没写出东西才算零产出轮次。
820
+ // 判据从「insight 有没有」换成「这一簇有没有任何副作用」—— 解耦之后 insight 弃权
821
+ // 而 pattern 产出是合法结局,把它算成零产出会让早停提前触发、白扔掉后面的簇。
822
+ if (effectsThisCluster === 0) {
823
+ consecutiveLowYield++;
824
+ } else {
825
+ consecutiveLowYield = 0;
826
+ }
635
827
  }
636
828
 
637
829
  return result;
@@ -666,8 +858,10 @@ export function formatConsolidationResult(result: ConsolidationResult): string {
666
858
  * captured from the outside world.
667
859
  *
668
860
  * Single source of truth for the two metadata flags: retrieval uses it to
669
- * collapse a derivative with its sources, and the dream gather uses it to keep
670
- * derivatives from being re-consolidated as if they were raw material.
861
+ * collapse a derivative with its sources, the dream gather uses it to keep
862
+ * derivatives from being re-consolidated as if they were raw material, and
863
+ * (2026-08-16) 3a uses it on both the seed side and the member side — a summary
864
+ * must never be eligible to become the canonical row over its own source.
671
865
  */
672
866
  export function isDerivedInsight(metadata: string | undefined): boolean {
673
867
  if (!metadata) return false;
@@ -1,6 +1,7 @@
1
1
  import type { PinAsset } from "./memory-assets.js";
2
2
  import type { RetrievalResult } from "./retriever.js";
3
3
  import type { SessionCheckpointRecord } from "./session-schema.js";
4
+ import { isFallbackSummary } from "./session-engine.js";
4
5
  import { selectPinnedContext } from "./context-composer-pins.js";
5
6
  import { cleanText, dedupeText } from "./context-composer-text.js";
6
7
  import { selectStableResults } from "./context-composer-stable-selection.js";
@@ -74,7 +75,10 @@ function buildCheckpointStableContext(
74
75
  if (!checkpoint) return [];
75
76
 
76
77
  const items = [
77
- checkpoint.summary ? `Checkpoint summary: ${checkpoint.summary}` : "",
78
+ // 兜底文案不占 slot:它零信息,占着会把真正有内容的 decision / next 挤出 limit。
79
+ checkpoint.summary && !isFallbackSummary(checkpoint.summary)
80
+ ? `Checkpoint summary: ${checkpoint.summary}`
81
+ : "",
78
82
  checkpoint.task ? `Checkpoint focus: ${checkpoint.task}` : "",
79
83
  checkpoint.decisions[0] ? `Checkpoint decision: ${checkpoint.decisions[0]}` : "",
80
84
  checkpoint.nextActions[0] ? `Checkpoint next: ${checkpoint.nextActions[0]}` : "",
@@ -130,7 +130,7 @@ function checkConflictBacklog(openConflictCount: number): CheckResult {
130
130
  }
131
131
 
132
132
  /** Check 5: Version group integrity — members should reference the same group ID. */
133
- function checkVersionGroups(entries: MemoryEntry[]): CheckResult {
133
+ function checkVersionGroups(entries: Array<Pick<MemoryEntry, "id" | "metadata">>): CheckResult {
134
134
  const groups = new Map<string, { ids: string[]; ranks: number[] }>();
135
135
 
136
136
  for (const e of entries) {
@@ -292,7 +292,7 @@ function checkUsageDistribution(entries: MemoryEntry[]): CheckResult {
292
292
  }
293
293
 
294
294
  export interface CheckupDeps {
295
- store: Pick<MemoryStore, "list" | "stats" | "getVectors">;
295
+ store: Pick<MemoryStore, "list" | "listPage" | "stats" | "getVectors">;
296
296
  openConflictCount: number;
297
297
  /** Optional override path for source-heartbeat.json (used in tests). */
298
298
  heartbeatPath?: string;
@@ -310,13 +310,27 @@ export async function runDataCheckup(deps: CheckupDeps): Promise<CheckupReport>
310
310
  // 截断披露:库总量 > 已扫,说明只体检了最近 SCAN_LIMIT 条。
311
311
  const totalAvailable = (await deps.store.stats()).totalCount;
312
312
 
313
+ // Version-group membership must be checked against the whole corpus. Looking only
314
+ // at the newest SCAN_LIMIT rows turns a healthy pair into a fake singleton whenever
315
+ // its other member falls outside the sample. Keep this second pass lightweight by
316
+ // retaining only id + metadata and never loading vectors.
317
+ const VERSION_GROUP_PAGE_SIZE = 5000;
318
+ const versionEntries: Array<Pick<MemoryEntry, "id" | "metadata">> = [];
319
+ for (let offset = 0; ; offset += VERSION_GROUP_PAGE_SIZE) {
320
+ const page = await deps.store.listPage({ limit: VERSION_GROUP_PAGE_SIZE, offset });
321
+ for (const entry of page) {
322
+ versionEntries.push({ id: entry.id, metadata: entry.metadata });
323
+ }
324
+ if (page.length < VERSION_GROUP_PAGE_SIZE) break;
325
+ }
326
+
313
327
  return {
314
328
  checks: [
315
329
  checkVectorDimensions(entries),
316
330
  checkOrphanMemories(entries),
317
331
  checkTierDistribution(entries),
318
332
  checkConflictBacklog(deps.openConflictCount),
319
- checkVersionGroups(entries),
333
+ checkVersionGroups(versionEntries),
320
334
  checkSourceHealth(deps.heartbeatPath),
321
335
  checkInterferenceDensity(entries),
322
336
  checkUsageDistribution(entries),