recallnest 2.3.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. package/CHANGELOG.md +187 -0
  2. package/README.md +49 -38
  3. package/README_CN.md +46 -31
  4. package/assets/ui/app.js +1031 -0
  5. package/assets/ui/index.html +224 -0
  6. package/assets/ui/styles.css +781 -0
  7. package/config.json.example +4 -1
  8. package/docs/api-reference.md +1291 -0
  9. package/docs/architecture.md +168 -0
  10. package/docs/connector-spec.md +126 -0
  11. package/eval/cases-canary.json +58 -0
  12. package/eval/cases-scope-robustness.json +278 -0
  13. package/eval/cases.json +117 -20
  14. package/package.json +25 -4
  15. package/src/__tests__/access-tracker-novelty.test.ts +304 -0
  16. package/src/__tests__/activity-counter.test.ts +125 -0
  17. package/src/__tests__/adaptive-fidelity.test.ts +102 -0
  18. package/src/__tests__/admission-control.test.ts +206 -0
  19. package/src/__tests__/anchor-generator.test.ts +67 -0
  20. package/src/__tests__/api-server-security.test.ts +102 -0
  21. package/src/__tests__/auto-consolidation.test.ts +168 -0
  22. package/src/__tests__/auto-recall.test.ts +131 -0
  23. package/src/__tests__/babel-integration.test.ts +144 -0
  24. package/src/__tests__/batch-dedup.test.ts +65 -0
  25. package/src/__tests__/belief-history.test.ts +220 -0
  26. package/src/__tests__/canary-eval.test.ts +114 -0
  27. package/src/__tests__/capture-engine.test.ts +1328 -0
  28. package/src/__tests__/capture-heuristic.test.ts +171 -0
  29. package/src/__tests__/cascade-forget.test.ts +210 -0
  30. package/src/__tests__/category-min-scores.test.ts +116 -0
  31. package/src/__tests__/cc8-cc1-essential-context.test.ts +298 -0
  32. package/src/__tests__/circuit-breaker.test.ts +99 -0
  33. package/src/__tests__/cluster-consolidation.test.ts +945 -0
  34. package/src/__tests__/confidence-metadata.test.ts +125 -0
  35. package/src/__tests__/confidence-tracker.test.ts +261 -0
  36. package/src/__tests__/conflict-advisor.test.ts +206 -0
  37. package/src/__tests__/conflict-escalation.test.ts +149 -0
  38. package/src/__tests__/conflict-lifecycle.test.ts +83 -0
  39. package/src/__tests__/conflict-output.test.ts +85 -0
  40. package/src/__tests__/conflict-store.test.ts +418 -0
  41. package/src/__tests__/connector-v1.test.ts +181 -0
  42. package/src/__tests__/consolidation-engine.test.ts +430 -0
  43. package/src/__tests__/constructive-retrieval.test.ts +509 -0
  44. package/src/__tests__/context-collapse-renderer.test.ts +214 -0
  45. package/src/__tests__/context-composer-pins.test.ts +64 -0
  46. package/src/__tests__/context-composer-task-selection.test.ts +24 -0
  47. package/src/__tests__/context-reconstructor.test.ts +65 -0
  48. package/src/__tests__/context-renderer.test.ts +91 -0
  49. package/src/__tests__/continuity-eval.test.ts +275 -0
  50. package/src/__tests__/continuity-schema.test.ts +97 -0
  51. package/src/__tests__/contracts.test.ts +262 -0
  52. package/src/__tests__/conversation-importer.test.ts +692 -0
  53. package/src/__tests__/core-recall-eval.test.ts +235 -0
  54. package/src/__tests__/core-summary.test.ts +109 -0
  55. package/src/__tests__/data-checkup.test.ts +276 -0
  56. package/src/__tests__/decay-exemption.test.ts +129 -0
  57. package/src/__tests__/dedup-actions.test.ts +58 -0
  58. package/src/__tests__/dedup-guard.test.ts +39 -0
  59. package/src/__tests__/deterministic-id.test.ts +39 -0
  60. package/src/__tests__/distill-lock.test.ts +305 -0
  61. package/src/__tests__/distill-session-argshape.test.ts +79 -0
  62. package/src/__tests__/doctor.test.ts +319 -0
  63. package/src/__tests__/dream-pipeline.test.ts +780 -0
  64. package/src/__tests__/drill-down.test.ts +108 -0
  65. package/src/__tests__/embedder.test.ts +122 -0
  66. package/src/__tests__/emotion-decay.test.ts +46 -0
  67. package/src/__tests__/emotion-detector.test.ts +56 -0
  68. package/src/__tests__/emotion-phase1-integration.test.ts +153 -0
  69. package/src/__tests__/emotion-retrieval.test.ts +56 -0
  70. package/src/__tests__/entity-resolver.test.ts +58 -0
  71. package/src/__tests__/env-config.test.ts +151 -0
  72. package/src/__tests__/error-signature-boost.test.ts +87 -0
  73. package/src/__tests__/error-signature.test.ts +95 -0
  74. package/src/__tests__/error-taxonomy.test.ts +129 -0
  75. package/src/__tests__/ethics-layer.test.ts +476 -0
  76. package/src/__tests__/event-segmenter.test.ts +153 -0
  77. package/src/__tests__/feature-flags-integration.test.ts +54 -0
  78. package/src/__tests__/frequency-tracker.test.ts +113 -0
  79. package/src/__tests__/freshness.test.ts +186 -0
  80. package/src/__tests__/graph-export.test.ts +489 -0
  81. package/src/__tests__/hotness-blend.test.ts +283 -0
  82. package/src/__tests__/ingest-chunking.test.ts +44 -0
  83. package/src/__tests__/ingest.test.ts +360 -0
  84. package/src/__tests__/interference-detector.test.ts +203 -0
  85. package/src/__tests__/kg-extractor.test.ts +192 -0
  86. package/src/__tests__/kg-store.test.ts +300 -0
  87. package/src/__tests__/llm-client-pivot.test.ts +628 -0
  88. package/src/__tests__/llm-consolidation.test.ts +226 -0
  89. package/src/__tests__/maintenance-locks.test.ts +189 -0
  90. package/src/__tests__/mcp-efficiency.test.ts +288 -0
  91. package/src/__tests__/mcp-related-scopes.test.ts +151 -0
  92. package/src/__tests__/memory-boundaries.test.ts +282 -0
  93. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +345 -0
  94. package/src/__tests__/memory-evolution.test.ts +271 -0
  95. package/src/__tests__/memory-health-rebalance.test.ts +144 -0
  96. package/src/__tests__/memory-lint.test.ts +421 -0
  97. package/src/__tests__/memory-output.test.ts +215 -0
  98. package/src/__tests__/memory-promotion.test.ts +315 -0
  99. package/src/__tests__/migrate-dedup-ids.test.ts +65 -0
  100. package/src/__tests__/multi-vector.test.ts +242 -0
  101. package/src/__tests__/narrative-architecture.test.ts +456 -0
  102. package/src/__tests__/noise-filter.test.ts +205 -0
  103. package/src/__tests__/noise-prototype-bank.test.ts +280 -0
  104. package/src/__tests__/obsidian-connector.test.ts +363 -0
  105. package/src/__tests__/omitted-reporting.test.ts +98 -0
  106. package/src/__tests__/pending-review.test.ts +77 -0
  107. package/src/__tests__/pii-redact.test.ts +333 -0
  108. package/src/__tests__/pivot-apply.test.ts +373 -0
  109. package/src/__tests__/pivot-distill-supervisor.test.ts +457 -0
  110. package/src/__tests__/pivot-distill.test.ts +1178 -0
  111. package/src/__tests__/post-retrieval-filter.test.ts +178 -0
  112. package/src/__tests__/ppr-traversal.test.ts +256 -0
  113. package/src/__tests__/prediction-engine.test.ts +349 -0
  114. package/src/__tests__/predictive-prospective-memory.test.ts +416 -0
  115. package/src/__tests__/preference-guard-smoke.test.ts +211 -0
  116. package/src/__tests__/preference-matcher.test.ts +282 -0
  117. package/src/__tests__/preference-slots.test.ts +205 -0
  118. package/src/__tests__/prospective-memory.test.ts +283 -0
  119. package/src/__tests__/pull-from-macbook-sync-only.test.ts +679 -0
  120. package/src/__tests__/query-entity-detector.test.ts +171 -0
  121. package/src/__tests__/query-expander-alias.test.ts +146 -0
  122. package/src/__tests__/query-expander.test.ts +34 -0
  123. package/src/__tests__/recall-gate.test.ts +266 -0
  124. package/src/__tests__/recall-governor.test.ts +227 -0
  125. package/src/__tests__/recall-mode.test.ts +64 -0
  126. package/src/__tests__/resolve-db-path.test.ts +130 -0
  127. package/src/__tests__/resolve-tier.test.ts +64 -0
  128. package/src/__tests__/result-synthesizer.test.ts +198 -0
  129. package/src/__tests__/resume-context.test.ts +4898 -0
  130. package/src/__tests__/retrieval-eval.test.ts +163 -0
  131. package/src/__tests__/retrieval-trace.test.ts +213 -0
  132. package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
  133. package/src/__tests__/retriever-lazy-init-dispatch.test.ts +123 -0
  134. package/src/__tests__/retroactive-boost.test.ts +223 -0
  135. package/src/__tests__/salience-scoring.test.ts +106 -0
  136. package/src/__tests__/scope-inventory.test.ts +200 -0
  137. package/src/__tests__/scope-policy.test.ts +69 -0
  138. package/src/__tests__/scope-suggester.test.ts +80 -0
  139. package/src/__tests__/session-checkpoint.test.ts +291 -0
  140. package/src/__tests__/session-distiller.test.ts +640 -0
  141. package/src/__tests__/session-gc.test.ts +215 -0
  142. package/src/__tests__/skill-engine.test.ts +552 -0
  143. package/src/__tests__/skill-promotion.test.ts +654 -0
  144. package/src/__tests__/skill-verifier.test.ts +122 -0
  145. package/src/__tests__/source-heartbeat.test.ts +250 -0
  146. package/src/__tests__/stable-context-length-contract.test.ts +62 -0
  147. package/src/__tests__/store-patch-metadata.test.ts +205 -0
  148. package/src/__tests__/store-read-consistency.test.ts +122 -0
  149. package/src/__tests__/store-schema-migration.test.ts +87 -0
  150. package/src/__tests__/store-scope-audit.test.ts +81 -0
  151. package/src/__tests__/store-upsert.test.ts +201 -0
  152. package/src/__tests__/supersede-chain.test.ts +154 -0
  153. package/src/__tests__/temporal-parser.test.ts +241 -0
  154. package/src/__tests__/temporal-validity.test.ts +160 -0
  155. package/src/__tests__/term-registry-terminal-roster.test.ts +127 -0
  156. package/src/__tests__/term-registry.test.ts +121 -0
  157. package/src/__tests__/tool-error-surfacing.test.ts +98 -0
  158. package/src/__tests__/tool-output-compressor.test.ts +160 -0
  159. package/src/__tests__/topic-tag.test.ts +113 -0
  160. package/src/__tests__/trusted-memory.test.ts +839 -0
  161. package/src/__tests__/ui-server-security.test.ts +93 -0
  162. package/src/__tests__/usage-tracker.test.ts +287 -0
  163. package/src/__tests__/version-manager.test.ts +213 -0
  164. package/src/__tests__/workflow-observation-skill-outcome.test.ts +352 -0
  165. package/src/__tests__/workflow-observation.test.ts +433 -0
  166. package/src/__tests__/write-verifier.test.ts +122 -0
  167. package/src/access-tracker.ts +90 -34
  168. package/src/activity-counter.ts +46 -18
  169. package/src/adaptive-retrieval.ts +11 -0
  170. package/src/aliases.ts +133 -0
  171. package/src/api-server.ts +36 -9
  172. package/src/audit-log.ts +4 -2
  173. package/src/auto-consolidation.ts +8 -3
  174. package/src/auto-gc.ts +85 -21
  175. package/src/auto-recall.ts +61 -1
  176. package/src/belief-history.ts +164 -0
  177. package/src/capture-engine.ts +142 -32
  178. package/src/cli.ts +268 -41
  179. package/src/confidence-tracker.ts +36 -18
  180. package/src/conflict-engine.ts +24 -3
  181. package/src/consolidation-engine.ts +255 -23
  182. package/src/context-composer-stable-selection.ts +7 -1
  183. package/src/context-composer.ts +8 -1
  184. package/src/data-checkup.ts +96 -7
  185. package/src/decay-engine.ts +59 -7
  186. package/src/distill-lock.ts +194 -15
  187. package/src/doctor.ts +13 -4
  188. package/src/dream-pipeline.ts +473 -41
  189. package/src/env-config.ts +118 -0
  190. package/src/error-signature.ts +153 -0
  191. package/src/error-taxonomy.ts +115 -0
  192. package/src/eval.ts +402 -14
  193. package/src/freshness.ts +192 -0
  194. package/src/http-request-policy.ts +148 -0
  195. package/src/idempotency.ts +8 -0
  196. package/src/ingest.ts +308 -12
  197. package/src/kg-extractor.ts +13 -3
  198. package/src/kg-store.ts +236 -44
  199. package/src/llm-client.ts +106 -11
  200. package/src/llm-consolidation.ts +2 -1
  201. package/src/mcp-server.ts +87 -1800
  202. package/src/mcp-tool-deps.ts +48 -0
  203. package/src/mcp-tools-advanced.ts +954 -0
  204. package/src/mcp-tools-core.ts +637 -0
  205. package/src/mcp-tools-governance.ts +475 -0
  206. package/src/memory-boundaries.ts +26 -3
  207. package/src/memory-evolution.ts +73 -1
  208. package/src/memory-health-rebalance.ts +4 -0
  209. package/src/memory-lint.ts +183 -10
  210. package/src/memory-output.ts +129 -4
  211. package/src/memory-promotion.ts +246 -0
  212. package/src/memory-schema.ts +25 -2
  213. package/src/memory-store-port.ts +46 -0
  214. package/src/multi-vector.ts +2 -1
  215. package/src/narrative-schema.ts +2 -1
  216. package/src/noise-filter.ts +57 -6
  217. package/src/noise-prototype-bank.ts +408 -0
  218. package/src/pii-detector.ts +207 -5
  219. package/src/pivot-apply.ts +399 -0
  220. package/src/preference-matcher.ts +37 -3
  221. package/src/query-expander.ts +90 -9
  222. package/src/recall-gate.ts +305 -0
  223. package/src/result-synthesizer.ts +2 -1
  224. package/src/retention-policy.ts +2 -1
  225. package/src/retrieval-profiles.ts +46 -4
  226. package/src/retriever.ts +354 -157
  227. package/src/retroactive-boost.ts +6 -2
  228. package/src/runtime-config.ts +78 -6
  229. package/src/scope-suggester.ts +113 -0
  230. package/src/session-distiller.ts +23 -2
  231. package/src/session-engine.ts +2 -1
  232. package/src/session-schema.ts +1 -0
  233. package/src/session-store.ts +5 -0
  234. package/src/skill-engine.ts +78 -0
  235. package/src/skill-promotion.ts +304 -48
  236. package/src/skill-schema.ts +16 -3
  237. package/src/skill-verifier.ts +152 -0
  238. package/src/store.ts +321 -57
  239. package/src/term-registry.ts +35 -0
  240. package/src/tool-output-compressor.ts +3 -1
  241. package/src/ui-server.ts +9 -1
  242. package/src/usage-tracker.ts +242 -0
  243. package/src/version-manager.ts +3 -2
  244. package/src/version.ts +19 -0
  245. package/src/workflow-observation-engine.ts +2 -1
  246. package/src/workflow-observation-schema.ts +2 -1
  247. package/src/workflow-observation-store.ts +6 -2
  248. package/.env +0 -4
  249. package/CLAUDE.md +0 -46
  250. package/config.json +0 -55
  251. package/daily-recall.sh +0 -110
  252. package/logs/daily-recall.error.log +0 -0
  253. package/logs/daily-recall.log +0 -60
  254. package/logs/ingest-2026-04-11.log +0 -110
  255. package/logs/ingest-2026-04-12.log +0 -90
  256. package/logs/ingest-2026-04-13.log +0 -87
  257. package/logs/launchagent-stderr.log +0 -0
  258. package/logs/launchagent-stdout.log +0 -0
