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.
- package/CHANGELOG.md +187 -0
- package/README.md +49 -38
- package/README_CN.md +46 -31
- package/assets/ui/app.js +1031 -0
- package/assets/ui/index.html +224 -0
- package/assets/ui/styles.css +781 -0
- package/config.json.example +4 -1
- package/docs/api-reference.md +1291 -0
- package/docs/architecture.md +168 -0
- package/docs/connector-spec.md +126 -0
- package/eval/cases-canary.json +58 -0
- package/eval/cases-scope-robustness.json +278 -0
- package/eval/cases.json +117 -20
- package/package.json +25 -4
- package/src/__tests__/access-tracker-novelty.test.ts +304 -0
- package/src/__tests__/activity-counter.test.ts +125 -0
- package/src/__tests__/adaptive-fidelity.test.ts +102 -0
- package/src/__tests__/admission-control.test.ts +206 -0
- package/src/__tests__/anchor-generator.test.ts +67 -0
- package/src/__tests__/api-server-security.test.ts +102 -0
- package/src/__tests__/auto-consolidation.test.ts +168 -0
- package/src/__tests__/auto-recall.test.ts +131 -0
- package/src/__tests__/babel-integration.test.ts +144 -0
- package/src/__tests__/batch-dedup.test.ts +65 -0
- package/src/__tests__/belief-history.test.ts +220 -0
- package/src/__tests__/canary-eval.test.ts +114 -0
- package/src/__tests__/capture-engine.test.ts +1328 -0
- package/src/__tests__/capture-heuristic.test.ts +171 -0
- package/src/__tests__/cascade-forget.test.ts +210 -0
- package/src/__tests__/category-min-scores.test.ts +116 -0
- package/src/__tests__/cc8-cc1-essential-context.test.ts +298 -0
- package/src/__tests__/circuit-breaker.test.ts +99 -0
- package/src/__tests__/cluster-consolidation.test.ts +945 -0
- package/src/__tests__/confidence-metadata.test.ts +125 -0
- package/src/__tests__/confidence-tracker.test.ts +261 -0
- package/src/__tests__/conflict-advisor.test.ts +206 -0
- package/src/__tests__/conflict-escalation.test.ts +149 -0
- package/src/__tests__/conflict-lifecycle.test.ts +83 -0
- package/src/__tests__/conflict-output.test.ts +85 -0
- package/src/__tests__/conflict-store.test.ts +418 -0
- package/src/__tests__/connector-v1.test.ts +181 -0
- package/src/__tests__/consolidation-engine.test.ts +430 -0
- package/src/__tests__/constructive-retrieval.test.ts +509 -0
- package/src/__tests__/context-collapse-renderer.test.ts +214 -0
- package/src/__tests__/context-composer-pins.test.ts +64 -0
- package/src/__tests__/context-composer-task-selection.test.ts +24 -0
- package/src/__tests__/context-reconstructor.test.ts +65 -0
- package/src/__tests__/context-renderer.test.ts +91 -0
- package/src/__tests__/continuity-eval.test.ts +275 -0
- package/src/__tests__/continuity-schema.test.ts +97 -0
- package/src/__tests__/contracts.test.ts +262 -0
- package/src/__tests__/conversation-importer.test.ts +692 -0
- package/src/__tests__/core-recall-eval.test.ts +235 -0
- package/src/__tests__/core-summary.test.ts +109 -0
- package/src/__tests__/data-checkup.test.ts +276 -0
- package/src/__tests__/decay-exemption.test.ts +129 -0
- package/src/__tests__/dedup-actions.test.ts +58 -0
- package/src/__tests__/dedup-guard.test.ts +39 -0
- package/src/__tests__/deterministic-id.test.ts +39 -0
- package/src/__tests__/distill-lock.test.ts +305 -0
- package/src/__tests__/distill-session-argshape.test.ts +79 -0
- package/src/__tests__/doctor.test.ts +319 -0
- package/src/__tests__/dream-pipeline.test.ts +780 -0
- package/src/__tests__/drill-down.test.ts +108 -0
- package/src/__tests__/embedder.test.ts +122 -0
- package/src/__tests__/emotion-decay.test.ts +46 -0
- package/src/__tests__/emotion-detector.test.ts +56 -0
- package/src/__tests__/emotion-phase1-integration.test.ts +153 -0
- package/src/__tests__/emotion-retrieval.test.ts +56 -0
- package/src/__tests__/entity-resolver.test.ts +58 -0
- package/src/__tests__/env-config.test.ts +151 -0
- package/src/__tests__/error-signature-boost.test.ts +87 -0
- package/src/__tests__/error-signature.test.ts +95 -0
- package/src/__tests__/error-taxonomy.test.ts +129 -0
- package/src/__tests__/ethics-layer.test.ts +476 -0
- package/src/__tests__/event-segmenter.test.ts +153 -0
- package/src/__tests__/feature-flags-integration.test.ts +54 -0
- package/src/__tests__/frequency-tracker.test.ts +113 -0
- package/src/__tests__/freshness.test.ts +186 -0
- package/src/__tests__/graph-export.test.ts +489 -0
- package/src/__tests__/hotness-blend.test.ts +283 -0
- package/src/__tests__/ingest-chunking.test.ts +44 -0
- package/src/__tests__/ingest.test.ts +360 -0
- package/src/__tests__/interference-detector.test.ts +203 -0
- package/src/__tests__/kg-extractor.test.ts +192 -0
- package/src/__tests__/kg-store.test.ts +300 -0
- package/src/__tests__/llm-client-pivot.test.ts +628 -0
- package/src/__tests__/llm-consolidation.test.ts +226 -0
- package/src/__tests__/maintenance-locks.test.ts +189 -0
- package/src/__tests__/mcp-efficiency.test.ts +288 -0
- package/src/__tests__/mcp-related-scopes.test.ts +151 -0
- package/src/__tests__/memory-boundaries.test.ts +282 -0
- package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +345 -0
- package/src/__tests__/memory-evolution.test.ts +271 -0
- package/src/__tests__/memory-health-rebalance.test.ts +144 -0
- package/src/__tests__/memory-lint.test.ts +421 -0
- package/src/__tests__/memory-output.test.ts +215 -0
- package/src/__tests__/memory-promotion.test.ts +315 -0
- package/src/__tests__/migrate-dedup-ids.test.ts +65 -0
- package/src/__tests__/multi-vector.test.ts +242 -0
- package/src/__tests__/narrative-architecture.test.ts +456 -0
- package/src/__tests__/noise-filter.test.ts +205 -0
- package/src/__tests__/noise-prototype-bank.test.ts +280 -0
- package/src/__tests__/obsidian-connector.test.ts +363 -0
- package/src/__tests__/omitted-reporting.test.ts +98 -0
- package/src/__tests__/pending-review.test.ts +77 -0
- package/src/__tests__/pii-redact.test.ts +333 -0
- package/src/__tests__/pivot-apply.test.ts +373 -0
- package/src/__tests__/pivot-distill-supervisor.test.ts +457 -0
- package/src/__tests__/pivot-distill.test.ts +1178 -0
- package/src/__tests__/post-retrieval-filter.test.ts +178 -0
- package/src/__tests__/ppr-traversal.test.ts +256 -0
- package/src/__tests__/prediction-engine.test.ts +349 -0
- package/src/__tests__/predictive-prospective-memory.test.ts +416 -0
- package/src/__tests__/preference-guard-smoke.test.ts +211 -0
- package/src/__tests__/preference-matcher.test.ts +282 -0
- package/src/__tests__/preference-slots.test.ts +205 -0
- package/src/__tests__/prospective-memory.test.ts +283 -0
- package/src/__tests__/pull-from-macbook-sync-only.test.ts +679 -0
- package/src/__tests__/query-entity-detector.test.ts +171 -0
- package/src/__tests__/query-expander-alias.test.ts +146 -0
- package/src/__tests__/query-expander.test.ts +34 -0
- package/src/__tests__/recall-gate.test.ts +266 -0
- package/src/__tests__/recall-governor.test.ts +227 -0
- package/src/__tests__/recall-mode.test.ts +64 -0
- package/src/__tests__/resolve-db-path.test.ts +130 -0
- package/src/__tests__/resolve-tier.test.ts +64 -0
- package/src/__tests__/result-synthesizer.test.ts +198 -0
- package/src/__tests__/resume-context.test.ts +4898 -0
- package/src/__tests__/retrieval-eval.test.ts +163 -0
- package/src/__tests__/retrieval-trace.test.ts +213 -0
- package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
- package/src/__tests__/retriever-lazy-init-dispatch.test.ts +123 -0
- package/src/__tests__/retroactive-boost.test.ts +223 -0
- package/src/__tests__/salience-scoring.test.ts +106 -0
- package/src/__tests__/scope-inventory.test.ts +200 -0
- package/src/__tests__/scope-policy.test.ts +69 -0
- package/src/__tests__/scope-suggester.test.ts +80 -0
- package/src/__tests__/session-checkpoint.test.ts +291 -0
- package/src/__tests__/session-distiller.test.ts +640 -0
- package/src/__tests__/session-gc.test.ts +215 -0
- package/src/__tests__/skill-engine.test.ts +552 -0
- package/src/__tests__/skill-promotion.test.ts +654 -0
- package/src/__tests__/skill-verifier.test.ts +122 -0
- package/src/__tests__/source-heartbeat.test.ts +250 -0
- package/src/__tests__/stable-context-length-contract.test.ts +62 -0
- package/src/__tests__/store-patch-metadata.test.ts +205 -0
- package/src/__tests__/store-read-consistency.test.ts +122 -0
- package/src/__tests__/store-schema-migration.test.ts +87 -0
- package/src/__tests__/store-scope-audit.test.ts +81 -0
- package/src/__tests__/store-upsert.test.ts +201 -0
- package/src/__tests__/supersede-chain.test.ts +154 -0
- package/src/__tests__/temporal-parser.test.ts +241 -0
- package/src/__tests__/temporal-validity.test.ts +160 -0
- package/src/__tests__/term-registry-terminal-roster.test.ts +127 -0
- package/src/__tests__/term-registry.test.ts +121 -0
- package/src/__tests__/tool-error-surfacing.test.ts +98 -0
- package/src/__tests__/tool-output-compressor.test.ts +160 -0
- package/src/__tests__/topic-tag.test.ts +113 -0
- package/src/__tests__/trusted-memory.test.ts +839 -0
- package/src/__tests__/ui-server-security.test.ts +93 -0
- package/src/__tests__/usage-tracker.test.ts +287 -0
- package/src/__tests__/version-manager.test.ts +213 -0
- package/src/__tests__/workflow-observation-skill-outcome.test.ts +352 -0
- package/src/__tests__/workflow-observation.test.ts +433 -0
- package/src/__tests__/write-verifier.test.ts +122 -0
- package/src/access-tracker.ts +90 -34
- package/src/activity-counter.ts +46 -18
- package/src/adaptive-retrieval.ts +11 -0
- package/src/aliases.ts +133 -0
- package/src/api-server.ts +36 -9
- package/src/audit-log.ts +4 -2
- package/src/auto-consolidation.ts +8 -3
- package/src/auto-gc.ts +85 -21
- package/src/auto-recall.ts +61 -1
- package/src/belief-history.ts +164 -0
- package/src/capture-engine.ts +142 -32
- package/src/cli.ts +268 -41
- package/src/confidence-tracker.ts +36 -18
- package/src/conflict-engine.ts +24 -3
- package/src/consolidation-engine.ts +255 -23
- package/src/context-composer-stable-selection.ts +7 -1
- package/src/context-composer.ts +8 -1
- package/src/data-checkup.ts +96 -7
- package/src/decay-engine.ts +59 -7
- package/src/distill-lock.ts +194 -15
- package/src/doctor.ts +13 -4
- package/src/dream-pipeline.ts +473 -41
- package/src/env-config.ts +118 -0
- package/src/error-signature.ts +153 -0
- package/src/error-taxonomy.ts +115 -0
- package/src/eval.ts +402 -14
- package/src/freshness.ts +192 -0
- package/src/http-request-policy.ts +148 -0
- package/src/idempotency.ts +8 -0
- package/src/ingest.ts +308 -12
- package/src/kg-extractor.ts +13 -3
- package/src/kg-store.ts +236 -44
- package/src/llm-client.ts +106 -11
- package/src/llm-consolidation.ts +2 -1
- package/src/mcp-server.ts +87 -1800
- package/src/mcp-tool-deps.ts +48 -0
- package/src/mcp-tools-advanced.ts +954 -0
- package/src/mcp-tools-core.ts +637 -0
- package/src/mcp-tools-governance.ts +475 -0
- package/src/memory-boundaries.ts +26 -3
- package/src/memory-evolution.ts +73 -1
- package/src/memory-health-rebalance.ts +4 -0
- package/src/memory-lint.ts +183 -10
- package/src/memory-output.ts +129 -4
- package/src/memory-promotion.ts +246 -0
- package/src/memory-schema.ts +25 -2
- package/src/memory-store-port.ts +46 -0
- package/src/multi-vector.ts +2 -1
- package/src/narrative-schema.ts +2 -1
- package/src/noise-filter.ts +57 -6
- package/src/noise-prototype-bank.ts +408 -0
- package/src/pii-detector.ts +207 -5
- package/src/pivot-apply.ts +399 -0
- package/src/preference-matcher.ts +37 -3
- package/src/query-expander.ts +90 -9
- package/src/recall-gate.ts +305 -0
- package/src/result-synthesizer.ts +2 -1
- package/src/retention-policy.ts +2 -1
- package/src/retrieval-profiles.ts +46 -4
- package/src/retriever.ts +354 -157
- package/src/retroactive-boost.ts +6 -2
- package/src/runtime-config.ts +78 -6
- package/src/scope-suggester.ts +113 -0
- package/src/session-distiller.ts +23 -2
- package/src/session-engine.ts +2 -1
- package/src/session-schema.ts +1 -0
- package/src/session-store.ts +5 -0
- package/src/skill-engine.ts +78 -0
- package/src/skill-promotion.ts +304 -48
- package/src/skill-schema.ts +16 -3
- package/src/skill-verifier.ts +152 -0
- package/src/store.ts +321 -57
- package/src/term-registry.ts +35 -0
- package/src/tool-output-compressor.ts +3 -1
- package/src/ui-server.ts +9 -1
- package/src/usage-tracker.ts +242 -0
- package/src/version-manager.ts +3 -2
- package/src/version.ts +19 -0
- package/src/workflow-observation-engine.ts +2 -1
- package/src/workflow-observation-schema.ts +2 -1
- package/src/workflow-observation-store.ts +6 -2
- package/.env +0 -4
- package/CLAUDE.md +0 -46
- package/config.json +0 -55
- package/daily-recall.sh +0 -110
- package/logs/daily-recall.error.log +0 -0
- package/logs/daily-recall.log +0 -60
- package/logs/ingest-2026-04-11.log +0 -110
- package/logs/ingest-2026-04-12.log +0 -90
- package/logs/ingest-2026-04-13.log +0 -87
- package/logs/launchagent-stderr.log +0 -0
- package/logs/launchagent-stdout.log +0 -0
package/src/dream-pipeline.ts
CHANGED
|
@@ -14,13 +14,15 @@
|
|
|
14
14
|
* LLM-driven consolidation for semantic-level maintenance.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import type {
|
|
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
|
-
|
|
79
|
-
store:
|
|
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 (
|
|
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 {
|
|
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
|
-
//
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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");
|