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
@@ -15,6 +15,7 @@
15
15
  */
16
16
 
17
17
  import type { MemoryStorePort } from "./memory-store-port.js";
18
+ import type { ScopeMatchMode } from "./scope-policy.js";
18
19
  import type { LLMClient } from "./llm-client.js";
19
20
  import type { Embedder } from "./embedder.js";
20
21
  import { ConsolidationEngine, clusterAndConsolidate, DEFAULT_CONSOLIDATION_CONFIG, isDerivedInsight, type ConsolidationKGSource } from "./consolidation-engine.js";
@@ -45,12 +46,14 @@ export interface DreamConfig {
45
46
  extractPatterns: boolean;
46
47
  /** Max entries to scan per consolidation run (default: 500) */
47
48
  maxEntriesPerRun: number;
48
- /** Wall-clock budget for one `dream --auto` sweep, ms (default: 6h).
49
+ /** Wall-clock budget for one `dream --auto` sweep, ms (default: 2h).
49
50
  * 防跨天兜底闸,不是调度器。2026-07-24 那轮跑了 4 天 15 小时(见 cli.ts --auto 分支
50
51
  * 与 scripts/dream-consolidation.sh:57 的注释),期间堵死三天调度。
51
- * 已知天花板:超预算后剩余 scope 直接丢弃,而每轮都从列表头重来 → 靠后的 scope 会被
52
- * 系统性饿死。真解是 session scope TTL(只 dream 近 N 天活跃的),不是把预算调大——
53
- * scope 数 07-10 是 133、07-29 是 475,只增不减,加预算追不上。 */
52
+ * 2026-08-14: 6h→2h。transcript scope 已整体出队(原文层不参与巩固,cli.ts --auto
53
+ * 的 pruneWriteCounts(isTranscriptScope)),inline 队列只剩个位数 durable scope,
54
+ * 典型轮次分钟级;2h 是纯兜底。单 scope 路径(weekly memory / MCP dream tool)不走
55
+ * 本预算(budget/deadline 只存在于 cli.ts --auto 分支)。env 覆盖:
56
+ * RECALLNEST_DREAM_BUDGET_MS。 */
54
57
  autoRunBudgetMs: number;
55
58
  /** Scopes the `--auto` sweep hands off to a dedicated schedule instead of running inline.
56
59
  * Exact match only — a prefix rule would silently swallow future `memory-*` scopes.
@@ -65,6 +68,23 @@ export interface DreamConfig {
65
68
  * 这是缓解不是根治:scope 只增不减仍在(07-10 是 133、07-29 是 475、07-30 是 507),
66
69
  * 真解仍是 session scope TTL —— 只 dream 近 N 天活跃的。 */
67
70
  autoExcludeScopes: readonly string[];
71
+ /**
72
+ * 按策略**永不**巩固的 scope(2026-08-16 加)。与 autoExcludeScopes 是两件事,别混:
73
+ * 那个是"交给专用 schedule 跑"(deferred,别处会跑),这个是"根本不该被 dream 碰"
74
+ * (never,没有任何 schedule 会跑)。
75
+ *
76
+ * 判定在 runDream 内部而不是 `--auto` 的分拣里 —— CLI、weekly、MCP `dream` 工具
77
+ * 三条入口都必须受同一条策略约束,放在分拣里等于只拦住了其中一条。
78
+ *
79
+ * 首个成员 `memory:pivot`:手写提炼层(判断转折 / 决策 / 偏好 / 踩坑,每条带
80
+ * canonicalKey + pinned + 来源指针)。2026-08-16 实测它被 dream 碰出两类违例:
81
+ * 3b 往池里铸了 23 条无 canonicalKey / 无 pinned / 无来源的 LLM 派生行;3a 把 3 条
82
+ * 手写原件合并进 LLM 复述、复述当了 canonical。**提炼层再合成一层就是"摘要的摘要"**
83
+ * ——同 gather 不回喂派生 insight 的道理(见 :245 注释)。
84
+ * 要翻回"允许 3a 去重",前置条件:派生行永不当 canonical(已修)+ pinned 不失活
85
+ * (未做,GC/3a 都不认 tag)+ 有 uptake 证据证明去重确有收益。
86
+ */
87
+ neverDreamScopes: readonly string[];
68
88
  /** GC config for prune phase */
69
89
  gc: AutoGcConfig;
70
90
  }
