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.
- package/CHANGELOG.md +292 -0
- package/README.md +280 -195
- package/README_CN.md +266 -199
- package/config.json.example +8 -1
- package/docs/api-reference.md +1 -1
- package/integrations/README.md +1 -1
- package/integrations/agy/README.md +73 -0
- package/integrations/agy/setup.sh +75 -0
- package/integrations/setup-all.sh +1 -1
- package/package.json +7 -5
- package/src/access-tracker.ts +10 -1
- package/src/activity-counter.ts +69 -11
- package/src/age-label.ts +29 -0
- package/src/audit-log.ts +35 -0
- package/src/auto-gc.ts +97 -30
- package/src/capture-engine.ts +26 -4
- package/src/cli.ts +224 -10
- package/src/consolidation-engine.ts +317 -123
- package/src/context-composer-stable.ts +5 -1
- package/src/data-checkup.ts +17 -3
- package/src/dream-pipeline.ts +90 -12
- package/src/embedder.ts +44 -5
- package/src/env-config.ts +71 -0
- package/src/forget-engine.ts +8 -2
- package/src/gateway-server.ts +272 -0
- package/src/ingest.ts +248 -22
- package/src/kg-extractor.ts +17 -0
- package/src/llm-client.ts +86 -25
- package/src/mcp-server.ts +2 -1
- package/src/mcp-tools-advanced.ts +7 -2
- package/src/mcp-tools-core.ts +11 -1
- package/src/mcp-tools-governance.ts +45 -3
- package/src/memory-evolution.ts +16 -0
- package/src/memory-output.ts +47 -8
- package/src/memory-promotion.ts +280 -0
- package/src/memory-store-port.ts +32 -3
- package/src/memory-utility.ts +314 -0
- package/src/pivot-apply.ts +52 -1
- package/src/pivot-evidence.ts +128 -0
- package/src/ppr-traversal.ts +25 -1
- package/src/recall-ledger.ts +76 -0
- package/src/retriever.ts +82 -9
- package/src/runtime-config.ts +9 -0
- package/src/scope-policy.ts +22 -1
- package/src/session-engine.ts +11 -4
- package/src/session-output.ts +13 -4
- package/src/session-store.ts +2 -3
- package/src/skill-promotion.ts +218 -1
- package/src/store.ts +231 -71
- package/src/synthesis-contract.ts +286 -0
- package/src/version-manager.ts +83 -0
- package/src/workflow-observation-schema.ts +26 -0
- package/integrations/gemini-cli/README.md +0 -57
- package/integrations/gemini-cli/setup.sh +0 -64
- package/src/__tests__/access-tracker-novelty.test.ts +0 -304
- package/src/__tests__/activity-counter.test.ts +0 -125
- package/src/__tests__/adaptive-fidelity.test.ts +0 -102
- package/src/__tests__/admission-control.test.ts +0 -206
- package/src/__tests__/anchor-generator.test.ts +0 -67
- package/src/__tests__/api-server-security.test.ts +0 -102
- package/src/__tests__/auto-consolidation.test.ts +0 -168
- package/src/__tests__/auto-recall.test.ts +0 -131
- package/src/__tests__/babel-integration.test.ts +0 -144
- package/src/__tests__/batch-dedup.test.ts +0 -65
- package/src/__tests__/belief-history.test.ts +0 -220
- package/src/__tests__/canary-eval.test.ts +0 -114
- package/src/__tests__/capture-engine.test.ts +0 -1328
- package/src/__tests__/capture-heuristic.test.ts +0 -171
- package/src/__tests__/cascade-forget.test.ts +0 -210
- package/src/__tests__/category-min-scores.test.ts +0 -116
- package/src/__tests__/cc8-cc1-essential-context.test.ts +0 -298
- package/src/__tests__/circuit-breaker.test.ts +0 -99
- package/src/__tests__/cluster-consolidation.test.ts +0 -945
- package/src/__tests__/confidence-metadata.test.ts +0 -125
- package/src/__tests__/confidence-tracker.test.ts +0 -261
- package/src/__tests__/conflict-advisor.test.ts +0 -206
- package/src/__tests__/conflict-escalation.test.ts +0 -149
- package/src/__tests__/conflict-lifecycle.test.ts +0 -83
- package/src/__tests__/conflict-output.test.ts +0 -85
- package/src/__tests__/conflict-store.test.ts +0 -418
- package/src/__tests__/connector-v1.test.ts +0 -181
- package/src/__tests__/consolidation-engine.test.ts +0 -430
- package/src/__tests__/constructive-retrieval.test.ts +0 -509
- package/src/__tests__/context-collapse-renderer.test.ts +0 -214
- package/src/__tests__/context-composer-pins.test.ts +0 -64
- package/src/__tests__/context-composer-task-selection.test.ts +0 -24
- package/src/__tests__/context-reconstructor.test.ts +0 -65
- package/src/__tests__/context-renderer.test.ts +0 -91
- package/src/__tests__/continuity-eval.test.ts +0 -275
- package/src/__tests__/continuity-schema.test.ts +0 -97
- package/src/__tests__/contracts.test.ts +0 -262
- package/src/__tests__/conversation-importer.test.ts +0 -692
- package/src/__tests__/core-recall-eval.test.ts +0 -235
- package/src/__tests__/core-summary.test.ts +0 -109
- package/src/__tests__/data-checkup.test.ts +0 -276
- package/src/__tests__/decay-exemption.test.ts +0 -129
- package/src/__tests__/dedup-actions.test.ts +0 -58
- package/src/__tests__/dedup-guard.test.ts +0 -39
- package/src/__tests__/deterministic-id.test.ts +0 -39
- package/src/__tests__/distill-lock.test.ts +0 -305
- package/src/__tests__/distill-session-argshape.test.ts +0 -79
- package/src/__tests__/doctor.test.ts +0 -319
- package/src/__tests__/dream-pipeline.test.ts +0 -780
- package/src/__tests__/drill-down.test.ts +0 -108
- package/src/__tests__/embedder.test.ts +0 -122
- package/src/__tests__/emotion-decay.test.ts +0 -46
- package/src/__tests__/emotion-detector.test.ts +0 -56
- package/src/__tests__/emotion-phase1-integration.test.ts +0 -153
- package/src/__tests__/emotion-retrieval.test.ts +0 -56
- package/src/__tests__/entity-resolver.test.ts +0 -58
- package/src/__tests__/env-config.test.ts +0 -151
- package/src/__tests__/error-signature-boost.test.ts +0 -87
- package/src/__tests__/error-signature.test.ts +0 -95
- package/src/__tests__/error-taxonomy.test.ts +0 -129
- package/src/__tests__/ethics-layer.test.ts +0 -476
- package/src/__tests__/event-segmenter.test.ts +0 -153
- package/src/__tests__/feature-flags-integration.test.ts +0 -54
- package/src/__tests__/frequency-tracker.test.ts +0 -113
- package/src/__tests__/freshness.test.ts +0 -186
- package/src/__tests__/graph-export.test.ts +0 -489
- package/src/__tests__/hotness-blend.test.ts +0 -283
- package/src/__tests__/ingest-chunking.test.ts +0 -44
- package/src/__tests__/ingest.test.ts +0 -360
- package/src/__tests__/interference-detector.test.ts +0 -203
- package/src/__tests__/kg-extractor.test.ts +0 -192
- package/src/__tests__/kg-store.test.ts +0 -300
- package/src/__tests__/llm-client-pivot.test.ts +0 -628
- package/src/__tests__/llm-consolidation.test.ts +0 -226
- package/src/__tests__/maintenance-locks.test.ts +0 -189
- package/src/__tests__/mcp-efficiency.test.ts +0 -288
- package/src/__tests__/mcp-related-scopes.test.ts +0 -151
- package/src/__tests__/memory-boundaries.test.ts +0 -282
- package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +0 -345
- package/src/__tests__/memory-evolution.test.ts +0 -271
- package/src/__tests__/memory-health-rebalance.test.ts +0 -144
- package/src/__tests__/memory-lint.test.ts +0 -421
- package/src/__tests__/memory-output.test.ts +0 -215
- package/src/__tests__/memory-promotion.test.ts +0 -315
- package/src/__tests__/migrate-dedup-ids.test.ts +0 -65
- package/src/__tests__/multi-vector.test.ts +0 -242
- package/src/__tests__/narrative-architecture.test.ts +0 -456
- package/src/__tests__/noise-filter.test.ts +0 -205
- package/src/__tests__/noise-prototype-bank.test.ts +0 -280
- package/src/__tests__/obsidian-connector.test.ts +0 -363
- package/src/__tests__/omitted-reporting.test.ts +0 -98
- package/src/__tests__/pending-review.test.ts +0 -77
- package/src/__tests__/pii-redact.test.ts +0 -333
- package/src/__tests__/pivot-apply.test.ts +0 -373
- package/src/__tests__/pivot-distill-supervisor.test.ts +0 -457
- package/src/__tests__/pivot-distill.test.ts +0 -1178
- package/src/__tests__/post-retrieval-filter.test.ts +0 -178
- package/src/__tests__/ppr-traversal.test.ts +0 -256
- package/src/__tests__/prediction-engine.test.ts +0 -349
- package/src/__tests__/predictive-prospective-memory.test.ts +0 -416
- package/src/__tests__/preference-guard-smoke.test.ts +0 -211
- package/src/__tests__/preference-matcher.test.ts +0 -282
- package/src/__tests__/preference-slots.test.ts +0 -205
- package/src/__tests__/prospective-memory.test.ts +0 -283
- package/src/__tests__/pull-from-macbook-sync-only.test.ts +0 -679
- package/src/__tests__/query-entity-detector.test.ts +0 -171
- package/src/__tests__/query-expander-alias.test.ts +0 -146
- package/src/__tests__/query-expander.test.ts +0 -34
- package/src/__tests__/recall-gate.test.ts +0 -266
- package/src/__tests__/recall-governor.test.ts +0 -227
- package/src/__tests__/recall-mode.test.ts +0 -64
- package/src/__tests__/resolve-db-path.test.ts +0 -130
- package/src/__tests__/resolve-tier.test.ts +0 -64
- package/src/__tests__/result-synthesizer.test.ts +0 -198
- package/src/__tests__/resume-context.test.ts +0 -4898
- package/src/__tests__/retrieval-eval.test.ts +0 -163
- package/src/__tests__/retrieval-trace.test.ts +0 -213
- package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
- package/src/__tests__/retriever-lazy-init-dispatch.test.ts +0 -123
- package/src/__tests__/retroactive-boost.test.ts +0 -223
- package/src/__tests__/salience-scoring.test.ts +0 -106
- package/src/__tests__/scope-inventory.test.ts +0 -200
- package/src/__tests__/scope-policy.test.ts +0 -69
- package/src/__tests__/scope-suggester.test.ts +0 -80
- package/src/__tests__/session-checkpoint.test.ts +0 -291
- package/src/__tests__/session-distiller.test.ts +0 -640
- package/src/__tests__/session-gc.test.ts +0 -215
- package/src/__tests__/skill-engine.test.ts +0 -552
- package/src/__tests__/skill-promotion.test.ts +0 -654
- package/src/__tests__/skill-verifier.test.ts +0 -122
- package/src/__tests__/source-heartbeat.test.ts +0 -250
- package/src/__tests__/stable-context-length-contract.test.ts +0 -62
- package/src/__tests__/store-patch-metadata.test.ts +0 -205
- package/src/__tests__/store-read-consistency.test.ts +0 -122
- package/src/__tests__/store-schema-migration.test.ts +0 -87
- package/src/__tests__/store-scope-audit.test.ts +0 -81
- package/src/__tests__/store-upsert.test.ts +0 -201
- package/src/__tests__/supersede-chain.test.ts +0 -154
- package/src/__tests__/temporal-parser.test.ts +0 -241
- package/src/__tests__/temporal-validity.test.ts +0 -160
- package/src/__tests__/term-registry-terminal-roster.test.ts +0 -127
- package/src/__tests__/term-registry.test.ts +0 -121
- package/src/__tests__/tool-error-surfacing.test.ts +0 -98
- package/src/__tests__/tool-output-compressor.test.ts +0 -160
- package/src/__tests__/topic-tag.test.ts +0 -113
- package/src/__tests__/trusted-memory.test.ts +0 -839
- package/src/__tests__/ui-server-security.test.ts +0 -93
- package/src/__tests__/usage-tracker.test.ts +0 -287
- package/src/__tests__/version-manager.test.ts +0 -213
- package/src/__tests__/workflow-observation-skill-outcome.test.ts +0 -352
- package/src/__tests__/workflow-observation.test.ts +0 -433
- package/src/__tests__/write-verifier.test.ts +0 -122
- /package/integrations/{gemini-cli/gemini-md-snippet.md → agy/agy-md-snippet.md} +0 -0
package/src/dream-pipeline.ts
CHANGED
|
@@ -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:
|
|
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
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* scope
|
|
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:
|
|
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
|
-
|
|
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;
|
package/src/forget-engine.ts
CHANGED
|
@@ -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
|
-
|
|
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, {
|