@@ -14,13 +14,15 @@
14
14
  * LLM-driven consolidation for semantic-level maintenance.
15
15
  */
16
16
 
17
- import type { MemoryStore } from "./store.js";
17
+ import type { MemoryStorePort } from "./memory-store-port.js";
18
18
  import type { LLMClient } from "./llm-client.js";
19
19
  import type { Embedder } from "./embedder.js";
20
- import { ConsolidationEngine, clusterAndConsolidate, DEFAULT_CONSOLIDATION_CONFIG } from "./consolidation-engine.js";
20
+ import { ConsolidationEngine, clusterAndConsolidate, DEFAULT_CONSOLIDATION_CONFIG, isDerivedInsight, type ConsolidationKGSource } from "./consolidation-engine.js";
21
21
  import { maybeRunGc, type GcResult, type AutoGcConfig, DEFAULT_AUTO_GC_CONFIG } from "./auto-gc.js";
22
22
  import { getWriteCount, resetWriteCount } from "./activity-counter.js";
23
+ import { deriveUsageStatus, getUsageMetadata, isUsageSignalActive } from "./usage-tracker.js";
23
24
  import { isActiveMemory } from "./memory-evolution.js";
25
+ import { withLock } from "./distill-lock.js";
24
26
 
25
27
  // ---------------------------------------------------------------------------