@@ -76,8 +96,9 @@ export const DEFAULT_DREAM_CONFIG: DreamConfig = {
76
96
  minClusterSize: 3,
77
97
  extractPatterns: true,
78
98
  maxEntriesPerRun: 500,
79
- autoRunBudgetMs: 6 * 60 * 60 * 1000,
99
+ autoRunBudgetMs: 2 * 60 * 60 * 1000,
80
100
  autoExcludeScopes: ["memory"],
101
+ neverDreamScopes: ["memory:pivot"],
81
102
  gc: DEFAULT_AUTO_GC_CONFIG,
82
103
  };
83
104
 
@@ -146,6 +167,13 @@ export interface DreamResult {
146
167
  semanticClustersFound: number;
147
168
  insightsGenerated: number;
148
169
  patternsExtracted: number;
170
+ /** 3b 合成里模型判定「这簇没有可提炼结论」的次数(insight + pattern 合计)。
171
+ * 2026-08-23 起才可能非 0——在此之前合成没有弃权路径,每个合格簇必写一条。
172
+ * 它要和 insightsGenerated 一起读:0 insight + 高弃权 = 今天确实没料,
173
+ * 0 insight + 0 弃权 = 合成这一步压根没跑起来,两者完全不同。 */
174
+ synthesisAbstained: number;
175
+ /** 3b 合成产物在写库前被校验拦下的次数(提示词回声 / evidence 不合法 / 长度越界)。 */
176
+ synthesisRejected: number;
149
177
  mergedCount: number;
150
178
  archivedCount: number;
151
179
  /** 3a 去重路径新建的关联数。此前引擎返回了但被 dream 丢弃,导致「只加了关联」
@@ -177,8 +205,14 @@ async function runDreamInner(params: {
177
205
  activityStatsPath?: string;
178
206
  /** Optional KG evidence source for triple-overlap merging (null/absent = vector-only) */
179
207
  kgStore?: ConsolidationKGSource | null;
208
+ /**
209
+ * scope 匹配模式(2026-08-16 加)。**调度入口一律传 "exact"** —— 它们手里的是
210
+ * activity-stats 里的真实 scope 名或运维指定的具体 scope,不是 family selector。
211
+ * 缺省保持 "family" 只为兼容既有手动调用(`dream --scope cc` 想扫 `cc:*`)。
212
+ */
213
+ scopeMatch?: ScopeMatchMode;
180
214
  }): Promise<DreamResult> {
181
- const { store, llm, embedder, scope, force = false } = params;
215
+ const { store, llm, embedder, scope, force = false, scopeMatch = "family" } = params;
182
216
  const config = { ...DEFAULT_DREAM_CONFIG, ...params.config };
183
217
  const statsCfg = params.activityStatsPath ? { statsPath: params.activityStatsPath } : undefined;
184
218
  const phases: DreamPhaseResult[] = [];
@@ -192,6 +226,8 @@ async function runDreamInner(params: {
192
226
  semanticClustersFound: 0,
193
227
  insightsGenerated: 0,
194
228
  patternsExtracted: 0,
229
+ synthesisAbstained: 0,
230
+ synthesisRejected: 0,
195
231
  mergedCount: 0,
196
232
  archivedCount: 0,
197
233
  relationsAdded: 0,
@@ -199,13 +235,31 @@ async function runDreamInner(params: {
199
235
  semanticVectorFed: 0,
200
236
  };
201
237
 
238
+ // =========================================================================
239
+ // Phase 0: Policy gate — scopes that must never be consolidated
240
+ // =========================================================================
241
+ // 在写计数门槛**之前**判:策略排除与"今天写得够不够"无关,force 也不该越过它。
242
+ // 放在 runDream 内部 = CLI / weekly / MCP 三条入口同受约束(互审 C2)。
243
+ if (config.neverDreamScopes.includes(scope)) {
244
+ return {
245
+ ran: false,
246
+ reason: `policy_excluded (${scope} is in neverDreamScopes)`,
247
+ output: { kind: "skipped", reason: "policy_excluded", effectsWritten: 0 },
248
+ phases: [{
249
+ phase: "orient",
250
+ detail: `scope ${scope} 按策略不参与巩固(DreamConfig.neverDreamScopes)`,
251
+ }],
252
+ stats,
253
+ };
254
+ }
255
+
202
256
  // =========================================================================
203
257
  // Phase 1: Orient — assess current memory state
204
258
  // =========================================================================
205
259
  const writeCount = getWriteCount(scope, statsCfg);
206
260
  stats.writesSinceLastDream = writeCount;
207
261
 
208
- const storeStats = await store.stats([scope]);
262
+ const storeStats = await store.stats([scope], scopeMatch);
209
263
  stats.totalMemories = storeStats.totalCount ?? 0;
210
264
 
211
265
  if (!force && writeCount < config.minWritesForDream) {
@@ -229,7 +283,7 @@ async function runDreamInner(params: {
229
283
  // =========================================================================
230
284
  // Phase 2: Gather — collect active entries for consolidation
231
285
  // =========================================================================
232
- const entries = await store.list([scope], undefined, config.maxEntriesPerRun, 0);
286
+ const entries = await store.list([scope], undefined, config.maxEntriesPerRun, 0, scopeMatch);
233
287
  const active = entries.filter(e => isActiveMemory(e.metadata));
234
288
  stats.activeMemories = active.length;
235
289
 
@@ -282,7 +336,7 @@ async function runDreamInner(params: {
282
336
  if (consolidatable.length < config.minClusterSize) {
283
337
  phases.push({ phase: "consolidate", detail: "skipped — too few active entries" });
284
338
  phases.push({ phase: "prune", detail: "skipped — too few entries for GC" });
285
- resetWriteCount(scope, statsCfg);
339
+ await resetWriteCount(scope, statsCfg);
286
340
  return {
287
341
  ran: true,
288
342
  reason: "completed_early",
@@ -307,6 +361,9 @@ async function runDreamInner(params: {
307
361
  ...DEFAULT_CONSOLIDATION_CONFIG,
308
362
  clusterThreshold: config.clusterThreshold,
309
363
  maxEntriesPerRun: config.maxEntriesPerRun,
364
+ // 3a 自己重新取数(list + 两处 vectorSearch),gather 收窄了它不会跟着窄 ——
365
+ // 模式必须显式传进来,漏传就只修了一半(2026-08-16 互审 K3)。
366
+ scopeMatch,
310
367
  }, params.kgStore ?? null);
311
368
  const consolidation = await engine.run(scope);
312
369
  stats.dedupeClustersFound += consolidation.clustersFound;
@@ -339,6 +396,8 @@ async function runDreamInner(params: {
339
396
  stats.clustersFound += clusterResult.clustersFound;
340
397
  stats.insightsGenerated = clusterResult.insightsGenerated;
341
398
  stats.patternsExtracted = clusterResult.patternsExtracted;
399
+ stats.synthesisAbstained = clusterResult.synthesisAbstained;
400
+ stats.synthesisRejected = clusterResult.synthesisRejected;
342
401
  }
343
402
  }, { onBusy: "skip", expireMs: 600_000 });
344
403
 
@@ -351,6 +410,9 @@ async function runDreamInner(params: {
351
410
  ? `${stats.dedupeClustersFound} dedupe-clusters (${stats.mergedCount} merged), `
352
411
  + `${stats.semanticClustersFound} semantic-clusters -> `
353
412
  + `${stats.insightsGenerated} insights, ${stats.patternsExtracted} patterns`
413
+ // 弃权与校验拦截跟着一起报:没有它们,「合成认真看过但确实没料」和
414
+ // 「合成根本没跑」在这一行里长得一模一样——这正是本文件反复吃过的亏。
415
+ + `(弃权 ${stats.synthesisAbstained},校验拦下 ${stats.synthesisRejected})`
354
416
  : `skipped — another process holds the consolidate lock for scope ${scope}`,
355
417
  });
356
418
 
@@ -363,7 +425,9 @@ async function runDreamInner(params: {
363
425
  phases.push({
364
426
  phase: "prune",
365
427
  detail: gcResult.triggered
366
- ? `${gcResult.archivedCount} entries archived`
428
+ ? `${gcResult.archivedCount} entries archived, ${gcResult.dissolvedVersionGroups} singleton version groups dissolved`
429
+ : gcResult.dissolvedVersionGroups > 0
430
+ ? `${gcResult.dissolvedVersionGroups} singleton version groups dissolved; archive skipped — ${gcResult.reason}`
367
431
  : `skipped — ${gcResult.reason}`,
368
432
  });
369
433
 
@@ -384,7 +448,7 @@ async function runDreamInner(params: {
384
448
  // 注意:清对之后,listScopesAboveThreshold 的 FIFO 自组织才真正开始工作 ——
385
449
  // 处理完的 scope 被 delete、下次攒够写入重新插入时排到队尾,被跳过的带原计数留在
386
450
  // 队首下轮优先。所以这里不需要额外的游标 / checkpoint 文件。
387
- if (consolidateOutcome.ran) resetWriteCount(scope, statsCfg);
451
+ if (consolidateOutcome.ran) await resetWriteCount(scope, statsCfg);
388
452
 
389
453
  const health = assertDreamSweepHealth({
390
454
  llmPresent: llm != null,
@@ -419,6 +483,8 @@ export async function runDream(params: {
419
483
  activityStatsPath?: string;
420
484
  /** Optional KG evidence source for triple-overlap merging (null/absent = vector-only) */
421
485
  kgStore?: ConsolidationKGSource | null;
486
+ /** 见 runDreamInner 的同名参数:调度入口一律 "exact",缺省 "family" 只为兼容手动调用。 */
487
+ scopeMatch?: ScopeMatchMode;
422
488
  }): Promise<DreamResult> {
423
489
  const outcome = await withLock(
424
490
  `dream-${params.scope}`,
@@ -440,6 +506,8 @@ export async function runDream(params: {
440
506
  semanticClustersFound: 0,
441
507
  insightsGenerated: 0,
442
508
  patternsExtracted: 0,
509
+ synthesisAbstained: 0,
510
+ synthesisRejected: 0,
443
511
  mergedCount: 0,
444
512
  archivedCount: 0,
445
513
  relationsAdded: 0,
@@ -577,12 +645,21 @@ export function classifyDreamFailure(message: string): DreamFailureKind {
577
645
  /** transient 失败占比超过这个比例仍判整轮失败——否则"475 个全被锁挡住"会假绿。 */
578
646
  export const DREAM_TRANSIENT_BLOCK_RATIO = 0.2;
579
647
 
648
+ /** 比例阈值之外的绝对量豁免:transient 失败不超过这个数就不算整轮失败。
649
+ * 2026-08-14 transcript 出队后 inline 队列缩到个位数,1/1 = 100% > 20% 让单个
650
+ * 瞬态锁竞争把整轮判红(当天验证轮实测:一个死进程的残留 store-write 锁 →
651
+ * 唯一 durable scope 等锁超时 → blocked → launchd 失败通知)。比例阈值的原意是
652
+ * 「环境性失败要成规模」,1-2 个绝对不算规模——失败的 scope 计数未清、明天自动
653
+ * 重试,兜底仍在。fatal 不享受豁免(一票否决不变)。 */
654
+ export const DREAM_TRANSIENT_ABSOLUTE_TOLERANCE = 2;
655
+
580
656
  /**
581
657
  * 一轮 `dream --auto` 是否该判 blocked。
582
658
  *
583
659
  * 旧语义是「任意一个 scope 抛异常即 blocked」,在 475 个 scope 的规模下等于
584
660
  * 「有一个撞上锁就整轮红」,而红了会让 shell 脚本走重试 → 重跑全量。
585
- * 新语义:代码/数据缺陷一票否决;环境性失败要成规模才算整轮失败。
661
+ * 新语义:代码/数据缺陷一票否决;环境性失败要成规模才算整轮失败——
662
+ * 「规模」同时看比例(> 20%)与绝对量(> 2 个),小队列下单个瞬态不再整轮红。
586
663
  */
587
664
  export function shouldBlockDreamRun(params: {
588
665
  totalScopes: number;
@@ -591,6 +668,7 @@ export function shouldBlockDreamRun(params: {
591
668
  }): boolean {
592
669
  if (params.fatalFailures > 0) return true;
593
670
  if (params.totalScopes <= 0) return false;
671
+ if (params.transientFailures <= DREAM_TRANSIENT_ABSOLUTE_TOLERANCE) return false;
594
672
  return params.transientFailures / params.totalScopes > DREAM_TRANSIENT_BLOCK_RATIO;
595
673
  }
596
674
 
package/src/embedder.ts CHANGED
@@ -103,6 +103,24 @@ export interface EmbeddingConfig {
103
103
  normalized?: boolean;
104
104
  /** Enable automatic chunking for documents exceeding context limits (default: true) */
105
105
  chunking?: boolean;
106
+
107
+ /**
108
+ * Per-request timeout (ms) handed to the OpenAI-compatible client.
109
+ *
110
+ * **Unset is the default and keeps the historical behavior**: no `timeout` key is
111
+ * passed to the SDK at all, so the client falls back to its own default (600s in
112
+ * openai v7). That default is deliberately left alone — bulk embedding of long
113
+ * documents is legitimately slow, and lowering it globally would turn working
114
+ * ingests into failures.
115
+ *
116
+ * Worth setting on latency-sensitive callers: the SDK default stacks with the SDK's
117
+ * own 2 retries *and* Embedder's 2 transient retries, so a black-holed endpoint can
118
+ * hold a single embed call for far longer than any caller expects.
119
+ *
120
+ * Non-finite or non-positive values are ignored (treated as unset) rather than
121
+ * throwing, matching how `env-config.ts` validates its other numeric knobs.
122
+ */
123
+ timeoutMs?: number;
106
124
  }
107
125
 
108
126
  // Known embedding model dimensions
@@ -205,9 +223,15 @@ export class Embedder {
205
223
  // Enable auto-chunking by default for better handling of long documents
206
224
  this._autoChunk = config.chunking !== false;
207
225
 
226
+ const timeoutMs =
227
+ typeof config.timeoutMs === "number" && Number.isFinite(config.timeoutMs) && config.timeoutMs > 0
228
+ ? config.timeoutMs
229
+ : undefined;
230
+
208
231
  this.client = new OpenAI({
209
232
  apiKey: resolvedApiKey,
210
233
  ...(config.baseURL ? { baseURL: config.baseURL } : {}),
234
+ ...(timeoutMs !== undefined ? { timeout: timeoutMs } : {}),
211
235
  });
212
236
 
213
237
  this.dimensions = getVectorDimensions(config.model, config.dimensions);
@@ -328,7 +352,15 @@ export class Embedder {
328
352
  }
329
353
  }
330
354
 
331
- private async embedSingle(text: string, task?: string): Promise<number[]> {
355
+ /**
356
+ * @param allowChunkRetry - False for texts that are already chunks. Without it the
357
+ * context-error branch below can re-enter itself forever: `smartChunk` returns the
358
+ * input unchanged when it already fits, so re-embedding that chunk reproduces the
359
+ * same provider error and chunks again. The regex that gates the branch also matches
360
+ * rate-limit wording ("quota exceeded", "Rate limit exceeded"), which turned a 429
361
+ * into an unbounded request storm against a paid endpoint.
362
+ */
363
+ private async embedSingle(text: string, task?: string, allowChunkRetry = true): Promise<number[]> {
332
364
  if (!text || text.trim().length === 0) {
333
365
  throw new Error("Cannot embed empty text");
334
366
  }
@@ -352,21 +384,28 @@ export class Embedder {
352
384
  const errorMsg = error instanceof Error ? error.message : String(error);
353
385
  const isContextError = /context|too long|exceed|length/i.test(errorMsg);
354
386
 
355
- if (isContextError && this._autoChunk) {
387
+ if (isContextError && this._autoChunk && allowChunkRetry) {
356
388
  try {
357
389
  logInfo(`Document exceeded context limit (${errorMsg}), attempting chunking...`);
358
390
  const chunkResult = smartChunk(text, this._model);
359
-
391
+
360
392
  if (chunkResult.chunks.length === 0) {
361
393
  throw new Error(`Failed to chunk document: ${errorMsg}`);
362
394
  }
363
395
 
396
+ // Terminating condition: the chunker hands short text straight back, so a
397
+ // single chunk identical to the input means splitting cannot help. Retrying it
398
+ // would reproduce the same error and recurse.
399
+ if (chunkResult.chunks.length === 1 && chunkResult.chunks[0] === text) {
400
+ throw new Error(`Chunking cannot reduce this input: ${errorMsg}`);
401
+ }
402
+
364
403
  // Embed all chunks in parallel
365
404
  logInfo(`Split document into ${chunkResult.chunkCount} chunks for embedding`);
366
405
  const chunkEmbeddings = await Promise.all(
367
406
  chunkResult.chunks.map(async (chunk, idx) => {
368
407
  try {
369
- const embedding = await this.embedSingle(chunk, task);
408
+ const embedding = await this.embedSingle(chunk, task, false);
370
409
  return { embedding };
371
410
  } catch (chunkError) {
372
411
  logWarn(`Failed to embed chunk ${idx}:`, chunkError);
@@ -468,7 +507,7 @@ export class Embedder {
468
507
 
469
508
  // Embed all chunks in parallel, then average.
470
509
  const embeddings = await Promise.all(
471
- chunkResult.chunks.map((chunk) => this.embedSingle(chunk, task))
510
+ chunkResult.chunks.map((chunk) => this.embedSingle(chunk, task, false))
472
511
  );
473
512
 
474
513
  const avgEmbedding = embeddings.reduce(
package/src/env-config.ts CHANGED
@@ -109,6 +109,61 @@ export const readConsistencyInterval = (): number | undefined => {
109
109
  return Number.isFinite(n) && n >= 0 ? n : 0;
110
110
  };
111
111
 
112
+ /**
113
+ * Wall-clock budget (ms) for one `dream --auto` sweep.
114
+ *
115
+ * NOT a pure movement (unlike every accessor above): the original inline read in
116
+ * cli.ts was `Number(process.env.X ?? DEFAULT)`, which had two silent failure
117
+ * modes — `""` slipped past `??` and became **0** (every scope skipped, run is a
118
+ * no-op that still reports ok), and any non-numeric value became **NaN**, making
119
+ * the `Date.now() > deadline` guard permanently false. The second one silently
120
+ * reverts the very incident this budget exists to prevent: the 2026-07-24 sweep
121
+ * ran 4d15h and blocked three days of scheduled runs. Both are corrected here
122
+ * on purpose; do not "restore" the original parsing.
123
+ *
124
+ * Unset / empty / non-finite / <= 0 → caller's fallback.
125
+ */
126
+ export const dreamBudgetMs = (fallbackMs: number): number => {
127
+ const raw = process.env.RECALLNEST_DREAM_BUDGET_MS;
128
+ if (raw === undefined || raw.trim() === "") return fallbackMs;
129
+ const n = Number(raw);
130
+ return Number.isFinite(n) && n > 0 ? n : fallbackMs;
131
+ };
132
+
133
+ /**
134
+ * dream 合成(cluster insight / cross-memory pattern)专用模型,覆盖 config.llm.model。
135
+ *
136
+ * 独立于全局模型的理由(2026-08-23):合成是全库唯一一处「产物质量直接决定库的
137
+ * 可用性」的 LLM 调用 —— 它写进去的东西会被当成记忆检索出来,而 ingest 侧的
138
+ * smartExtract 只是给原文打标签,错了还能回原文。三臂实验里模型贡献了约 1/3 的
139
+ * 改善(同提示词下 32.3% → 45.2%),把这一档单独抬上去,不必让全库调用一起涨价。
140
+ *
141
+ * 未设 / 空串 → 用 config.llm.model(即全局默认),保持老行为。
142
+ */
143
+ export const synthesisModel = (): string | undefined => {
144
+ const raw = process.env.RECALLNEST_SYNTHESIS_MODEL;
145
+ const trimmed = raw?.trim();
146
+ return trimmed ? trimmed : undefined;
147
+ };
148
+
149
+ /**
150
+ * Per-request timeout (ms) for the embedding client, overriding `embedding.timeoutMs`
151
+ * in config.json.
152
+ *
153
+ * Exists so the timeout can be set per deployment (the resident MCP server, a one-off
154
+ * CLI ingest) without editing the shared config file — the same reason
155
+ * `RECALLNEST_LAYER_ADMISSION` lives here.
156
+ *
157
+ * Unset / empty / non-finite / <= 0 → undefined, i.e. fall through to config.json and
158
+ * ultimately to the SDK default. **Not setting it anywhere keeps the old behavior.**
159
+ */
160
+ export const embeddingTimeoutMs = (): number | undefined => {
161
+ const raw = process.env.RECALLNEST_EMBEDDING_TIMEOUT_MS;
162
+ if (raw === undefined || raw.trim() === "") return undefined;
163
+ const n = Number(raw);
164
+ return Number.isFinite(n) && n > 0 ? n : undefined;
165
+ };
166
+
112
167
  // --- Raw env values (caller validates / clamps / falls back to config) ---
113
168
 
114
169
  export const recallModeRaw = (): string | undefined => process.env.RECALLNEST_RECALL_MODE;
@@ -116,3 +171,19 @@ export const recallModeRaw = (): string | undefined => process.env.RECALLNEST_RE
116
171
  export const uiPortRaw = (): string | undefined => process.env.RECALLNEST_UI_PORT;
117
172
 
118
173
  export const apiPortRaw = (): string | undefined => process.env.RECALLNEST_API_PORT;
174
+
175
+ // --- Gateway (read-only front door; caller validates / clamps) ---
176
+
177
+ export const gatewayPortRaw = (): string | undefined => process.env.RECALLNEST_GATEWAY_PORT;
178
+
179
+ export const gatewayHostRaw = (): string | undefined => process.env.RECALLNEST_GATEWAY_HOST;
180
+
181
+ export const gatewayTokenRaw = (): string | undefined => process.env.RECALLNEST_GATEWAY_TOKEN;
182
+
183
+ export const gatewayRateMaxRaw = (): string | undefined => process.env.RECALLNEST_GATEWAY_RATE_MAX;
184
+
185
+ export const gatewayFileRootsRaw = (): string | undefined => process.env.RECALLNEST_GATEWAY_FILE_ROOTS;
186
+
187
+ export const gatewayRgRaw = (): string | undefined => process.env.RECALLNEST_GATEWAY_RG;
188
+
189
+ export const apiUrlRaw = (): string | undefined => process.env.RECALLNEST_API_URL;
@@ -223,8 +223,14 @@ export async function forgetByScope(
223
223
  let failedCount = 0;
224
224
  let totalCascadeDemoted = 0;
225
225
 
226
- // Fetch all entries in scope
227
- const entries = await store.list([scope], undefined, 5000);
226
+ // Fetch all entries in scope.
227
+ //
228
+ // scopeMatch: "exact" —— **破坏性操作绝不做前缀展开**(2026-08-16 互审 Codex R3 提出)。
229
+ // 默认 family 语义下 `forgetByScope("memory")` 会连 `memory:pivot` 一起遗忘,而下面
230
+ // 的 KG 清理走的是精确 `scope = 'memory'`(kg-store.ts:506)—— 记忆行按家族删、KG 只
231
+ // 清一个 scope,两边还会不一致。当前无生产 caller,正因如此更要在有 caller 之前定死语义:
232
+ // 要清一整个家族,就显式枚举那些 scope 逐个调用。
233
+ const entries = await store.list([scope], undefined, 5000, 0, "exact");
228
234
 
229
235
  for (const entry of entries) {
230
236
  const result = await forgetMemory(deps, {