recallnest 2.3.0 → 2.5.4
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 +150 -0
- package/README.md +17 -31
- package/README_CN.md +12 -22
- 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 +22 -1
- package/src/__tests__/access-tracker-novelty.test.ts +238 -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__/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__/canary-eval.test.ts +114 -0
- package/src/__tests__/capture-engine.test.ts +1238 -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 +33 -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 +782 -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 +374 -0
- package/src/__tests__/connector-v1.test.ts +181 -0
- package/src/__tests__/consolidation-engine.test.ts +333 -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 +216 -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 +84 -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 +321 -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__/graph-export.test.ts +489 -0
- package/src/__tests__/hotness-blend.test.ts +76 -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-consolidation.test.ts +226 -0
- package/src/__tests__/maintenance-locks.test.ts +174 -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 +250 -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__/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 +314 -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 +230 -0
- package/src/__tests__/preference-slots.test.ts +205 -0
- package/src/__tests__/prospective-memory.test.ts +283 -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-governor.test.ts +227 -0
- package/src/__tests__/recall-mode.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-trace.test.ts +213 -0
- package/src/__tests__/retriever-boundary-weight.test.ts +136 -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 +601 -0
- package/src/__tests__/skill-verifier.test.ts +122 -0
- package/src/__tests__/source-heartbeat.test.ts +250 -0
- package/src/__tests__/store-patch-metadata.test.ts +205 -0
- package/src/__tests__/store-scope-audit.test.ts +81 -0
- package/src/__tests__/store-upsert.test.ts +184 -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.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__/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 +38 -32
- package/src/activity-counter.ts +46 -18
- package/src/aliases.ts +133 -0
- package/src/api-server.ts +27 -7
- package/src/audit-log.ts +4 -2
- package/src/auto-consolidation.ts +8 -3
- package/src/auto-gc.ts +85 -21
- package/src/capture-engine.ts +54 -21
- package/src/cli.ts +169 -37
- package/src/confidence-tracker.ts +36 -18
- package/src/consolidation-engine.ts +109 -11
- package/src/context-composer.ts +8 -1
- package/src/data-checkup.ts +95 -6
- package/src/distill-lock.ts +194 -15
- package/src/doctor.ts +9 -2
- package/src/dream-pipeline.ts +118 -34
- package/src/env-config.ts +77 -0
- package/src/error-signature.ts +153 -0
- package/src/error-taxonomy.ts +115 -0
- package/src/eval.ts +251 -6
- package/src/idempotency.ts +8 -0
- package/src/ingest.ts +31 -10
- package/src/kg-extractor.ts +6 -2
- package/src/kg-store.ts +236 -44
- package/src/llm-consolidation.ts +2 -1
- package/src/mcp-server.ts +85 -1799
- package/src/mcp-tool-deps.ts +48 -0
- package/src/mcp-tools-advanced.ts +941 -0
- package/src/mcp-tools-core.ts +633 -0
- package/src/mcp-tools-governance.ts +475 -0
- package/src/memory-boundaries.ts +3 -1
- package/src/memory-evolution.ts +73 -1
- package/src/memory-lint.ts +183 -10
- package/src/memory-output.ts +114 -3
- package/src/memory-promotion.ts +246 -0
- package/src/memory-schema.ts +25 -2
- package/src/memory-store-port.ts +43 -0
- package/src/multi-vector.ts +2 -1
- package/src/narrative-schema.ts +2 -1
- package/src/noise-filter.ts +57 -6
- package/src/pii-detector.ts +157 -5
- package/src/query-expander.ts +90 -9
- package/src/result-synthesizer.ts +2 -1
- package/src/retention-policy.ts +2 -1
- package/src/retriever.ts +259 -139
- package/src/runtime-config.ts +8 -1
- 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 +247 -47
- package/src/skill-schema.ts +16 -3
- package/src/skill-verifier.ts +152 -0
- package/src/store.ts +267 -38
- package/src/tool-output-compressor.ts +3 -1
- package/src/ui-server.ts +2 -1
- package/src/usage-tracker.ts +242 -0
- package/src/version-manager.ts +3 -2
- 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/retriever.ts
CHANGED
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
import type { MemoryStore, MemorySearchResult } from "./store.js";
|
|
7
7
|
import type { Embedder } from "./embedder.js";
|
|
8
8
|
import { filterNoise } from "./noise-filter.js";
|
|
9
|
+
import * as envConfig from "./env-config.js";
|
|
10
|
+
import { extractErrorSignatures } from "./error-signature.js";
|
|
9
11
|
import { shouldSkipRetrieval } from "./adaptive-retrieval.js";
|
|
12
|
+
import { expandQueryWithAliases } from "./aliases.js";
|
|
10
13
|
import { expandQuery } from "./query-expander.js";
|
|
11
14
|
import { detectLang, tokenizeFts } from "./language-hook.js";
|
|
12
15
|
import { type AccessTracker, computeHotnessScore, parseAccessMetadata } from "./access-tracker.js";
|
|
@@ -30,6 +33,7 @@ import {
|
|
|
30
33
|
tokenize,
|
|
31
34
|
} from "./multi-vector.js";
|
|
32
35
|
import { parseTemporalQuery, matchesTemporalConstraint, type TemporalConstraint } from "./temporal-parser.js";
|
|
36
|
+
import { parseSourceIds } from "./kg-store.js";
|
|
33
37
|
import type { KGStore } from "./kg-store.js";
|
|
34
38
|
import { isKGModeEnabled } from "./kg-extractor.js";
|
|
35
39
|
import { detectEntities } from "./query-entity-detector.js";
|
|
@@ -41,6 +45,7 @@ import { parseNarrative, isNarrativeModeEnabled } from "./narrative-schema.js";
|
|
|
41
45
|
import type { EmotionMetadata } from "./memory-schema.js";
|
|
42
46
|
import { detectEmotion } from "./emotion-detector.js";
|
|
43
47
|
import { shouldReconstruct, reconstruct as runReconstruction } from "./context-reconstructor.js";
|
|
48
|
+
import { recordReconstructionUsage as runUsageShadow } from "./usage-tracker.js";
|
|
44
49
|
import type {
|
|
45
50
|
ReconstructionLLMClient,
|
|
46
51
|
ReconstructionOutput,
|
|
@@ -189,6 +194,12 @@ export interface RetrievalResult extends MemorySearchResult {
|
|
|
189
194
|
*/
|
|
190
195
|
export type RetrievalResultSet = RetrievalResult[] & {
|
|
191
196
|
reconstruction?: ReconstructionOutput;
|
|
197
|
+
/**
|
|
198
|
+
* P-omitted: 因 limit 上限被截断、未返回的达标结果数(提高 limit 可见)。
|
|
199
|
+
* 仅统计 finalizeResults 的 limit 截断,不含质量过滤(min-score/noise/MMR)丢弃——
|
|
200
|
+
* 那些提高 limit 也拿不到,报告出来反而误导调用方。
|
|
201
|
+
*/
|
|
202
|
+
omitted?: { count: number; reason: "limit" };
|
|
192
203
|
};
|
|
193
204
|
|
|
194
205
|
// ============================================================================
|
|
@@ -209,13 +220,21 @@ export const DEFAULT_CATEGORY_MIN_SCORES: Record<string, number> = {
|
|
|
209
220
|
patterns: 0.45,
|
|
210
221
|
};
|
|
211
222
|
|
|
223
|
+
// P1.2(2026-06-25):默认 vector-only。canary eval 实测个人记忆库 vector 召回质量全面优于
|
|
224
|
+
// hybrid(robustness 带 scope WITH 6/12 + 不带 scope WITHOUT 10/12,完胜 hybrid 的 3/9;BM25 在
|
|
225
|
+
// 高度同质的个人记忆里是净噪声,会把 vector 本应 top 的目标如 dbcb317a 挤掉)。改频率层救低频
|
|
226
|
+
// 目标实测是死结(动 hotness 必伤靠 hotness 上位的记忆)。hybrid 路径代码全保留,mode 改回
|
|
227
|
+
// "hybrid" 即可切换(注:multiHopExpand 仍硬编码走 hybrid,默认 off,config.retrieval.multiHop
|
|
228
|
+
// 开了会破纯 vector)。数据盲区:canary 全是语义 query,未覆盖 BM25 强项的精确关键词/token 召回
|
|
229
|
+
// (变量名 / commit hash / alias-map 昵称)——待补 exact-token canary 验证。
|
|
212
230
|
export const DEFAULT_RETRIEVAL_CONFIG: RetrievalConfig = {
|
|
213
|
-
mode: "
|
|
231
|
+
mode: "vector",
|
|
214
232
|
vectorWeight: 0.7,
|
|
215
233
|
bm25Weight: 0.3,
|
|
216
234
|
minScore: 0.3,
|
|
217
235
|
rerank: "cross-encoder",
|
|
218
|
-
candidatePoolSize:
|
|
236
|
+
candidatePoolSize: 30,
|
|
237
|
+
sourceDiversity: 1,
|
|
219
238
|
recencyHalfLifeDays: 14,
|
|
220
239
|
recencyWeight: 0.10,
|
|
221
240
|
filterNoise: true,
|
|
@@ -227,6 +246,30 @@ export const DEFAULT_RETRIEVAL_CONFIG: RetrievalConfig = {
|
|
|
227
246
|
hotnessWeight: 0,
|
|
228
247
|
};
|
|
229
248
|
|
|
249
|
+
/**
|
|
250
|
+
* P1.2+: 检测 query 是否含「精确 token」——大写常量 / commit hash / 文件名 / snake/camelCase /
|
|
251
|
+
* 低元音 ID(如 rwmjhb)。这类 token vectorOnly 抓不到(embedding 做语义相似非精确匹配,跟语言
|
|
252
|
+
* 无关:实测纯/中文/中英混 query 都漏靠 token 区分的目标),需 BM25 兜底。刻意避开元音正常的普通词
|
|
253
|
+
* (mem0/recallnest/GitHub),避免日常语义 query 误触发 BM25 噪声。
|
|
254
|
+
*/
|
|
255
|
+
export function hasExactToken(query: string): boolean {
|
|
256
|
+
for (const t of query.split(/[\s,。、;:()()【】「」"'`??!!]+/)) {
|
|
257
|
+
if (t.length < 5) continue;
|
|
258
|
+
// 全大写 + 下划线常量(必须含下划线,避免 JSON/HTTP/GitHub 等普通缩写/驼峰误触发)
|
|
259
|
+
if (/^[A-Z][A-Z0-9]*_[A-Z0-9_]+$/.test(t)) return true; // DEDUP_HARD_THRESHOLD
|
|
260
|
+
// snake_case 标识符(含下划线)
|
|
261
|
+
if (/^[a-z][a-z0-9]*_[a-z0-9_]+$/i.test(t)) return true; // capture_engine
|
|
262
|
+
// 文件名(基名 + 代码扩展名)
|
|
263
|
+
if (/^[\w./-]+\.(ts|tsx|js|jsx|py|md|sh|json|sql|go|rs|java|cpp|yml|yaml|toml)$/i.test(t)) return true; // capture-heuristic.ts
|
|
264
|
+
// hex commit hash(7-40,含数字+字母,纯 hex)
|
|
265
|
+
if (/^[0-9a-f]{7,40}$/i.test(t) && /[0-9]/.test(t) && /[a-f]/i.test(t)) return true; // dd12fb5
|
|
266
|
+
// 注:camelCase(GitHub/TypeScript)与低元音 ID(rwmjhb/pytest/nginx)两条宽规则已移除——
|
|
267
|
+
// 前者误伤日常技术 query 引回 BM25 噪声;rwmjhb 类纯小写 ID 即便触发,fts 中英混分词也召回不到
|
|
268
|
+
// (根因在 tokenizer,是独立 backlog),靠中英一起 query 的中文锚点已能补回。
|
|
269
|
+
}
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
|
|
230
273
|
// ============================================================================
|
|
231
274
|
// Utility Functions
|
|
232
275
|
// ============================================================================
|
|
@@ -314,6 +357,32 @@ function applyAnchorBoost(results: RetrievalResult[], query: string): RetrievalR
|
|
|
314
357
|
});
|
|
315
358
|
}
|
|
316
359
|
|
|
360
|
+
/** Multiplicative boost applied to a case on exact error-signature match. */
|
|
361
|
+
const ERROR_SIGNATURE_BOOST_FACTOR = 1.5;
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* A1 (P3-A): error-signature exact-match boost. When RECALLNEST_ERROR_SIGNATURE_BOOST
|
|
365
|
+
* is on, case memories whose metadata.error_signature overlaps the query's extracted
|
|
366
|
+
* signatures get a strong multiplicative boost ("上次一模一样的报错"精确召回). Flag off
|
|
367
|
+
* (default) returns the input unchanged — bit-identical, so the pure-move eval baseline holds.
|
|
368
|
+
* Only affects category="cases"; read and query side share extractErrorSignatures so the
|
|
369
|
+
* signatures compare apples-to-apples.
|
|
370
|
+
*/
|
|
371
|
+
export function applyErrorSignatureBoost(results: RetrievalResult[], query: string): RetrievalResult[] {
|
|
372
|
+
if (!envConfig.errorSignatureBoost()) return results;
|
|
373
|
+
const querySignatures = extractErrorSignatures({ problem: query });
|
|
374
|
+
if (querySignatures.length === 0) return results;
|
|
375
|
+
const querySet = new Set(querySignatures.map(s => s.toLowerCase()));
|
|
376
|
+
return results.map(r => {
|
|
377
|
+
if (r.entry.category !== "cases") return r;
|
|
378
|
+
const sigs = parseMetadata(r.entry.metadata).error_signature;
|
|
379
|
+
if (!Array.isArray(sigs) || sigs.length === 0) return r;
|
|
380
|
+
const hit = sigs.some(s => typeof s === "string" && querySet.has(s.toLowerCase()));
|
|
381
|
+
if (!hit) return r;
|
|
382
|
+
return { ...r, score: Math.min(1, Math.max(0, r.score * ERROR_SIGNATURE_BOOST_FACTOR)) };
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
317
386
|
/**
|
|
318
387
|
* Emotion scoring stage: boost memories matching query emotional tone.
|
|
319
388
|
* No-op when query is neutral or memory lacks emotion data.
|
|
@@ -668,6 +737,16 @@ export class MemoryRetriever {
|
|
|
668
737
|
this.llmClient = client;
|
|
669
738
|
}
|
|
670
739
|
|
|
740
|
+
/**
|
|
741
|
+
* Shadow-record which memories a reconstruction actually cited via [src:ID].
|
|
742
|
+
* 影子统计:写 metadata.usage(injection-vs-use 双计数),第一阶段不参与 ranking/forget。
|
|
743
|
+
* best-effort:不传 scope(影子写入不做权限过滤,避免跨 scope 扩展候选触发越权 throw);
|
|
744
|
+
* 内部 allSettled 不抛错,绝不阻断主检索/resume 流程。
|
|
745
|
+
*/
|
|
746
|
+
async recordReconstructionUsage(citedIds: string[]): Promise<void> {
|
|
747
|
+
await runUsageShadow(this.store, citedIds);
|
|
748
|
+
}
|
|
749
|
+
|
|
671
750
|
/** Suppress a memory ID for the current session (score × 0.3). */
|
|
672
751
|
suppressForSession(id: string): void {
|
|
673
752
|
this.sessionSuppressed.add(id);
|
|
@@ -687,14 +766,18 @@ export class MemoryRetriever {
|
|
|
687
766
|
}
|
|
688
767
|
|
|
689
768
|
async retrieve(context: RetrievalContext): Promise<RetrievalResultSet> {
|
|
690
|
-
const { query, limit, scopeFilter, category, includeArchived, trace, graph } = context;
|
|
769
|
+
const { query: rawQuery, limit, scopeFilter, category, includeArchived, trace, graph } = context;
|
|
691
770
|
const safeLimit = clampInt(limit, 1, 20);
|
|
692
771
|
|
|
693
772
|
// Adaptive retrieval: skip trivial queries to save embedding API calls
|
|
694
|
-
if (shouldSkipRetrieval(
|
|
773
|
+
if (shouldSkipRetrieval(rawQuery)) {
|
|
695
774
|
return [];
|
|
696
775
|
}
|
|
697
776
|
|
|
777
|
+
// Step 2: alias expansion — append canonical entity tokens for matching colloquial phrases.
|
|
778
|
+
// 不改原 query,仅追加,保留 vector embedding 原意;BM25 拿到更多锚点。
|
|
779
|
+
const query = expandQueryWithAliases(rawQuery);
|
|
780
|
+
|
|
698
781
|
let results: RetrievalResult[];
|
|
699
782
|
|
|
700
783
|
// For vector-only mode, use legacy behavior
|
|
@@ -705,6 +788,28 @@ export class MemoryRetriever {
|
|
|
705
788
|
results = await this.hybridRetrieval(query, safeLimit, scopeFilter, category, includeArchived, trace, graph);
|
|
706
789
|
}
|
|
707
790
|
|
|
791
|
+
// 0-hit fallback: minScore filter 把所有候选过滤光时,放宽阈值(=0)重试一次。
|
|
792
|
+
// 只在原 path 0 hit 触发;正常召回不受影响。
|
|
793
|
+
if (results.length === 0) {
|
|
794
|
+
if (this.config.mode === "vector" || !this.store.hasFtsSupport) {
|
|
795
|
+
results = await this.vectorOnlyRetrieval(query, safeLimit, scopeFilter, category, includeArchived, trace, 0);
|
|
796
|
+
} else {
|
|
797
|
+
results = await this.hybridRetrieval(query, safeLimit, scopeFilter, category, includeArchived, trace, graph, 0);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// P-omitted: 快照 finalize 阶段因 limit 截断的达标结果数(提高 limit 可见)。
|
|
802
|
+
// 必须在后续 validity/topicTag filter(以及 BM25 补偿等换数组操作)丢失 ResultSet 属性之前读取。
|
|
803
|
+
let omittedByLimit = (results as RetrievalResultSet).omitted?.count ?? 0;
|
|
804
|
+
|
|
805
|
+
// P1.2+: exact-token BM25 补偿——query 含精确 token(大写常量/commit hash/.ts 文件名/camelCase/
|
|
806
|
+
// 低元音 ID 如 rwmjhb)时,vector 漏掉的精确命中用 BM25 补回(vectorOnly 做语义相似、不做精确
|
|
807
|
+
// 字符串匹配,是 embedding 本质,跟语言无关)。普通词(mem0/recallnest/GitHub)不触发,日常
|
|
808
|
+
// 语义召回不受影响。
|
|
809
|
+
if (hasExactToken(rawQuery)) {
|
|
810
|
+
results = await this.supplementExactTokenWithBm25(query, results, safeLimit, scopeFilter, category, includeArchived);
|
|
811
|
+
}
|
|
812
|
+
|
|
708
813
|
// LME-2: Multi-hop retrieval — extract entities from first-pass results,
|
|
709
814
|
// run focused follow-up queries, merge to improve cross-session coverage.
|
|
710
815
|
const useMultiHop = context.multiHop ?? this.config.multiHop ?? false;
|
|
@@ -773,7 +878,11 @@ export class MemoryRetriever {
|
|
|
773
878
|
|
|
774
879
|
// Phase 4: Constructive retrieval — first-class reconstruction (no metadata hack)
|
|
775
880
|
const resultSet = results as RetrievalResultSet;
|
|
776
|
-
|
|
881
|
+
// P-omitted: 把快照的 limit 截断数挂回最终 resultSet(validity filter 已丢原数组属性)。
|
|
882
|
+
if (omittedByLimit > 0) {
|
|
883
|
+
resultSet.omitted = { count: omittedByLimit, reason: "limit" };
|
|
884
|
+
}
|
|
885
|
+
const constructiveFlag = envConfig.constructiveRetrieval();
|
|
777
886
|
if (shouldReconstruct({
|
|
778
887
|
flagEnabled: constructiveFlag,
|
|
779
888
|
callerOptIn: context.reconstruct === true,
|
|
@@ -790,6 +899,8 @@ export class MemoryRetriever {
|
|
|
790
899
|
expansionDeps,
|
|
791
900
|
);
|
|
792
901
|
resultSet.reconstruction = reconstruction;
|
|
902
|
+
// Shadow usage:记录被 reconstruction 真正引用的记忆(use 信号),不动 ranking/forget。
|
|
903
|
+
await this.recordReconstructionUsage(reconstruction.sources.map(s => s.id));
|
|
793
904
|
} catch {
|
|
794
905
|
// Silent degradation — return raw results
|
|
795
906
|
}
|
|
@@ -798,6 +909,36 @@ export class MemoryRetriever {
|
|
|
798
909
|
return resultSet;
|
|
799
910
|
}
|
|
800
911
|
|
|
912
|
+
/**
|
|
913
|
+
* P1.2+: exact-token BM25 补偿。vectorOnly 主路径后,若 query 含精确 token,用 BM25 精确匹配把
|
|
914
|
+
* vector 漏掉的命中补回。hasFtsSupport 在 vectorOnly 触发 store init 后为真;fake store 为假时跳过。
|
|
915
|
+
* bm25 精确命中提到结果前(exact-token 意图=精确优先),vector 语义结果跟后,去重后 slice limit。
|
|
916
|
+
*/
|
|
917
|
+
private async supplementExactTokenWithBm25(
|
|
918
|
+
query: string,
|
|
919
|
+
vectorResults: RetrievalResult[],
|
|
920
|
+
limit: number,
|
|
921
|
+
scopeFilter?: string[],
|
|
922
|
+
category?: string,
|
|
923
|
+
includeArchived?: boolean,
|
|
924
|
+
): Promise<RetrievalResult[]> {
|
|
925
|
+
if (!this.store.hasFtsSupport) return vectorResults;
|
|
926
|
+
const ftsQuery = tokenizeFts(expandQuery(query), detectLang(query));
|
|
927
|
+
const bm25 = await this.runBM25Search(ftsQuery, Math.max(limit, 5), scopeFilter, category, includeArchived);
|
|
928
|
+
const vecIds = new Set(vectorResults.map(r => r.entry.id));
|
|
929
|
+
const supplements = bm25
|
|
930
|
+
.filter(b => !vecIds.has(b.entry.id))
|
|
931
|
+
.filter(b => isActiveMemory(b.entry.metadata)) // 排掉 superseded/archived/consolidated(补偿绕过了 finalize 的尾部过滤,这里补回 active 校验)
|
|
932
|
+
.slice(0, 3)
|
|
933
|
+
.map(b => ({
|
|
934
|
+
entry: b.entry,
|
|
935
|
+
score: b.score,
|
|
936
|
+
sources: { bm25: { score: b.score, rank: b.rank } },
|
|
937
|
+
} as RetrievalResult));
|
|
938
|
+
if (supplements.length === 0) return vectorResults;
|
|
939
|
+
return [...supplements, ...vectorResults].slice(0, limit);
|
|
940
|
+
}
|
|
941
|
+
|
|
801
942
|
/**
|
|
802
943
|
* LME-2: Multi-hop expansion — extract entities from first-pass results,
|
|
803
944
|
* run entity-focused follow-up queries, merge with original results.
|
|
@@ -860,6 +1001,7 @@ export class MemoryRetriever {
|
|
|
860
1001
|
category?: string,
|
|
861
1002
|
includeArchived?: boolean,
|
|
862
1003
|
trace?: TraceCollector,
|
|
1004
|
+
minScoreOverride?: number,
|
|
863
1005
|
): Promise<RetrievalResult[]> {
|
|
864
1006
|
// Temporal reasoning for vector-only mode
|
|
865
1007
|
const temporal = parseTemporalQuery(query);
|
|
@@ -867,12 +1009,20 @@ export class MemoryRetriever {
|
|
|
867
1009
|
|
|
868
1010
|
trace?.startStage("vector_search", 0);
|
|
869
1011
|
const queryVector = await this.embedder.embedQuery(searchQuery);
|
|
870
|
-
// P0.1: Lower minScore for short queries
|
|
1012
|
+
// P0.1: Lower minScore for short queries; minScoreOverride 用于 0-hit fallback retry
|
|
871
1013
|
const shortQ = isShortQuery(searchQuery);
|
|
872
|
-
const vectorMinScore =
|
|
873
|
-
?
|
|
874
|
-
:
|
|
875
|
-
|
|
1014
|
+
const vectorMinScore = minScoreOverride !== undefined
|
|
1015
|
+
? minScoreOverride
|
|
1016
|
+
: (shortQ
|
|
1017
|
+
? this.config.minScore * SHORT_QUERY_MIN_SCORE_FACTOR
|
|
1018
|
+
: this.config.minScore);
|
|
1019
|
+
// P-omitted: over-fetch a wider candidate pool so finalizeResults can detect limit-capped
|
|
1020
|
+
// omissions. Previously fetched exactly `limit`, so vector-only omitted was always 0
|
|
1021
|
+
// (hybrid already over-fetches candidatePoolSize). Cost is minimal — ANN with a larger K
|
|
1022
|
+
// plus a few more local scoring passes, no extra API; P1.2's vector-only savings come from
|
|
1023
|
+
// skipping hybrid BM25/fusion/rerank, not from fetch size.
|
|
1024
|
+
const fetchLimit = Math.max(limit, this.config.candidatePoolSize ?? limit);
|
|
1025
|
+
const results = await this.store.vectorSearch(queryVector, fetchLimit, vectorMinScore, scopeFilter);
|
|
876
1026
|
trace?.endStage(results.length, results.map(r => r.score));
|
|
877
1027
|
|
|
878
1028
|
// Filter by category if specified
|
|
@@ -900,103 +1050,18 @@ export class MemoryRetriever {
|
|
|
900
1050
|
// P0.1: Anchor boost for vector-only path
|
|
901
1051
|
const anchorBoosted = applyAnchorBoost(mapped, searchQuery);
|
|
902
1052
|
|
|
903
|
-
|
|
904
|
-
const boosted = this.applyRecencyBoost(anchorBoosted);
|
|
905
|
-
trace?.endStage(boosted.length, boosted.map(r => r.score));
|
|
1053
|
+
const scored = this.applySharedScoring(anchorBoosted, query, trace);
|
|
906
1054
|
|
|
907
|
-
|
|
908
|
-
const
|
|
909
|
-
trace?.endStage(weighted.length, weighted.map(r => r.score));
|
|
910
|
-
|
|
911
|
-
trace?.startStage("confidence_weight", weighted.length);
|
|
912
|
-
const confidenceWeighted = this.applyConfidence(weighted);
|
|
913
|
-
trace?.endStage(confidenceWeighted.length, confidenceWeighted.map(r => r.score));
|
|
914
|
-
|
|
915
|
-
let afterEmotionVector = confidenceWeighted;
|
|
916
|
-
if (isEmotionScoringEnabled()) {
|
|
917
|
-
const queryEmotion = detectEmotion(query);
|
|
918
|
-
const adapted = confidenceWeighted.map(r => ({ ...r, metadata: r.entry.metadata ?? "{}" }));
|
|
919
|
-
const emotioned = applyEmotionWeight(adapted, queryEmotion);
|
|
920
|
-
afterEmotionVector = confidenceWeighted.map((r, i) => ({ ...r, score: emotioned[i]?.score ?? r.score }));
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
trace?.startStage("boundary_weight", afterEmotionVector.length);
|
|
924
|
-
const boundaryWeighted = this.applyBoundaryWeight(afterEmotionVector);
|
|
925
|
-
trace?.endStage(boundaryWeighted.length, boundaryWeighted.map(r => r.score));
|
|
1055
|
+
// Session suppression: temporarily penalize "do not disturb" memories (vector-only path)
|
|
1056
|
+
const afterSuppression = this.applySessionSuppression(scored);
|
|
926
1057
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
trace?.startStage("time_decay", lengthNormalized.length);
|
|
936
|
-
const timeDecayed = this.applyTimeDecay(lengthNormalized);
|
|
937
|
-
trace?.endStage(timeDecayed.length, timeDecayed.map(r => r.score));
|
|
938
|
-
|
|
939
|
-
// B-1/E-1: Evolution decay blend — boost memories with high composite decay score
|
|
940
|
-
const evolutionBlended = this.applyEvolutionDecayBlend(timeDecayed);
|
|
941
|
-
|
|
942
|
-
// E-1: Access count boost — memories retrieved more often get a score nudge
|
|
943
|
-
const accessBoosted = this.applyAccessCountBoost(evolutionBlended);
|
|
944
|
-
|
|
945
|
-
// Hotness blend: boost frequently + recently accessed memories
|
|
946
|
-
trace?.startStage("hotness_blend", accessBoosted.length);
|
|
947
|
-
const hotnessBlended = this.applyHotnessBlend(accessBoosted);
|
|
948
|
-
trace?.endStage(hotnessBlended.length, hotnessBlended.map(r => r.score));
|
|
949
|
-
|
|
950
|
-
// P0.2: Frequency boost — repeatedly retrieved memories score higher
|
|
951
|
-
trace?.startStage("frequency_boost", hotnessBlended.length);
|
|
952
|
-
const frequencyBoosted = this.applyFrequencyBoost(hotnessBlended);
|
|
953
|
-
trace?.endStage(frequencyBoosted.length, frequencyBoosted.map(r => r.score));
|
|
954
|
-
|
|
955
|
-
// Session suppression: temporarily penalize "do not disturb" memories
|
|
956
|
-
const afterSuppression = this.applySessionSuppression(frequencyBoosted);
|
|
957
|
-
const rescored = [...afterSuppression].sort((a, b) => b.score - a.score);
|
|
958
|
-
|
|
959
|
-
trace?.startStage("hard_min_score", rescored.length);
|
|
960
|
-
const hardFiltered = rescored.filter(r => r.score >= this.minScoreFor(r.entry.category, shortQ));
|
|
961
|
-
trace?.endStage(hardFiltered.length, hardFiltered.map(r => r.score));
|
|
962
|
-
|
|
963
|
-
// Evolution status filter: exclude superseded/archived/consolidated memories
|
|
964
|
-
const evolutionFiltered = includeArchived
|
|
965
|
-
? hardFiltered
|
|
966
|
-
: hardFiltered.filter(r => isActiveMemory(r.entry.metadata));
|
|
967
|
-
|
|
968
|
-
trace?.startStage("noise_filter", evolutionFiltered.length);
|
|
969
|
-
const denoised = this.config.filterNoise
|
|
970
|
-
? filterNoise(evolutionFiltered, r => r.entry.text)
|
|
971
|
-
: evolutionFiltered;
|
|
972
|
-
trace?.endStage(denoised.length, denoised.map(r => r.score));
|
|
973
|
-
|
|
974
|
-
// RIF: demote near-duplicate weak results to improve diversity
|
|
975
|
-
const afterRif = this.config.enableRIF
|
|
976
|
-
? filterInterference(denoised, this.config.rifThreshold ?? 0.85, this.config.rifScoreRatio ?? 0.80)
|
|
977
|
-
: denoised;
|
|
978
|
-
|
|
979
|
-
// MMR deduplication: avoid top-k filled with near-identical memories
|
|
980
|
-
trace?.startStage("mmr_diversity", afterRif.length);
|
|
981
|
-
const deduplicated = this.applyMMRDiversity(afterRif);
|
|
982
|
-
trace?.endStage(deduplicated.length, deduplicated.map(r => r.score));
|
|
983
|
-
|
|
984
|
-
// Tier 3.3: Version group dedup — keep only the top-ranked version per group
|
|
985
|
-
const versionDeduped = deduplicateByVersionGroup(deduplicated);
|
|
986
|
-
|
|
987
|
-
// LC-P2: Cluster insight dedup — prefer cluster summary over individual source memories
|
|
988
|
-
const clusterDeduped = deduplicateByClusterInsight(versionDeduped);
|
|
989
|
-
|
|
990
|
-
// Source diversity: round-robin across scopes for multi-session coverage
|
|
991
|
-
const diversified = (this.config.sourceDiversity ?? 0) > 0
|
|
992
|
-
? applySourceDiversity(clusterDeduped, limit)
|
|
993
|
-
: clusterDeduped;
|
|
994
|
-
|
|
995
|
-
trace?.startStage("final_limit", diversified.length);
|
|
996
|
-
const final = diversified.slice(0, limit);
|
|
997
|
-
trace?.endStage(final.length, final.map(r => r.score));
|
|
998
|
-
|
|
999
|
-
return final;
|
|
1058
|
+
return this.finalizeResults(afterSuppression, {
|
|
1059
|
+
shortQ,
|
|
1060
|
+
minScoreOverride,
|
|
1061
|
+
includeArchived,
|
|
1062
|
+
limit,
|
|
1063
|
+
trace,
|
|
1064
|
+
});
|
|
1000
1065
|
}
|
|
1001
1066
|
|
|
1002
1067
|
private async hybridRetrieval(
|
|
@@ -1007,6 +1072,7 @@ export class MemoryRetriever {
|
|
|
1007
1072
|
includeArchived?: boolean,
|
|
1008
1073
|
trace?: TraceCollector,
|
|
1009
1074
|
graph?: boolean,
|
|
1075
|
+
minScoreOverride?: number,
|
|
1010
1076
|
): Promise<RetrievalResult[]> {
|
|
1011
1077
|
// Adaptive pool: widen candidate pool for aggregation queries ("how many", "all the")
|
|
1012
1078
|
const multiplier = (this.config.adaptivePoolMultiplier ?? 1) > 1 && isAggregationQuery(query)
|
|
@@ -1070,10 +1136,13 @@ export class MemoryRetriever {
|
|
|
1070
1136
|
|
|
1071
1137
|
// Apply minimum score threshold
|
|
1072
1138
|
trace?.startStage("min_score_filter", anchorBoosted.length);
|
|
1073
|
-
// P0.1: Lower minScore for short queries to widen the candidate pool
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1139
|
+
// P0.1: Lower minScore for short queries to widen the candidate pool;
|
|
1140
|
+
// minScoreOverride 用于 retrieve() 顶层的 0-hit fallback retry。
|
|
1141
|
+
const effectiveMinScore = minScoreOverride !== undefined
|
|
1142
|
+
? minScoreOverride
|
|
1143
|
+
: (isShortQuery(searchQuery)
|
|
1144
|
+
? this.config.minScore * SHORT_QUERY_MIN_SCORE_FACTOR
|
|
1145
|
+
: this.config.minScore);
|
|
1077
1146
|
const filtered = anchorBoosted.filter(r => r.score >= effectiveMinScore);
|
|
1078
1147
|
trace?.endStage(filtered.length, filtered.map(r => r.score));
|
|
1079
1148
|
|
|
@@ -1084,46 +1153,61 @@ export class MemoryRetriever {
|
|
|
1084
1153
|
: filtered;
|
|
1085
1154
|
trace?.endStage(reranked.length, reranked.map(r => r.score));
|
|
1086
1155
|
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1156
|
+
const scored = this.applySharedScoring(reranked, query, trace);
|
|
1157
|
+
|
|
1158
|
+
return this.finalizeResults(scored, {
|
|
1159
|
+
shortQ,
|
|
1160
|
+
minScoreOverride,
|
|
1161
|
+
includeArchived,
|
|
1162
|
+
limit,
|
|
1163
|
+
trace,
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
/**
|
|
1168
|
+
* Shared scoring pipeline — the 12 stages identical across the vector-only and
|
|
1169
|
+
* hybrid paths. Order is load-bearing (Codex §3.6 "顺序即语义"); do not reorder.
|
|
1170
|
+
* `query` feeds the emotion stage; `trace` carries per-stage trace spans.
|
|
1171
|
+
* Path-specific stages stay in the caller: anchor / min_score / rerank / multi-vector
|
|
1172
|
+
* blend run BEFORE this; vector-only session suppression runs AFTER this.
|
|
1173
|
+
*/
|
|
1174
|
+
private applySharedScoring(
|
|
1175
|
+
results: RetrievalResult[],
|
|
1176
|
+
query: string,
|
|
1177
|
+
trace?: TraceCollector,
|
|
1178
|
+
): RetrievalResult[] {
|
|
1179
|
+
trace?.startStage("recency_boost", results.length);
|
|
1180
|
+
const boosted = this.applyRecencyBoost(results);
|
|
1181
|
+
trace?.endStage(boosted.length, boosted.map(r => r.score));
|
|
1091
1182
|
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
trace?.endStage(importanceWeighted.length, importanceWeighted.map(r => r.score));
|
|
1183
|
+
trace?.startStage("importance_weight", boosted.length);
|
|
1184
|
+
const weighted = this.applyImportanceWeight(boosted);
|
|
1185
|
+
trace?.endStage(weighted.length, weighted.map(r => r.score));
|
|
1096
1186
|
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
const confidenceWeighted = this.applyConfidence(importanceWeighted);
|
|
1187
|
+
trace?.startStage("confidence_weight", weighted.length);
|
|
1188
|
+
const confidenceWeighted = this.applyConfidence(weighted);
|
|
1100
1189
|
trace?.endStage(confidenceWeighted.length, confidenceWeighted.map(r => r.score));
|
|
1101
1190
|
|
|
1102
|
-
|
|
1103
|
-
let afterEmotion = confidenceWeighted;
|
|
1191
|
+
let afterEmotionVector = confidenceWeighted;
|
|
1104
1192
|
if (isEmotionScoringEnabled()) {
|
|
1105
1193
|
const queryEmotion = detectEmotion(query);
|
|
1106
1194
|
const adapted = confidenceWeighted.map(r => ({ ...r, metadata: r.entry.metadata ?? "{}" }));
|
|
1107
1195
|
const emotioned = applyEmotionWeight(adapted, queryEmotion);
|
|
1108
|
-
|
|
1196
|
+
afterEmotionVector = confidenceWeighted.map((r, i) => ({ ...r, score: emotioned[i]?.score ?? r.score }));
|
|
1109
1197
|
}
|
|
1110
1198
|
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
const boundaryWeighted = this.applyBoundaryWeight(afterEmotion);
|
|
1199
|
+
trace?.startStage("boundary_weight", afterEmotionVector.length);
|
|
1200
|
+
const boundaryWeighted = this.applyBoundaryWeight(afterEmotionVector);
|
|
1114
1201
|
trace?.endStage(boundaryWeighted.length, boundaryWeighted.map(r => r.score));
|
|
1115
1202
|
|
|
1116
|
-
// Separate pinned assets from synthesized briefs.
|
|
1117
1203
|
trace?.startStage("asset_type_weight", boundaryWeighted.length);
|
|
1118
1204
|
const assetWeighted = this.applyAssetTypeWeight(boundaryWeighted);
|
|
1119
1205
|
trace?.endStage(assetWeighted.length, assetWeighted.map(r => r.score));
|
|
1120
1206
|
|
|
1121
|
-
// Apply length normalization (penalize long entries dominating via keyword density)
|
|
1122
1207
|
trace?.startStage("length_norm", assetWeighted.length);
|
|
1123
1208
|
const lengthNormalized = this.applyLengthNormalization(assetWeighted);
|
|
1124
1209
|
trace?.endStage(lengthNormalized.length, lengthNormalized.map(r => r.score));
|
|
1125
1210
|
|
|
1126
|
-
// Apply time decay (penalize stale entries)
|
|
1127
1211
|
trace?.startStage("time_decay", lengthNormalized.length);
|
|
1128
1212
|
const timeDecayed = this.applyTimeDecay(lengthNormalized);
|
|
1129
1213
|
trace?.endStage(timeDecayed.length, timeDecayed.map(r => r.score));
|
|
@@ -1144,12 +1228,33 @@ export class MemoryRetriever {
|
|
|
1144
1228
|
const frequencyBoosted = this.applyFrequencyBoost(hotnessBlended);
|
|
1145
1229
|
trace?.endStage(frequencyBoosted.length, frequencyBoosted.map(r => r.score));
|
|
1146
1230
|
|
|
1147
|
-
//
|
|
1148
|
-
|
|
1149
|
-
|
|
1231
|
+
// A1 error-signature 精确匹配 boost(flag 门控,默认 off → no-op、bit-identical)
|
|
1232
|
+
return applyErrorSignatureBoost(frequencyBoosted, query);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
/**
|
|
1236
|
+
* Shared finalization tail — identical across both paths: sort → hard min →
|
|
1237
|
+
* evolution-status filter → noise → RIF → MMR → version/cluster dedup →
|
|
1238
|
+
* source diversity → limit. `shortQ` must be the value derived from the
|
|
1239
|
+
* path's searchQuery (do not recompute from the raw query here).
|
|
1240
|
+
*/
|
|
1241
|
+
private finalizeResults(
|
|
1242
|
+
results: RetrievalResult[],
|
|
1243
|
+
opts: {
|
|
1244
|
+
shortQ: boolean;
|
|
1245
|
+
minScoreOverride?: number;
|
|
1246
|
+
includeArchived?: boolean;
|
|
1247
|
+
limit: number;
|
|
1248
|
+
trace?: TraceCollector;
|
|
1249
|
+
},
|
|
1250
|
+
): RetrievalResult[] {
|
|
1251
|
+
const { shortQ, minScoreOverride, includeArchived, limit, trace } = opts;
|
|
1252
|
+
const rescored = [...results].sort((a, b) => b.score - a.score);
|
|
1150
1253
|
|
|
1151
1254
|
trace?.startStage("hard_min_score", rescored.length);
|
|
1152
|
-
const hardFiltered =
|
|
1255
|
+
const hardFiltered = minScoreOverride !== undefined
|
|
1256
|
+
? rescored
|
|
1257
|
+
: rescored.filter(r => r.score >= this.minScoreFor(r.entry.category, shortQ));
|
|
1153
1258
|
trace?.endStage(hardFiltered.length, hardFiltered.map(r => r.score));
|
|
1154
1259
|
|
|
1155
1260
|
// Evolution status filter: exclude superseded/archived/consolidated memories
|
|
@@ -1157,7 +1262,6 @@ export class MemoryRetriever {
|
|
|
1157
1262
|
? hardFiltered
|
|
1158
1263
|
: hardFiltered.filter(r => isActiveMemory(r.entry.metadata));
|
|
1159
1264
|
|
|
1160
|
-
// Filter noise
|
|
1161
1265
|
trace?.startStage("noise_filter", evolutionFiltered.length);
|
|
1162
1266
|
const denoised = this.config.filterNoise
|
|
1163
1267
|
? filterNoise(evolutionFiltered, r => r.entry.text)
|
|
@@ -1189,6 +1293,16 @@ export class MemoryRetriever {
|
|
|
1189
1293
|
const final = diversified.slice(0, limit);
|
|
1190
1294
|
trace?.endStage(final.length, final.map(r => r.score));
|
|
1191
1295
|
|
|
1296
|
+
// P-omitted: 从 source-diversity 之前的候选池(clusterDeduped)算 limit 截断数。
|
|
1297
|
+
// 若启用 sourceDiversity,applySourceDiversity 会先把 diversified 压到 limit,
|
|
1298
|
+
// 用 diversified.length 会让 omitted 恒为 0(Codex 复审 P3-1,与 vector-only 同类)。
|
|
1299
|
+
// 注意:clusterDeduped 仍可能含将被 retrieve 层 validity filter 过滤的 expired 项,
|
|
1300
|
+
// 故 omitted 是“上界估计”,输出层用 "up to N" 措辞反映(P3-2)。
|
|
1301
|
+
const omittedByLimit = Math.max(0, clusterDeduped.length - final.length);
|
|
1302
|
+
if (omittedByLimit > 0) {
|
|
1303
|
+
(final as RetrievalResultSet).omitted = { count: omittedByLimit, reason: "limit" };
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1192
1306
|
return final;
|
|
1193
1307
|
}
|
|
1194
1308
|
|
|
@@ -1293,9 +1407,13 @@ export class MemoryRetriever {
|
|
|
1293
1407
|
const objectScore = entityScoreMap.get(triple.object) ?? 0;
|
|
1294
1408
|
const tripleScore = Math.max(subjectScore, objectScore) * triple.confidence;
|
|
1295
1409
|
|
|
1296
|
-
if (tripleScore > 0
|
|
1297
|
-
|
|
1298
|
-
|
|
1410
|
+
if (tripleScore > 0) {
|
|
1411
|
+
// All contributing memories share the fact's score — not just the
|
|
1412
|
+
// first contributor (multi-source triples after mention counting)
|
|
1413
|
+
for (const mid of parseSourceIds(triple.source_memory_ids, triple.source_memory_id)) {
|
|
1414
|
+
const existing = memoryScoreMap.get(mid) ?? 0;
|
|
1415
|
+
memoryScoreMap.set(mid, Math.max(existing, tripleScore));
|
|
1416
|
+
}
|
|
1299
1417
|
}
|
|
1300
1418
|
}
|
|
1301
1419
|
}
|
|
@@ -1831,8 +1949,10 @@ export class MemoryRetriever {
|
|
|
1831
1949
|
const existingIds = new Set(results.map(r => r.entry.id));
|
|
1832
1950
|
for (const n of neighborhood) {
|
|
1833
1951
|
for (const t of n.triples) {
|
|
1834
|
-
|
|
1835
|
-
|
|
1952
|
+
// Expand through ALL contributing memories of each fact, not just
|
|
1953
|
+
// the first contributor (multi-source triples after mention counting)
|
|
1954
|
+
for (const mid of parseSourceIds(t.source_memory_ids, t.source_memory_id)) {
|
|
1955
|
+
if (!existingIds.has(mid)) sourceIds.add(mid);
|
|
1836
1956
|
}
|
|
1837
1957
|
}
|
|
1838
1958
|
}
|
package/src/runtime-config.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { AccessTracker } from "./access-tracker.js";
|
|
|
11
11
|
import { FrequencyTracker } from "./frequency-tracker.js";
|
|
12
12
|
import { createLLMClient, type LLMClient, type LLMConfig } from "./llm-client.js";
|
|
13
13
|
import { logInfo } from "./stderr-log.js";
|
|
14
|
+
import * as envConfig from "./env-config.js";
|
|
14
15
|
|
|
15
16
|
export type RecallMode = "full" | "light" | "summary" | "off";
|
|
16
17
|
|
|
@@ -33,6 +34,12 @@ export interface LocalMemoryConfig {
|
|
|
33
34
|
};
|
|
34
35
|
sources: Record<string, { path: string; glob: string; description: string }>;
|
|
35
36
|
retrieval?: Partial<RetrievalConfig>;
|
|
37
|
+
/**
|
|
38
|
+
* Opt-in cross-scope sidecar map for search_memory.
|
|
39
|
+
* Main scoped search remains isolated; callers must pass includeRelatedScopes
|
|
40
|
+
* before configured related scopes are queried and shown separately.
|
|
41
|
+
*/
|
|
42
|
+
scopeRelations?: Record<string, string[]>;
|
|
36
43
|
/**
|
|
37
44
|
* Default depth for auto-recall injection.
|
|
38
45
|
* - "full": inject complete text (default, backward compatible)
|
|
@@ -179,7 +186,7 @@ export function resolveRecallMode(config: LocalMemoryConfig, perCallOverride?: s
|
|
|
179
186
|
if (perCallOverride && VALID_RECALL_MODES.includes(perCallOverride as RecallMode)) {
|
|
180
187
|
return perCallOverride as RecallMode;
|
|
181
188
|
}
|
|
182
|
-
const envMode =
|
|
189
|
+
const envMode = envConfig.recallModeRaw();
|
|
183
190
|
if (envMode && VALID_RECALL_MODES.includes(envMode as RecallMode)) {
|
|
184
191
|
return envMode as RecallMode;
|
|
185
192
|
}
|