26
28
  // Types
@@ -31,12 +33,38 @@ export interface DreamConfig {
31
33
  minWritesForDream: number;
32
34
  /** Consolidation cluster threshold (default: 0.82) */
33
35
  clusterThreshold: number;
36
+ /** 3b semantic clustering threshold (default: 0.68). 独立于 clusterThreshold——
37
+ * 旧实现用魔法 offset(clusterThreshold - 0.07 = 0.75),2026-07-23 三 scope
38
+ * 真实数据造影:相似度 p99 = 0.736/0.739/0.986,0.75 卡在 p99 之上,semantic
39
+ * 簇恒 0、LLM 从未被调、2716 次运行零 insight。0.68 落在甜点区
40
+ * (实测每 scope 产 3~7 个 ≥3 簇);产物质量由 synthesis uptake 指标观察。 */
41
+ semanticClusterThreshold: number;
34
42
  /** Minimum cluster size for insight/pattern generation (default: 3) */
35
43
  minClusterSize: number;
36
44
  /** Enable cross-memory pattern extraction in consolidation (default: true) */
37
45
  extractPatterns: boolean;
38
46
  /** Max entries to scan per consolidation run (default: 500) */
39
47
  maxEntriesPerRun: number;
48
+ /** Wall-clock budget for one `dream --auto` sweep, ms (default: 6h).
49
+ * 防跨天兜底闸,不是调度器。2026-07-24 那轮跑了 4 天 15 小时(见 cli.ts --auto 分支
50
+ * 与 scripts/dream-consolidation.sh:57 的注释),期间堵死三天调度。
51
+ * 已知天花板:超预算后剩余 scope 直接丢弃,而每轮都从列表头重来 → 靠后的 scope 会被
52
+ * 系统性饿死。真解是 session scope TTL(只 dream 近 N 天活跃的),不是把预算调大——
53
+ * scope 数 07-10 是 133、07-29 是 475,只增不减,加预算追不上。 */
54
+ autoRunBudgetMs: number;
55
+ /** Scopes the `--auto` sweep hands off to a dedicated schedule instead of running inline.
56
+ * Exact match only — a prefix rule would silently swallow future `memory-*` scopes.
57
+ *
58
+ * 2026-07-30 实测的动机:那轮 6 小时只处理了 15/507 个 scope,而这 15 个的规模是
59
+ * 118/37/15/190/3075/84/136/76/14/31/39/15/10/6/55 —— `memory` 一个 scope 独占
60
+ * 3075 条(全部 3891 条的 79%),其余都是几十条的小 scope。配合上面那条「每轮都从
61
+ * 列表头重来」,等于每天花掉整个预算巩固同一个巨型 scope,靠后的 492 个永远排不到。
62
+ * 把它挪进 com.recallnest.dream-memory-weekly(周日 12:00,走 DREAM_SCOPE=memory)
63
+ * 后,日常轮次的预算全部留给小 scope。
64
+ *
65
+ * 这是缓解不是根治:scope 只增不减仍在(07-10 是 133、07-29 是 475、07-30 是 507),
66
+ * 真解仍是 session scope TTL —— 只 dream 近 N 天活跃的。 */
67
+ autoExcludeScopes: readonly string[];
40
68
  /** GC config for prune phase */
41
69
  gc: AutoGcConfig;
42
70
  }
@@ -44,9 +72,12 @@ export interface DreamConfig {
44
72
  export const DEFAULT_DREAM_CONFIG: DreamConfig = {
45
73
  minWritesForDream: 10,
46
74
  clusterThreshold: 0.82,
75
+ semanticClusterThreshold: 0.68,
47
76
  minClusterSize: 3,
48
77
  extractPatterns: true,
49
78
  maxEntriesPerRun: 500,
79
+ autoRunBudgetMs: 6 * 60 * 60 * 1000,
80
+ autoExcludeScopes: ["memory"],
50
81
  gc: DEFAULT_AUTO_GC_CONFIG,
51
82
  };
52
83
 
@@ -55,19 +86,78 @@ export interface DreamPhaseResult {
55
86
  detail: string;
56
87
  }
57
88
 
89
+ /**
90
+ * 一轮 dream 的产出归类(2026-08-12 加)。
91
+ *
92
+ * 存在的理由:在此之前 dream 只报 `status=ok`,而 ok 只证明「没崩」,不证明
93
+ * 「产出了东西」。后台阶段的失败形态是「什么都没发生」,它和「跑完了但确实没
94
+ * 东西可做」在日志里长得一模一样 —— 2026-07-23 查出的语义阈值卡在 p99 之上,
95
+ * 就是这样静默跑了 2716 次零 insight 而无人察觉。
96
+ *
97
+ * 刻意**不**做的事:不设「insight 数 ≥ N」这类闸门。数据真不成簇时零产出是
98
+ * 合法的,给它设下限等于造第三个阈值陷阱(前两个是 0.75 语义阈值和 0.35
99
+ * novelty 门,都因为阈值落在真实数据分布之外而静默归零)。这里只区分
100
+ * 「确有副作用」和「合法空转」,两者都不需要任何可调参数。
101
+ */
102
+ export type DreamOutputKind =
103
+ /** 至少写入了一个真实的生命周期副作用(合并/关联/insight/pattern/归档) */
104
+ | "produced"
105
+ /** 四阶段完整跑过,但确实没有值得写入的结果 —— 合法状态,不是失败 */
106
+ | "noop"
107
+ /** 必经阶段没跑(如 consolidate 锁被占),本轮结论不完整 */
108
+ | "partial"
109
+ /** 压根没进管线(写计数不足 / dream 锁被占) */
110
+ | "skipped";
111
+
112
+ export interface DreamOutput {
113
+ kind: DreamOutputKind;
114
+ /** 为什么不是 produced;produced 时省略 */
115
+ reason?: string;
116
+ /** 本轮真实写入的副作用总数。硬不变量:kind === "produced" <=> effectsWritten > 0 */
117
+ effectsWritten: number;
118
+ /**
119
+ * 结构性健康降级 —— **与 kind 正交,不要合并**。
120
+ *
121
+ * 2026-08-12 实测教训:3a 确定性去重不需要向量,3b 语义聚类需要。向量管线整段
122
+ * 断裂时 3a 照样合并、照样产出副作用,于是 effects > 0 把 kind 顶成了 produced,
123
+ * **一条路径的产出掩盖了另一条路径的完全断裂**。这是"跑完了 ≠ 做成了"的变体:
124
+ * 部分做成了,比全都没做更容易蒙混过关。
125
+ *
126
+ * 所以健康必须单列:produced 也可以是 degraded 的。
127
+ */
128
+ degraded?: "no_llm_configured" | "vector_pipeline_empty";
129
+ }
130
+
58
131
  export interface DreamResult {
59
132
  ran: boolean;
60
133
  reason?: string;
61
134
  phases: DreamPhaseResult[];
135
+ /** 本轮到底产出了什么(2026-08-12 加,见 DreamOutputKind 注释) */
136
+ output: DreamOutput;
62
137
  stats: {
63
138
  totalMemories: number;
64
139
  activeMemories: number;
65
140
  writesSinceLastDream: number;
141
+ /** 两条聚类路径的总和,保持向后兼容;排障要看下面拆开的两个。 */
66
142
  clustersFound: number;
143
+ /** 3a 确定性去重找到的簇(不需要 LLM)。 */
144
+ dedupeClustersFound: number;
145
+ /** 3b 语义聚类里达到 minClusterSize 的簇——只有这些会去调 LLM 生成 insight。 */
146
+ semanticClustersFound: number;
67
147
  insightsGenerated: number;
68
148
  patternsExtracted: number;
69
149
  mergedCount: number;
70
150
  archivedCount: number;
151
+ /** 3a 去重路径新建的关联数。此前引擎返回了但被 dream 丢弃,导致「只加了关联」
152
+ * 的一轮会被误判成零产出。 */
153
+ relationsAdded: number;
154
+ /** gather 之后可参与合并的条目数(已排除 dream 自己的派生 insight)。 */
155
+ consolidatableCount: number;
156
+ /** 真正带着非空向量喂进 3b 语义聚类的条目数。
157
+ * 它与 consolidatableCount 的落差就是向量管线的健康读数:
158
+ * consolidatable > 0 而 vectorFed === 0 是结构性故障、不可能是合法的
159
+ * ——2026-07-23 那次 store.list() 恒返回假空向量、84/84 全被滤光,就是这个形状。 */
160
+ semanticVectorFed: number;
71
161
  };
72
162
  }
73
163
 
@@ -75,17 +165,22 @@ export interface DreamResult {
75
165
  // Pipeline
76
166
  // ---------------------------------------------------------------------------
77
167
 
78
- export async function runDream(params: {
79
- store: MemoryStore;
168
+ async function runDreamInner(params: {
169
+ store: MemoryStorePort;
80
170
  llm: LLMClient | null;
81
171
  embedder: Pick<Embedder, "embedPassage">;
82
172
  scope: string;
83
173
  config?: Partial<DreamConfig>;
84
174
  /** Skip the minimum-writes gate (force run) */
85
175
  force?: boolean;
176
+ /** Override the activity-counter stats file (defaults to <dataDir>/activity-stats.json). */
177
+ activityStatsPath?: string;
178
+ /** Optional KG evidence source for triple-overlap merging (null/absent = vector-only) */
179
+ kgStore?: ConsolidationKGSource | null;
86
180
  }): Promise<DreamResult> {
87
181
  const { store, llm, embedder, scope, force = false } = params;
88
182
  const config = { ...DEFAULT_DREAM_CONFIG, ...params.config };
183
+ const statsCfg = params.activityStatsPath ? { statsPath: params.activityStatsPath } : undefined;
89
184
  const phases: DreamPhaseResult[] = [];
90
185
 
91
186
  const stats = {
@@ -93,16 +188,21 @@ export async function runDream(params: {
93
188
  activeMemories: 0,
94
189
  writesSinceLastDream: 0,
95
190
  clustersFound: 0,
191
+ dedupeClustersFound: 0,
192
+ semanticClustersFound: 0,
96
193
  insightsGenerated: 0,
97
194
  patternsExtracted: 0,
98
195
  mergedCount: 0,
99
196
  archivedCount: 0,
197
+ relationsAdded: 0,
198
+ consolidatableCount: 0,
199
+ semanticVectorFed: 0,
100
200
  };
101
201
 
102
202
  // =========================================================================
103
203
  // Phase 1: Orient — assess current memory state
104
204
  // =========================================================================
105
- const writeCount = getWriteCount();
205
+ const writeCount = getWriteCount(scope, statsCfg);
106
206
  stats.writesSinceLastDream = writeCount;
107
207
 
108
208
  const storeStats = await store.stats([scope]);
@@ -112,6 +212,7 @@ export async function runDream(params: {
112
212
  return {
113
213
  ran: false,
114
214
  reason: `insufficient_writes (${writeCount}/${config.minWritesForDream})`,
215
+ output: { kind: "skipped", reason: "insufficient_writes", effectsWritten: 0 },
115
216
  phases: [{
116
217
  phase: "orient",
117
218
  detail: `${stats.totalMemories} memories, ${writeCount} writes since last dream — below threshold`,
@@ -132,52 +233,125 @@ export async function runDream(params: {
132
233
  const active = entries.filter(e => isActiveMemory(e.metadata));
133
234
  stats.activeMemories = active.length;
134
235
 
236
+ // Consolidation derivatives are kept out of the *input* to the next round.
237
+ // A cluster insight is written by the LLM from other entries, so feeding it
238
+ // back as raw material means the next run summarises a summary, and each
239
+ // generation drifts further from what was actually captured with nothing
240
+ // anchoring it back. Their sources stay eligible, so real material still gets
241
+ // re-consolidated as it accumulates.
242
+ //
243
+ // They stay in `active` on purpose: stats and the usage snapshot below are
244
+ // observational, and derivatives are exactly what we want more visibility
245
+ // into, not less. Only consolidation skips them.
246
+ const consolidatable = active.filter(e => !isDerivedInsight(e.metadata));
247
+
248
+ // P0 B-1 观察:离线持久化 usageStatus 快照。写入路径不写 status(写入时
249
+ // useCount 必 >0,永远写不出 cold),所以 cold 只能由离线批量 derive 产生。
250
+ // 持久化值仅供 data_checkup/RIF 等观察视图——任何决策路径必须现场
251
+ // deriveUsageStatus(快照会 stale:之后 useCount/accessCount 继续变化)。
252
+ // best-effort:走 patchMetadata 单写通道,失败不阻断 dream。
253
+ // use 信号未采集时整段跳过——否则持久化的全是假 cold(useCount 恒零)。
254
+ let usageSnapshots = 0;
255
+ for (const entry of isUsageSignalActive() ? active : []) {
256
+ const status = deriveUsageStatus(entry);
257
+ const current = getUsageMetadata(entry).usageStatus;
258
+ if (status === current) continue;
259
+ // 默认态(unused 且从无 usage 对象)不写,避免给全库无谓写放大
260
+ if (status === "unused" && current === undefined) continue;
261
+ try {
262
+ await store.patchMetadata(entry.id, meta => {
263
+ const usage = meta.usage && typeof meta.usage === "object" && !Array.isArray(meta.usage)
264
+ ? (meta.usage as Record<string, unknown>)
265
+ : {};
266
+ meta.usage = { ...usage, usageStatus: status };
267
+ return meta;
268
+ });
269
+ usageSnapshots++;
270
+ } catch { /* observation snapshot must never block dream */ }
271
+ }
272
+
273
+ stats.consolidatableCount = consolidatable.length;
274
+ const derivedCount = active.length - consolidatable.length;
135
275
  phases.push({
136
276
  phase: "gather",
137
- detail: `${active.length} active entries gathered from ${entries.length} total`,
277
+ detail: `${active.length} active entries gathered from ${entries.length} total`
278
+ + (derivedCount > 0 ? `; ${derivedCount} derivatives held back from consolidation` : "")
279
+ + (usageSnapshots > 0 ? `; usage snapshot: ${usageSnapshots} statuses persisted` : ""),
138
280
  });
139
281
 
140
- if (active.length < config.minClusterSize) {
282
+ if (consolidatable.length < config.minClusterSize) {
141
283
  phases.push({ phase: "consolidate", detail: "skipped — too few active entries" });
142
284
  phases.push({ phase: "prune", detail: "skipped — too few entries for GC" });
143
- resetWriteCount();
144
- return { ran: true, reason: "completed_early", phases, stats };
285
+ resetWriteCount(scope, statsCfg);
286
+ return {
287
+ ran: true,
288
+ reason: "completed_early",
289
+ // 素材不足是合法的空转,不是失败 —— 这正是「不设 insight 数下限」的理由所在。
290
+ output: { kind: "noop", reason: "too_few_active_entries", effectsWritten: 0 },
291
+ phases,
292
+ stats,
293
+ };
145
294
  }
146
295
 
147
296
  // =========================================================================
148
297
  // Phase 3: Consolidate — cluster, merge, generate insights + patterns
149
298
  // =========================================================================
150
299
 
151
- // 3a: Deterministic consolidation (merge near-duplicates, link clusters)
152
- const engine = new ConsolidationEngine(store, {
153
- ...DEFAULT_CONSOLIDATION_CONFIG,
154
- clusterThreshold: config.clusterThreshold,
155
- maxEntriesPerRun: config.maxEntriesPerRun,
156
- });
157
- const consolidation = await engine.run(scope);
158
- stats.clustersFound += consolidation.clustersFound;
159
- stats.mergedCount += consolidation.mergedCount;
160
-
161
- // 3b: LLM-driven cluster consolidation (insights + patterns) — requires LLM
162
- if (llm) {
163
- const clusterResult = await clusterAndConsolidate({
164
- entries: active,
165
- embedder,
166
- llm,
167
- store,
168
- scope,
169
- minClusterSize: config.minClusterSize,
170
- clusterThreshold: config.clusterThreshold - 0.07, // slightly lower for semantic clustering
171
- extractPatterns: config.extractPatterns,
172
- });
173
- stats.clustersFound += clusterResult.clustersFound;
174
- stats.insightsGenerated = clusterResult.insightsGenerated;
175
- stats.patternsExtracted = clusterResult.patternsExtracted;
176
- }
300
+ // P2 (codex): run the consolidation phase under the scope's consolidate lock so a
301
+ // standalone consolidate_memories on the same scope can't run concurrently with the
302
+ // dream's own consolidation. Nested inside the dream lock (dream → consolidate →
303
+ // store-write), consistent with the one-way lock order.
304
+ const consolidateOutcome = await withLock(`consolidate-${scope}`, async () => {
305
+ // 3a: Deterministic consolidation (merge near-duplicates, link clusters)
306
+ const engine = new ConsolidationEngine(store, {
307
+ ...DEFAULT_CONSOLIDATION_CONFIG,
308
+ clusterThreshold: config.clusterThreshold,
309
+ maxEntriesPerRun: config.maxEntriesPerRun,
310
+ }, params.kgStore ?? null);
311
+ const consolidation = await engine.run(scope);
312
+ stats.dedupeClustersFound += consolidation.clustersFound;
313
+ stats.clustersFound += consolidation.clustersFound;
314
+ stats.mergedCount += consolidation.mergedCount;
315
+ stats.relationsAdded += consolidation.relationsAdded;
316
+
317
+ // 3b: LLM-driven cluster consolidation (insights + patterns) — requires LLM
318
+ if (llm) {
319
+ // gather 走 store.list(),其性能优化恒返回 vector:[](假空数组)——
320
+ // clusterAndConsolidate 进门就按 vector?.length>0 过滤,不回填则 84/84 全被
321
+ // 滤光、聚类空转(2026-07-23 生产实锤:门槛修对后 semantic 仍恒 0 的第二真凶;
322
+ // promote_scan 同款坑 07-21 已修,dream 这条漏了)。
323
+ const vectorMap = await store.getVectors(consolidatable.map(e => e.id));
324
+ const withVectors = consolidatable.map(e => ({ ...e, vector: vectorMap.get(e.id) ?? [] }));
325
+ // 回填之后仍为空 = 向量管线断了(见 semanticVectorFed 字段注释)。这里只记数,
326
+ // 判定留给 assertDreamSweepHealth,保持本函数无分支。
327
+ stats.semanticVectorFed = withVectors.filter(e => e.vector.length > 0).length;
328
+ const clusterResult = await clusterAndConsolidate({
329
+ entries: withVectors,
330
+ embedder,
331
+ llm,
332
+ store,
333
+ scope,
334
+ minClusterSize: config.minClusterSize,
335
+ clusterThreshold: config.semanticClusterThreshold,
336
+ extractPatterns: config.extractPatterns,
337
+ });
338
+ stats.semanticClustersFound += clusterResult.clustersFound;
339
+ stats.clustersFound += clusterResult.clustersFound;
340
+ stats.insightsGenerated = clusterResult.insightsGenerated;
341
+ stats.patternsExtracted = clusterResult.patternsExtracted;
342
+ }
343
+ }, { onBusy: "skip", expireMs: 600_000 });
177
344
 
178
345
  phases.push({
179
346
  phase: "consolidate",
180
- detail: `${stats.clustersFound} clusters, ${stats.mergedCount} merged, ${stats.insightsGenerated} insights, ${stats.patternsExtracted} patterns`,
347
+ detail: consolidateOutcome.ran
348
+ // 两条路径分开报:dedupe 那条不需要 LLM,semantic 那条才会去调 LLM 生成 insight。
349
+ // 合成一个数字的话,「semantic 一个合格簇都没凑出来」和「凑出来了但 LLM 没产出」
350
+ // 长得一模一样——2026-07 排查时正是被这个加总带偏的。
351
+ ? `${stats.dedupeClustersFound} dedupe-clusters (${stats.mergedCount} merged), `
352
+ + `${stats.semanticClustersFound} semantic-clusters -> `
353
+ + `${stats.insightsGenerated} insights, ${stats.patternsExtracted} patterns`
354
+ : `skipped — another process holds the consolidate lock for scope ${scope}`,
181
355
  });
182
356
 
183
357
  // =========================================================================
@@ -193,10 +367,253 @@ export async function runDream(params: {
193
367
  : `skipped — ${gcResult.reason}`,
194
368
  });
195
369
 
196
- // Reset activity counter after successful dream
197
- resetWriteCount();
370
+ // Reset activity counter after successful dream.
371
+ //
372
+ // 只在 consolidate 真跑过时才清:锁被占时(onBusy: "skip")这个 scope 并没有被真正
373
+ // 处理完,计数必须留着等下一轮,否则它会被当成"已处理"而丢掉本轮积累的写入。
374
+ //
375
+ // 2026-08-12 修复:此处原为无参 `resetWriteCount()`,而签名 (activity-counter.ts:110)
376
+ // 要求 scope 必填 —— 运行时不报错,只是静默执行 `delete stats.scopes[undefined]`,
377
+ // 目标 scope 的计数纹丝不动。后果:成功处理的 scope 永远不出队,达标数只增不减
378
+ // (生产实测 85 → 107 → 135 → 563 → 572),每轮从队首重扫同一批最老的 scope,
379
+ // 6h 预算耗尽后排在后面的永远轮不到(日志:已处理 167/563,剩余 396 本轮跳过)。
380
+ // 早退路径 (:226) 一直是对的。源于 d269159(2026-07-02 per-scope 写计数重构)漏改一处,
381
+ // 该 commit message 自己写着「reset 在末尾清该 scope(含 dream 自写→防永动)」——
382
+ // 意图正确、实现漏了一个参数,活了 41 天。codex + kimi 双路独立冷读同时查出。
383
+ //
384
+ // 注意:清对之后,listScopesAboveThreshold 的 FIFO 自组织才真正开始工作 ——
385
+ // 处理完的 scope 被 delete、下次攒够写入重新插入时排到队尾,被跳过的带原计数留在
386
+ // 队首下轮优先。所以这里不需要额外的游标 / checkpoint 文件。
387
+ if (consolidateOutcome.ran) resetWriteCount(scope, statsCfg);
388
+
389
+ const health = assertDreamSweepHealth({
390
+ llmPresent: llm != null,
391
+ consolidatable: stats.consolidatableCount,
392
+ vectorFed: stats.semanticVectorFed,
393
+ });
394
+ const output = classifyDreamOutput({
395
+ consolidateRan: consolidateOutcome.ran,
396
+ effects: countDreamEffects(stats),
397
+ health,
398
+ });
399
+
400
+ return { ran: true, output, phases, stats };
401
+ }
198
402
 
199
- return { ran: true, phases, stats };
403
+ /**
404
+ * P0-1: run dream under a per-scope cross-process lock. Dream internally does
405
+ * consolidation + gc, so the lock sits at this outermost entry — no nested maintenance
406
+ * locks. If another of the 11 mcp-server processes holds the scope's dream lock, skip
407
+ * rather than queue: a redundant dream on a personal store is pure waste, and the next
408
+ * writer's dream picks up whatever this run skipped.
409
+ */
410
+ export async function runDream(params: {
411
+ store: MemoryStorePort;
412
+ llm: LLMClient | null;
413
+ embedder: Pick<Embedder, "embedPassage">;
414
+ scope: string;
415
+ config?: Partial<DreamConfig>;
416
+ /** Skip the minimum-writes gate (force run) */
417
+ force?: boolean;
418
+ /** Override the activity-counter stats file (defaults to <dataDir>/activity-stats.json). */
419
+ activityStatsPath?: string;
420
+ /** Optional KG evidence source for triple-overlap merging (null/absent = vector-only) */
421
+ kgStore?: ConsolidationKGSource | null;
422
+ }): Promise<DreamResult> {
423
+ const outcome = await withLock(
424
+ `dream-${params.scope}`,
425
+ () => runDreamInner(params),
426
+ { onBusy: "skip", expireMs: 600_000 },
427
+ );
428
+ if (outcome.ran) return outcome.result;
429
+ return {
430
+ ran: false,
431
+ reason: "locked_by_another_process",
432
+ output: { kind: "skipped", reason: "dream_lock_held", effectsWritten: 0 },
433
+ phases: [{ phase: "orient", detail: `another process holds the dream lock for scope ${params.scope}` }],
434
+ stats: {
435
+ totalMemories: 0,
436
+ activeMemories: 0,
437
+ writesSinceLastDream: 0,
438
+ clustersFound: 0,
439
+ dedupeClustersFound: 0,
440
+ semanticClustersFound: 0,
441
+ insightsGenerated: 0,
442
+ patternsExtracted: 0,
443
+ mergedCount: 0,
444
+ archivedCount: 0,
445
+ relationsAdded: 0,
446
+ consolidatableCount: 0,
447
+ semanticVectorFed: 0,
448
+ },
449
+ };
450
+ }
451
+
452
+ // ---------------------------------------------------------------------------
453
+ // Output classification (2026-08-12)
454
+ // ---------------------------------------------------------------------------
455
+
456
+ export interface DreamHealthVerdict {
457
+ healthy: boolean;
458
+ /** 不健康时的机器可读原因;健康时省略 */
459
+ reason?: "no_llm_configured" | "vector_pipeline_empty";
460
+ }
461
+
462
+ /**
463
+ * 结构性健康断言 —— **只认两种确定性零值**,零歧义、免调参、误伤面≈0。
464
+ *
465
+ * 为什么是这两个而不是「insight 数 < N」:这个库已经被阈值坑过两次
466
+ * (0.75 语义聚类阈值卡在真实 p99 之上;0.35 novelty 门把最相关的命中全排除),
467
+ * 共同形状都是**断言条件依赖数据分布**,分布一偏就静默归零。下面两条都不依赖
468
+ * 任何分布,它们描述的是结构断裂,不是数据稀疏:
469
+ *
470
+ * 1. `llmPresent === false` —— 没配 LLM 时 3b 整段不跑,insight 恒为 0。
471
+ * 这是确定性的配置缺陷,不是「今天数据不成簇」。
472
+ * 2. `consolidatable > 0 但 vectorFed === 0` —— 有素材可聚,却一条带向量的都没有。
473
+ * dream 在调用前刚做完 getVectors 回填,回填后还全空只有一种解释:向量管线断了。
474
+ * 这正是 2026-07-23 第二真凶(store.list() 恒返回假空向量)的复发通道。
475
+ *
476
+ * 刻意留在门外的:`maxPairSim < threshold`(能结构性判定「阈值又落在分布之上」)
477
+ * 只适合当观察字段,不适合当闸门 —— 数据真分散时它会天天告警,而告警疲劳比没有
478
+ * 告警更糟。
479
+ */
480
+ export function assertDreamSweepHealth(input: {
481
+ llmPresent: boolean;
482
+ consolidatable: number;
483
+ vectorFed: number;
484
+ }): DreamHealthVerdict {
485
+ if (!input.llmPresent) return { healthy: false, reason: "no_llm_configured" };
486
+ if (input.consolidatable > 0 && input.vectorFed === 0) {
487
+ return { healthy: false, reason: "vector_pipeline_empty" };
488
+ }
489
+ return { healthy: true };
490
+ }
491
+
492
+ /** 本轮真实写入的生命周期副作用总数。五项都是「库里确实多/少了东西」,
493
+ * 不含任何只读统计 —— 这是 produced/noop 之分的唯一依据。 */
494
+ export function countDreamEffects(stats: DreamResult["stats"]): number {
495
+ return stats.mergedCount
496
+ + stats.relationsAdded
497
+ + stats.insightsGenerated
498
+ + stats.patternsExtracted
499
+ + stats.archivedCount;
500
+ }
501
+
502
+ /**
503
+ * 产出计量行的唯一格式来源。
504
+ *
505
+ * 抽出来是因为它有一个**进程外的消费者**:`dream-consolidation.sh` 的存在性闸靠
506
+ * 字面量 `[[DREAM_METRICS]]` 匹配,报了 ok 却没有这行就判整轮失败。两条 CLI 路径
507
+ * (`--auto` 与单 scope)各写一份格式,迟早漂;漂了的那条会被闸当成"断言缺失"误杀。
508
+ *
509
+ * 2026-08-13 补单 scope 路径时发现:`--scope memory` 分支**从来没打过这行**,
510
+ * 而闸是 08-12 加的 —— 也就是说 08-16 周日的 memory 轮次即使跑成功也会被判失败。
511
+ * 这是那道闸的第一个真实误伤(它自称"误伤面≈0",成立的前提是所有 ok 路径都打这行)。
512
+ */
513
+ export function formatDreamMetrics(input: {
514
+ tally: Record<DreamOutputKind, number>;
515
+ degraded: number;
516
+ effects: number;
517
+ processed: number;
518
+ total: number;
519
+ }): string {
520
+ return `[[DREAM_METRICS]] produced=${input.tally.produced} noop=${input.tally.noop} `
521
+ + `partial=${input.tally.partial} skipped=${input.tally.skipped} `
522
+ + `degraded=${input.degraded} effects=${input.effects} `
523
+ + `processed=${input.processed}/${input.total}`;
524
+ }
525
+
526
+ /** 把一轮 dream 归类。硬不变量:`kind === "produced"` <=> `effectsWritten > 0`。 */
527
+ export function classifyDreamOutput(input: {
528
+ consolidateRan: boolean;
529
+ effects: number;
530
+ health: DreamHealthVerdict;
531
+ }): DreamOutput {
532
+ // 必经阶段没跑完,本轮结论不完整 —— 不能算 noop(那会掩盖「没跑」和「跑了没东西」的区别),
533
+ // 也不能算 produced。partial 同时是「不要清写计数」的信号。
534
+ // degraded 独立于 kind 计算:结构断裂不该被另一条路径的产出掩盖(见 DreamOutput.degraded)。
535
+ const degraded = input.health.healthy ? undefined : input.health.reason;
536
+
537
+ if (!input.consolidateRan) {
538
+ return { kind: "partial", reason: "consolidate_lock_held", effectsWritten: input.effects, degraded };
539
+ }
540
+ if (input.effects > 0) return { kind: "produced", effectsWritten: input.effects, degraded };
541
+ return {
542
+ kind: "noop",
543
+ // 健康的零产出叫 no_qualified_work(合法);不健康的零产出直接把故障原因摆在 reason 上。
544
+ reason: degraded ?? "no_qualified_work",
545
+ effectsWritten: 0,
546
+ degraded,
547
+ };
548
+ }
549
+
550
+ // ---------------------------------------------------------------------------
551
+ // Failure triage (--auto sweep)
552
+ // ---------------------------------------------------------------------------
553
+
554
+ export type DreamFailureKind = "transient" | "fatal";
555
+
556
+ /** 已知的环境性失败——重跑同一轮救不了,也不代表代码/数据有缺陷。
557
+ * 故意用精确 pattern 而非宽泛关键词:分类错成 transient 会静默真 bug,
558
+ * 错成 fatal 只是多一次告警。拿不准就让它落到 fatal。 */
559
+ const TRANSIENT_FAILURE_PATTERNS: readonly RegExp[] = [
560
+ /lock '[^']*' timed out/i, // distill-lock.ts 跨进程 store-write 锁竞争
561
+ /request was aborted/i, // llm-client.ts:757 的 15s AbortController 超时
562
+ /econnreset|etimedout|socket hang up|fetch failed/i, // 网络层
563
+ ];
564
+
565
+ /**
566
+ * 把单个 scope 的 dream 失败分成「环境性瞬态」和「代码/数据缺陷」。
567
+ *
568
+ * 2026-07-29 生产分诊依据(dream-consolidation-launchd.log):
569
+ * fatal — `TypeError: Array.from requires an array-like object`(store.ts 病行,d1a2a9c 已修)
570
+ * transient — `lock 'store-write' timed out after 10000ms (held by another process)`
571
+ * transient — `Error: Request was aborted.`
572
+ */
573
+ export function classifyDreamFailure(message: string): DreamFailureKind {
574
+ return TRANSIENT_FAILURE_PATTERNS.some(re => re.test(message)) ? "transient" : "fatal";
575
+ }
576
+
577
+ /** transient 失败占比超过这个比例仍判整轮失败——否则"475 个全被锁挡住"会假绿。 */
578
+ export const DREAM_TRANSIENT_BLOCK_RATIO = 0.2;
579
+
580
+ /**
581
+ * 一轮 `dream --auto` 是否该判 blocked。
582
+ *
583
+ * 旧语义是「任意一个 scope 抛异常即 blocked」,在 475 个 scope 的规模下等于
584
+ * 「有一个撞上锁就整轮红」,而红了会让 shell 脚本走重试 → 重跑全量。
585
+ * 新语义:代码/数据缺陷一票否决;环境性失败要成规模才算整轮失败。
586
+ */
587
+ export function shouldBlockDreamRun(params: {
588
+ totalScopes: number;
589
+ fatalFailures: number;
590
+ transientFailures: number;
591
+ }): boolean {
592
+ if (params.fatalFailures > 0) return true;
593
+ if (params.totalScopes <= 0) return false;
594
+ return params.transientFailures / params.totalScopes > DREAM_TRANSIENT_BLOCK_RATIO;
595
+ }
596
+
597
+ /**
598
+ * 把 `--auto` 的达标 scope 列表拆成「本轮跑」和「交给专用 schedule」两份。
599
+ *
600
+ * 精确匹配,不做前缀——前缀规则会连带吞掉将来可能出现的 `memory-*`。
601
+ * 保持原顺序:调用方靠列表顺序决定谁先跑,排序策略的改动不该藏进这个函数。
602
+ *
603
+ * 用处见 DreamConfig.autoExcludeScopes 的注释:2026-07-30 实测 `memory` 一个 scope
604
+ * 独占 79% 的记忆条数,把它挪走后日常轮次的 6h 预算才轮得到靠后的小 scope。
605
+ */
606
+ export function partitionAutoDreamScopes(
607
+ scopes: readonly string[],
608
+ excluded: readonly string[],
609
+ ): { run: string[]; deferred: string[] } {
610
+ const excludeSet = new Set(excluded);
611
+ const run: string[] = [];
612
+ const deferred: string[] = [];
613
+ for (const scope of scopes) {
614
+ (excludeSet.has(scope) ? deferred : run).push(scope);
615
+ }
616
+ return { run, deferred };
200
617
  }
201
618
 
202
619
  // ---------------------------------------------------------------------------
@@ -209,18 +626,33 @@ export function formatDreamResult(result: DreamResult): string {
209
626
  }
210
627
 
211
628
  const lines = [
212
- "Dream completed",
629
+ // 只有真写入了副作用才说 completed;合法空转说 noop,必经阶段没跑说 partial。
630
+ // 「跑完了」和「做成了」从这里开始就不再是同一句话。
631
+ result.output.kind === "produced" ? "Dream completed" : `Dream ${result.output.kind}`,
213
632
  "",
214
633
  ...result.phases.map(p => `[${p.phase}] ${p.detail}`),
215
634
  "",
216
635
  "Stats:",
217
636
  ` Total memories: ${result.stats.totalMemories}`,
218
637
  ` Active: ${result.stats.activeMemories}`,
219
- ` Clusters: ${result.stats.clustersFound}`,
638
+ ` Clusters: ${result.stats.clustersFound} (dedupe ${result.stats.dedupeClustersFound} + semantic ${result.stats.semanticClustersFound})`,
220
639
  ` Insights: ${result.stats.insightsGenerated}`,
221
640
  ` Patterns: ${result.stats.patternsExtracted}`,
222
641
  ` Merged: ${result.stats.mergedCount}`,
642
+ ` Relations: ${result.stats.relationsAdded}`,
223
643
  ` Archived: ${result.stats.archivedCount}`,
644
+ // 漏斗读数:consolidatable → vector-fed → semantic 簇 → insight。
645
+ // 哪一级掉到 0,断在哪一级就一目了然,不用再去猜。
646
+ ` Funnel: ${result.stats.consolidatableCount} consolidatable -> `
647
+ + `${result.stats.semanticVectorFed} vector-fed -> `
648
+ + `${result.stats.semanticClustersFound} semantic-clusters -> `
649
+ + `${result.stats.insightsGenerated} insights`,
650
+ ` Output: ${result.output.kind}`
651
+ + (result.output.reason ? ` (${result.output.reason})` : "")
652
+ + ` — ${result.output.effectsWritten} effects written`,
653
+ ...(result.output.degraded
654
+ ? [` ⚠ DEGRADED: ${result.output.degraded} — 有产出不代表没断,见上面的 Funnel`]
655
+ : []),
224
656
  ];
225
657
 
226
658
  return lines.join("\n");