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/retriever.ts
CHANGED
|
@@ -6,12 +6,15 @@
|
|
|
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";
|
|
13
16
|
import { weibullDecay, resolveTier, isDecayExempt, adjustHalfLifeForEmotion, computeArousalBoost } from "./decay-engine.js";
|
|
14
|
-
import { logWarn } from "./stderr-log.js";
|
|
17
|
+
import { logInfo, logWarn } from "./stderr-log.js";
|
|
15
18
|
import { extractBoundaryMetadata, isDurableMemoryScope, isTranscriptScope } from "./memory-boundaries.js";
|
|
16
19
|
import type { TraceCollector } from "./retrieval-trace.js";
|
|
17
20
|
import { extractTopicTag } from "./topic-tag.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,
|
|
@@ -102,6 +107,7 @@ export interface RetrievalConfig {
|
|
|
102
107
|
/**
|
|
103
108
|
* Hotness blend weight. Blends access-frequency hotness into final score.
|
|
104
109
|
* Formula: final = score * (1-alpha) + hotness * alpha.
|
|
110
|
+
* Zero-access rows keep their score unchanged (bonus-only cold start).
|
|
105
111
|
* 0 = disabled (default), 0.15 = recommended.
|
|
106
112
|
*/
|
|
107
113
|
hotnessWeight: number;
|
|
@@ -189,6 +195,12 @@ export interface RetrievalResult extends MemorySearchResult {
|
|
|
189
195
|
*/
|
|
190
196
|
export type RetrievalResultSet = RetrievalResult[] & {
|
|
191
197
|
reconstruction?: ReconstructionOutput;
|
|
198
|
+
/**
|
|
199
|
+
* P-omitted: 因 limit 上限被截断、未返回的达标结果数(提高 limit 可见)。
|
|
200
|
+
* 仅统计 finalizeResults 的 limit 截断,不含质量过滤(min-score/noise/MMR)丢弃——
|
|
201
|
+
* 那些提高 limit 也拿不到,报告出来反而误导调用方。
|
|
202
|
+
*/
|
|
203
|
+
omitted?: { count: number; reason: "limit" };
|
|
192
204
|
};
|
|
193
205
|
|
|
194
206
|
// ============================================================================
|
|
@@ -209,24 +221,73 @@ export const DEFAULT_CATEGORY_MIN_SCORES: Record<string, number> = {
|
|
|
209
221
|
patterns: 0.45,
|
|
210
222
|
};
|
|
211
223
|
|
|
224
|
+
// P1.2(2026-06-25):默认 vector-only。canary eval 实测个人记忆库 vector 召回质量全面优于
|
|
225
|
+
// hybrid(robustness 带 scope WITH 6/12 + 不带 scope WITHOUT 10/12,完胜 hybrid 的 3/9;BM25 在
|
|
226
|
+
// 高度同质的个人记忆里是净噪声,会把 vector 本应 top 的目标如 dbcb317a 挤掉)。改频率层救低频
|
|
227
|
+
// 目标实测是死结(动 hotness 必伤靠 hotness 上位的记忆)。hybrid 路径代码全保留,mode 改回
|
|
228
|
+
// "hybrid" 即可切换(注:multiHopExpand 仍硬编码走 hybrid,默认 off,config.retrieval.multiHop
|
|
229
|
+
// 开了会破纯 vector)。数据盲区:canary 全是语义 query,未覆盖 BM25 强项的精确关键词/token 召回
|
|
230
|
+
// (变量名 / commit hash / alias-map 昵称)——待补 exact-token canary 验证。
|
|
212
231
|
export const DEFAULT_RETRIEVAL_CONFIG: RetrievalConfig = {
|
|
213
|
-
mode: "
|
|
232
|
+
mode: "vector",
|
|
214
233
|
vectorWeight: 0.7,
|
|
215
234
|
bm25Weight: 0.3,
|
|
216
235
|
minScore: 0.3,
|
|
217
236
|
rerank: "cross-encoder",
|
|
218
|
-
candidatePoolSize:
|
|
237
|
+
candidatePoolSize: 30,
|
|
238
|
+
sourceDiversity: 1,
|
|
219
239
|
recencyHalfLifeDays: 14,
|
|
220
240
|
recencyWeight: 0.10,
|
|
221
241
|
filterNoise: true,
|
|
222
242
|
rerankModel: "jina-reranker-v3",
|
|
223
243
|
rerankEndpoint: "https://api.jina.ai/v1/rerank",
|
|
224
|
-
|
|
244
|
+
// 2026-08-06:500 → 800。不是拍的——writing profile 早就在用 800
|
|
245
|
+
// (retrieval-profiles.ts),这里只是把一个已在生产跑着的值推广到 default。
|
|
246
|
+
// 实测(eval/ghost-scan.ts,14,949 条 active 全库快照):结构性幽灵
|
|
247
|
+
// 775 → 245(5.18% → 1.64%),救回 530 条,中位长度 1,356 字符那批正落在
|
|
248
|
+
// 打折最重的区间。挤占效应见 eval/lengthnorm-shadow.ts:20 条真实 query
|
|
249
|
+
// 里 14 条 top-6 完全不变,6 条掉出在 limit 放大到 20 后全部复现
|
|
250
|
+
// ——即没有任何条目被删掉,只是挤出窗口,而那 6 条的重排来自
|
|
251
|
+
// applySourceDiversity + MMR(候选数 ≤ limit 时它根本不轮转,
|
|
252
|
+
// anchor 放大让更多候选过阈值才把这个既有行为激活),与本参数无关。
|
|
253
|
+
lengthNormAnchor: 800,
|
|
225
254
|
hardMinScore: 0.35,
|
|
226
255
|
timeDecayHalfLifeDays: 60,
|
|
227
256
|
hotnessWeight: 0,
|
|
228
257
|
};
|
|
229
258
|
|
|
259
|
+
/**
|
|
260
|
+
* P1.2+: 检测 query 是否含「精确 token」——大写常量 / commit hash / 文件名 / snake/camelCase /
|
|
261
|
+
* 低元音 ID(如 rwmjhb)。这类 token vectorOnly 抓不到(embedding 做语义相似非精确匹配,跟语言
|
|
262
|
+
* 无关:实测纯/中文/中英混 query 都漏靠 token 区分的目标),需 BM25 兜底。刻意避开元音正常的普通词
|
|
263
|
+
* (mem0/recallnest/GitHub),避免日常语义 query 误触发 BM25 噪声。
|
|
264
|
+
*/
|
|
265
|
+
export function hasExactToken(query: string): boolean {
|
|
266
|
+
// 【为什么中文 query 不触发本函数 —— 这是实测后的决定,不是 ASCII-only 的遗漏】
|
|
267
|
+
// 2026-08-01 用临时开关对 5 条真实中文 query 做过 A/B:强制让中文触发 BM25 补偿后,
|
|
268
|
+
// 3/4 的 query 零新增(BM25 捞回的条目 vector 早已召回,被 vecIds 去重滤掉),
|
|
269
|
+
// 唯一有新增的那次补进 3 条、其中 1 条是当天无关对话,且补偿结果是**插在队首**的。
|
|
270
|
+
// 中文分词本身没问题(babel-memory 已注册,detectLang 正确返回 zh,
|
|
271
|
+
// "双机契约" → "双机 契约"),所以这不是 tokenizer 问题 ——
|
|
272
|
+
// 是 BM25 对中文**语义类** query 的边际收益本就接近零,放开只会拿噪声占前三名。
|
|
273
|
+
// 若将来要重开,先复现那组 A/B,别只凭"中文没走 BM25"这个现象下判断。
|
|
274
|
+
for (const t of query.split(/[\s,。、;:()()【】「」"'`??!!]+/)) {
|
|
275
|
+
if (t.length < 5) continue;
|
|
276
|
+
// 全大写 + 下划线常量(必须含下划线,避免 JSON/HTTP/GitHub 等普通缩写/驼峰误触发)
|
|
277
|
+
if (/^[A-Z][A-Z0-9]*_[A-Z0-9_]+$/.test(t)) return true; // DEDUP_HARD_THRESHOLD
|
|
278
|
+
// snake_case 标识符(含下划线)
|
|
279
|
+
if (/^[a-z][a-z0-9]*_[a-z0-9_]+$/i.test(t)) return true; // capture_engine
|
|
280
|
+
// 文件名(基名 + 代码扩展名)
|
|
281
|
+
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
|
|
282
|
+
// hex commit hash(7-40,含数字+字母,纯 hex)
|
|
283
|
+
if (/^[0-9a-f]{7,40}$/i.test(t) && /[0-9]/.test(t) && /[a-f]/i.test(t)) return true; // dd12fb5
|
|
284
|
+
// 注:camelCase(GitHub/TypeScript)与低元音 ID(rwmjhb/pytest/nginx)两条宽规则已移除——
|
|
285
|
+
// 前者误伤日常技术 query 引回 BM25 噪声;rwmjhb 类纯小写 ID 即便触发,fts 中英混分词也召回不到
|
|
286
|
+
// (根因在 tokenizer,是独立 backlog),靠中英一起 query 的中文锚点已能补回。
|
|
287
|
+
}
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
|
|
230
291
|
// ============================================================================
|
|
231
292
|
// Utility Functions
|
|
232
293
|
// ============================================================================
|
|
@@ -314,6 +375,32 @@ function applyAnchorBoost(results: RetrievalResult[], query: string): RetrievalR
|
|
|
314
375
|
});
|
|
315
376
|
}
|
|
316
377
|
|
|
378
|
+
/** Multiplicative boost applied to a case on exact error-signature match. */
|
|
379
|
+
const ERROR_SIGNATURE_BOOST_FACTOR = 1.5;
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* A1 (P3-A): error-signature exact-match boost. When RECALLNEST_ERROR_SIGNATURE_BOOST
|
|
383
|
+
* is on, case memories whose metadata.error_signature overlaps the query's extracted
|
|
384
|
+
* signatures get a strong multiplicative boost ("上次一模一样的报错"精确召回). Flag off
|
|
385
|
+
* (default) returns the input unchanged — bit-identical, so the pure-move eval baseline holds.
|
|
386
|
+
* Only affects category="cases"; read and query side share extractErrorSignatures so the
|
|
387
|
+
* signatures compare apples-to-apples.
|
|
388
|
+
*/
|
|
389
|
+
export function applyErrorSignatureBoost(results: RetrievalResult[], query: string): RetrievalResult[] {
|
|
390
|
+
if (!envConfig.errorSignatureBoost()) return results;
|
|
391
|
+
const querySignatures = extractErrorSignatures({ problem: query });
|
|
392
|
+
if (querySignatures.length === 0) return results;
|
|
393
|
+
const querySet = new Set(querySignatures.map(s => s.toLowerCase()));
|
|
394
|
+
return results.map(r => {
|
|
395
|
+
if (r.entry.category !== "cases") return r;
|
|
396
|
+
const sigs = parseMetadata(r.entry.metadata).error_signature;
|
|
397
|
+
if (!Array.isArray(sigs) || sigs.length === 0) return r;
|
|
398
|
+
const hit = sigs.some(s => typeof s === "string" && querySet.has(s.toLowerCase()));
|
|
399
|
+
if (!hit) return r;
|
|
400
|
+
return { ...r, score: Math.min(1, Math.max(0, r.score * ERROR_SIGNATURE_BOOST_FACTOR)) };
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
317
404
|
/**
|
|
318
405
|
* Emotion scoring stage: boost memories matching query emotional tone.
|
|
319
406
|
* No-op when query is neutral or memory lacks emotion data.
|
|
@@ -668,6 +755,16 @@ export class MemoryRetriever {
|
|
|
668
755
|
this.llmClient = client;
|
|
669
756
|
}
|
|
670
757
|
|
|
758
|
+
/**
|
|
759
|
+
* Shadow-record which memories a reconstruction actually cited via [src:ID].
|
|
760
|
+
* 影子统计:写 metadata.usage(injection-vs-use 双计数),第一阶段不参与 ranking/forget。
|
|
761
|
+
* best-effort:不传 scope(影子写入不做权限过滤,避免跨 scope 扩展候选触发越权 throw);
|
|
762
|
+
* 内部 allSettled 不抛错,绝不阻断主检索/resume 流程。
|
|
763
|
+
*/
|
|
764
|
+
async recordReconstructionUsage(citedIds: string[]): Promise<void> {
|
|
765
|
+
await runUsageShadow(this.store, citedIds);
|
|
766
|
+
}
|
|
767
|
+
|
|
671
768
|
/** Suppress a memory ID for the current session (score × 0.3). */
|
|
672
769
|
suppressForSession(id: string): void {
|
|
673
770
|
this.sessionSuppressed.add(id);
|
|
@@ -687,16 +784,30 @@ export class MemoryRetriever {
|
|
|
687
784
|
}
|
|
688
785
|
|
|
689
786
|
async retrieve(context: RetrievalContext): Promise<RetrievalResultSet> {
|
|
690
|
-
const { query, limit, scopeFilter, category, includeArchived, trace, graph } = context;
|
|
787
|
+
const { query: rawQuery, limit, scopeFilter, category, includeArchived, trace, graph } = context;
|
|
691
788
|
const safeLimit = clampInt(limit, 1, 20);
|
|
692
789
|
|
|
693
790
|
// Adaptive retrieval: skip trivial queries to save embedding API calls
|
|
694
|
-
if (shouldSkipRetrieval(
|
|
791
|
+
if (shouldSkipRetrieval(rawQuery)) {
|
|
695
792
|
return [];
|
|
696
793
|
}
|
|
697
794
|
|
|
795
|
+
// Step 2: alias expansion — append canonical entity tokens for matching colloquial phrases.
|
|
796
|
+
// 不改原 query,仅追加,保留 vector embedding 原意;BM25 拿到更多锚点。
|
|
797
|
+
const query = expandQueryWithAliases(rawQuery);
|
|
798
|
+
|
|
698
799
|
let results: RetrievalResult[];
|
|
699
800
|
|
|
801
|
+
// 分派前必须先让 store 完成惰性初始化,否则下面读到的 hasFtsSupport 是初始化前的
|
|
802
|
+
// false,hybrid 模式的**首次**检索会静默落回 vector 路径(同一实例第二次才正常)。
|
|
803
|
+
// 该缺陷自 c10607a 初始版本就在,长期未暴露有两个原因:生产 mode 一直是 "vector",
|
|
804
|
+
// 第一个条件短路、根本读不到第二个;而 eval 的 createFreshEvalComponentsFactory 每
|
|
805
|
+
// case 新建组件,每个 case 都停在"首次",于是 eval 从未测到过 hybrid 路径。
|
|
806
|
+
// 注:supplementExactTokenWithBm25 那里的顺序依赖是被意识到的(见其注释"hasFtsSupport
|
|
807
|
+
// 在 vectorOnly 触发 store init 后为真"),漏的只是这个分派点。
|
|
808
|
+
// `?.` 是必需的:测试大量使用鸭子类型的 fake store,它们没有 ready()。
|
|
809
|
+
await this.store.ready?.();
|
|
810
|
+
|
|
700
811
|
// For vector-only mode, use legacy behavior
|
|
701
812
|
if (this.config.mode === "vector" || !this.store.hasFtsSupport) {
|
|
702
813
|
results = await this.vectorOnlyRetrieval(query, safeLimit, scopeFilter, category, includeArchived, trace);
|
|
@@ -705,6 +816,28 @@ export class MemoryRetriever {
|
|
|
705
816
|
results = await this.hybridRetrieval(query, safeLimit, scopeFilter, category, includeArchived, trace, graph);
|
|
706
817
|
}
|
|
707
818
|
|
|
819
|
+
// 0-hit fallback: minScore filter 把所有候选过滤光时,放宽阈值(=0)重试一次。
|
|
820
|
+
// 只在原 path 0 hit 触发;正常召回不受影响。
|
|
821
|
+
if (results.length === 0) {
|
|
822
|
+
if (this.config.mode === "vector" || !this.store.hasFtsSupport) {
|
|
823
|
+
results = await this.vectorOnlyRetrieval(query, safeLimit, scopeFilter, category, includeArchived, trace, 0);
|
|
824
|
+
} else {
|
|
825
|
+
results = await this.hybridRetrieval(query, safeLimit, scopeFilter, category, includeArchived, trace, graph, 0);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// P-omitted: 快照 finalize 阶段因 limit 截断的达标结果数(提高 limit 可见)。
|
|
830
|
+
// 必须在后续 validity/topicTag filter(以及 BM25 补偿等换数组操作)丢失 ResultSet 属性之前读取。
|
|
831
|
+
let omittedByLimit = (results as RetrievalResultSet).omitted?.count ?? 0;
|
|
832
|
+
|
|
833
|
+
// P1.2+: exact-token BM25 补偿——query 含精确 token(大写常量/commit hash/.ts 文件名/camelCase/
|
|
834
|
+
// 低元音 ID 如 rwmjhb)时,vector 漏掉的精确命中用 BM25 补回(vectorOnly 做语义相似、不做精确
|
|
835
|
+
// 字符串匹配,是 embedding 本质,跟语言无关)。普通词(mem0/recallnest/GitHub)不触发,日常
|
|
836
|
+
// 语义召回不受影响。
|
|
837
|
+
if (hasExactToken(rawQuery)) {
|
|
838
|
+
results = await this.supplementExactTokenWithBm25(query, results, safeLimit, scopeFilter, category, includeArchived);
|
|
839
|
+
}
|
|
840
|
+
|
|
708
841
|
// LME-2: Multi-hop retrieval — extract entities from first-pass results,
|
|
709
842
|
// run focused follow-up queries, merge to improve cross-session coverage.
|
|
710
843
|
const useMultiHop = context.multiHop ?? this.config.multiHop ?? false;
|
|
@@ -773,7 +906,11 @@ export class MemoryRetriever {
|
|
|
773
906
|
|
|
774
907
|
// Phase 4: Constructive retrieval — first-class reconstruction (no metadata hack)
|
|
775
908
|
const resultSet = results as RetrievalResultSet;
|
|
776
|
-
|
|
909
|
+
// P-omitted: 把快照的 limit 截断数挂回最终 resultSet(validity filter 已丢原数组属性)。
|
|
910
|
+
if (omittedByLimit > 0) {
|
|
911
|
+
resultSet.omitted = { count: omittedByLimit, reason: "limit" };
|
|
912
|
+
}
|
|
913
|
+
const constructiveFlag = envConfig.constructiveRetrieval();
|
|
777
914
|
if (shouldReconstruct({
|
|
778
915
|
flagEnabled: constructiveFlag,
|
|
779
916
|
callerOptIn: context.reconstruct === true,
|
|
@@ -790,6 +927,8 @@ export class MemoryRetriever {
|
|
|
790
927
|
expansionDeps,
|
|
791
928
|
);
|
|
792
929
|
resultSet.reconstruction = reconstruction;
|
|
930
|
+
// Shadow usage:记录被 reconstruction 真正引用的记忆(use 信号),不动 ranking/forget。
|
|
931
|
+
await this.recordReconstructionUsage(reconstruction.sources.map(s => s.id));
|
|
793
932
|
} catch {
|
|
794
933
|
// Silent degradation — return raw results
|
|
795
934
|
}
|
|
@@ -798,6 +937,36 @@ export class MemoryRetriever {
|
|
|
798
937
|
return resultSet;
|
|
799
938
|
}
|
|
800
939
|
|
|
940
|
+
/**
|
|
941
|
+
* P1.2+: exact-token BM25 补偿。vectorOnly 主路径后,若 query 含精确 token,用 BM25 精确匹配把
|
|
942
|
+
* vector 漏掉的命中补回。hasFtsSupport 在 vectorOnly 触发 store init 后为真;fake store 为假时跳过。
|
|
943
|
+
* bm25 精确命中提到结果前(exact-token 意图=精确优先),vector 语义结果跟后,去重后 slice limit。
|
|
944
|
+
*/
|
|
945
|
+
private async supplementExactTokenWithBm25(
|
|
946
|
+
query: string,
|
|
947
|
+
vectorResults: RetrievalResult[],
|
|
948
|
+
limit: number,
|
|
949
|
+
scopeFilter?: string[],
|
|
950
|
+
category?: string,
|
|
951
|
+
includeArchived?: boolean,
|
|
952
|
+
): Promise<RetrievalResult[]> {
|
|
953
|
+
if (!this.store.hasFtsSupport) return vectorResults;
|
|
954
|
+
const ftsQuery = tokenizeFts(expandQuery(query), detectLang(query));
|
|
955
|
+
const bm25 = await this.runBM25Search(ftsQuery, Math.max(limit, 5), scopeFilter, category, includeArchived);
|
|
956
|
+
const vecIds = new Set(vectorResults.map(r => r.entry.id));
|
|
957
|
+
const supplements = bm25
|
|
958
|
+
.filter(b => !vecIds.has(b.entry.id))
|
|
959
|
+
.filter(b => isActiveMemory(b.entry.metadata)) // 排掉 superseded/archived/consolidated(补偿绕过了 finalize 的尾部过滤,这里补回 active 校验)
|
|
960
|
+
.slice(0, 3)
|
|
961
|
+
.map(b => ({
|
|
962
|
+
entry: b.entry,
|
|
963
|
+
score: b.score,
|
|
964
|
+
sources: { bm25: { score: b.score, rank: b.rank } },
|
|
965
|
+
} as RetrievalResult));
|
|
966
|
+
if (supplements.length === 0) return vectorResults;
|
|
967
|
+
return [...supplements, ...vectorResults].slice(0, limit);
|
|
968
|
+
}
|
|
969
|
+
|
|
801
970
|
/**
|
|
802
971
|
* LME-2: Multi-hop expansion — extract entities from first-pass results,
|
|
803
972
|
* run entity-focused follow-up queries, merge with original results.
|
|
@@ -860,6 +1029,7 @@ export class MemoryRetriever {
|
|
|
860
1029
|
category?: string,
|
|
861
1030
|
includeArchived?: boolean,
|
|
862
1031
|
trace?: TraceCollector,
|
|
1032
|
+
minScoreOverride?: number,
|
|
863
1033
|
): Promise<RetrievalResult[]> {
|
|
864
1034
|
// Temporal reasoning for vector-only mode
|
|
865
1035
|
const temporal = parseTemporalQuery(query);
|
|
@@ -867,12 +1037,20 @@ export class MemoryRetriever {
|
|
|
867
1037
|
|
|
868
1038
|
trace?.startStage("vector_search", 0);
|
|
869
1039
|
const queryVector = await this.embedder.embedQuery(searchQuery);
|
|
870
|
-
// P0.1: Lower minScore for short queries
|
|
1040
|
+
// P0.1: Lower minScore for short queries; minScoreOverride 用于 0-hit fallback retry
|
|
871
1041
|
const shortQ = isShortQuery(searchQuery);
|
|
872
|
-
const vectorMinScore =
|
|
873
|
-
?
|
|
874
|
-
:
|
|
875
|
-
|
|
1042
|
+
const vectorMinScore = minScoreOverride !== undefined
|
|
1043
|
+
? minScoreOverride
|
|
1044
|
+
: (shortQ
|
|
1045
|
+
? this.config.minScore * SHORT_QUERY_MIN_SCORE_FACTOR
|
|
1046
|
+
: this.config.minScore);
|
|
1047
|
+
// P-omitted: over-fetch a wider candidate pool so finalizeResults can detect limit-capped
|
|
1048
|
+
// omissions. Previously fetched exactly `limit`, so vector-only omitted was always 0
|
|
1049
|
+
// (hybrid already over-fetches candidatePoolSize). Cost is minimal — ANN with a larger K
|
|
1050
|
+
// plus a few more local scoring passes, no extra API; P1.2's vector-only savings come from
|
|
1051
|
+
// skipping hybrid BM25/fusion/rerank, not from fetch size.
|
|
1052
|
+
const fetchLimit = Math.max(limit, this.config.candidatePoolSize ?? limit);
|
|
1053
|
+
const results = await this.store.vectorSearch(queryVector, fetchLimit, vectorMinScore, scopeFilter);
|
|
876
1054
|
trace?.endStage(results.length, results.map(r => r.score));
|
|
877
1055
|
|
|
878
1056
|
// Filter by category if specified
|
|
@@ -900,103 +1078,18 @@ export class MemoryRetriever {
|
|
|
900
1078
|
// P0.1: Anchor boost for vector-only path
|
|
901
1079
|
const anchorBoosted = applyAnchorBoost(mapped, searchQuery);
|
|
902
1080
|
|
|
903
|
-
|
|
904
|
-
const boosted = this.applyRecencyBoost(anchorBoosted);
|
|
905
|
-
trace?.endStage(boosted.length, boosted.map(r => r.score));
|
|
906
|
-
|
|
907
|
-
trace?.startStage("importance_weight", boosted.length);
|
|
908
|
-
const weighted = this.applyImportanceWeight(boosted);
|
|
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));
|
|
926
|
-
|
|
927
|
-
trace?.startStage("asset_type_weight", boundaryWeighted.length);
|
|
928
|
-
const assetWeighted = this.applyAssetTypeWeight(boundaryWeighted);
|
|
929
|
-
trace?.endStage(assetWeighted.length, assetWeighted.map(r => r.score));
|
|
930
|
-
|
|
931
|
-
trace?.startStage("length_norm", assetWeighted.length);
|
|
932
|
-
const lengthNormalized = this.applyLengthNormalization(assetWeighted);
|
|
933
|
-
trace?.endStage(lengthNormalized.length, lengthNormalized.map(r => r.score));
|
|
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;
|
|
1081
|
+
const scored = this.applySharedScoring(anchorBoosted, query, trace);
|
|
994
1082
|
|
|
995
|
-
|
|
996
|
-
const
|
|
997
|
-
trace?.endStage(final.length, final.map(r => r.score));
|
|
1083
|
+
// Session suppression: temporarily penalize "do not disturb" memories (vector-only path)
|
|
1084
|
+
const afterSuppression = this.applySessionSuppression(scored);
|
|
998
1085
|
|
|
999
|
-
return
|
|
1086
|
+
return this.finalizeResults(afterSuppression, {
|
|
1087
|
+
shortQ,
|
|
1088
|
+
minScoreOverride,
|
|
1089
|
+
includeArchived,
|
|
1090
|
+
limit,
|
|
1091
|
+
trace,
|
|
1092
|
+
});
|
|
1000
1093
|
}
|
|
1001
1094
|
|
|
1002
1095
|
private async hybridRetrieval(
|
|
@@ -1007,6 +1100,7 @@ export class MemoryRetriever {
|
|
|
1007
1100
|
includeArchived?: boolean,
|
|
1008
1101
|
trace?: TraceCollector,
|
|
1009
1102
|
graph?: boolean,
|
|
1103
|
+
minScoreOverride?: number,
|
|
1010
1104
|
): Promise<RetrievalResult[]> {
|
|
1011
1105
|
// Adaptive pool: widen candidate pool for aggregation queries ("how many", "all the")
|
|
1012
1106
|
const multiplier = (this.config.adaptivePoolMultiplier ?? 1) > 1 && isAggregationQuery(query)
|
|
@@ -1070,10 +1164,13 @@ export class MemoryRetriever {
|
|
|
1070
1164
|
|
|
1071
1165
|
// Apply minimum score threshold
|
|
1072
1166
|
trace?.startStage("min_score_filter", anchorBoosted.length);
|
|
1073
|
-
// P0.1: Lower minScore for short queries to widen the candidate pool
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1167
|
+
// P0.1: Lower minScore for short queries to widen the candidate pool;
|
|
1168
|
+
// minScoreOverride 用于 retrieve() 顶层的 0-hit fallback retry。
|
|
1169
|
+
const effectiveMinScore = minScoreOverride !== undefined
|
|
1170
|
+
? minScoreOverride
|
|
1171
|
+
: (isShortQuery(searchQuery)
|
|
1172
|
+
? this.config.minScore * SHORT_QUERY_MIN_SCORE_FACTOR
|
|
1173
|
+
: this.config.minScore);
|
|
1077
1174
|
const filtered = anchorBoosted.filter(r => r.score >= effectiveMinScore);
|
|
1078
1175
|
trace?.endStage(filtered.length, filtered.map(r => r.score));
|
|
1079
1176
|
|
|
@@ -1084,46 +1181,61 @@ export class MemoryRetriever {
|
|
|
1084
1181
|
: filtered;
|
|
1085
1182
|
trace?.endStage(reranked.length, reranked.map(r => r.score));
|
|
1086
1183
|
|
|
1087
|
-
|
|
1088
|
-
trace?.startStage("recency_boost", reranked.length);
|
|
1089
|
-
const temporalReranked = this.applyRecencyBoost(reranked);
|
|
1090
|
-
trace?.endStage(temporalReranked.length, temporalReranked.map(r => r.score));
|
|
1184
|
+
const scored = this.applySharedScoring(reranked, query, trace);
|
|
1091
1185
|
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1186
|
+
return this.finalizeResults(scored, {
|
|
1187
|
+
shortQ,
|
|
1188
|
+
minScoreOverride,
|
|
1189
|
+
includeArchived,
|
|
1190
|
+
limit,
|
|
1191
|
+
trace,
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1096
1194
|
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1195
|
+
/**
|
|
1196
|
+
* Shared scoring pipeline — the 12 stages identical across the vector-only and
|
|
1197
|
+
* hybrid paths. Order is load-bearing (Codex §3.6 "顺序即语义"); do not reorder.
|
|
1198
|
+
* `query` feeds the emotion stage; `trace` carries per-stage trace spans.
|
|
1199
|
+
* Path-specific stages stay in the caller: anchor / min_score / rerank / multi-vector
|
|
1200
|
+
* blend run BEFORE this; vector-only session suppression runs AFTER this.
|
|
1201
|
+
*/
|
|
1202
|
+
private applySharedScoring(
|
|
1203
|
+
results: RetrievalResult[],
|
|
1204
|
+
query: string,
|
|
1205
|
+
trace?: TraceCollector,
|
|
1206
|
+
): RetrievalResult[] {
|
|
1207
|
+
trace?.startStage("recency_boost", results.length);
|
|
1208
|
+
const boosted = this.applyRecencyBoost(results);
|
|
1209
|
+
trace?.endStage(boosted.length, boosted.map(r => r.score));
|
|
1210
|
+
|
|
1211
|
+
trace?.startStage("importance_weight", boosted.length);
|
|
1212
|
+
const weighted = this.applyImportanceWeight(boosted);
|
|
1213
|
+
trace?.endStage(weighted.length, weighted.map(r => r.score));
|
|
1214
|
+
|
|
1215
|
+
trace?.startStage("confidence_weight", weighted.length);
|
|
1216
|
+
const confidenceWeighted = this.applyConfidence(weighted);
|
|
1100
1217
|
trace?.endStage(confidenceWeighted.length, confidenceWeighted.map(r => r.score));
|
|
1101
1218
|
|
|
1102
|
-
|
|
1103
|
-
let afterEmotion = confidenceWeighted;
|
|
1219
|
+
let afterEmotionVector = confidenceWeighted;
|
|
1104
1220
|
if (isEmotionScoringEnabled()) {
|
|
1105
1221
|
const queryEmotion = detectEmotion(query);
|
|
1106
1222
|
const adapted = confidenceWeighted.map(r => ({ ...r, metadata: r.entry.metadata ?? "{}" }));
|
|
1107
1223
|
const emotioned = applyEmotionWeight(adapted, queryEmotion);
|
|
1108
|
-
|
|
1224
|
+
afterEmotionVector = confidenceWeighted.map((r, i) => ({ ...r, score: emotioned[i]?.score ?? r.score }));
|
|
1109
1225
|
}
|
|
1110
1226
|
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
const boundaryWeighted = this.applyBoundaryWeight(afterEmotion);
|
|
1227
|
+
trace?.startStage("boundary_weight", afterEmotionVector.length);
|
|
1228
|
+
const boundaryWeighted = this.applyBoundaryWeight(afterEmotionVector);
|
|
1114
1229
|
trace?.endStage(boundaryWeighted.length, boundaryWeighted.map(r => r.score));
|
|
1115
1230
|
|
|
1116
|
-
// Separate pinned assets from synthesized briefs.
|
|
1117
1231
|
trace?.startStage("asset_type_weight", boundaryWeighted.length);
|
|
1118
1232
|
const assetWeighted = this.applyAssetTypeWeight(boundaryWeighted);
|
|
1119
1233
|
trace?.endStage(assetWeighted.length, assetWeighted.map(r => r.score));
|
|
1120
1234
|
|
|
1121
|
-
// Apply length normalization (penalize long entries dominating via keyword density)
|
|
1122
1235
|
trace?.startStage("length_norm", assetWeighted.length);
|
|
1123
1236
|
const lengthNormalized = this.applyLengthNormalization(assetWeighted);
|
|
1124
1237
|
trace?.endStage(lengthNormalized.length, lengthNormalized.map(r => r.score));
|
|
1125
1238
|
|
|
1126
|
-
// Apply time decay (penalize stale entries)
|
|
1127
1239
|
trace?.startStage("time_decay", lengthNormalized.length);
|
|
1128
1240
|
const timeDecayed = this.applyTimeDecay(lengthNormalized);
|
|
1129
1241
|
trace?.endStage(timeDecayed.length, timeDecayed.map(r => r.score));
|
|
@@ -1144,12 +1256,33 @@ export class MemoryRetriever {
|
|
|
1144
1256
|
const frequencyBoosted = this.applyFrequencyBoost(hotnessBlended);
|
|
1145
1257
|
trace?.endStage(frequencyBoosted.length, frequencyBoosted.map(r => r.score));
|
|
1146
1258
|
|
|
1147
|
-
//
|
|
1148
|
-
|
|
1149
|
-
|
|
1259
|
+
// A1 error-signature 精确匹配 boost(flag 门控,默认 off → no-op、bit-identical)
|
|
1260
|
+
return applyErrorSignatureBoost(frequencyBoosted, query);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
/**
|
|
1264
|
+
* Shared finalization tail — identical across both paths: sort → hard min →
|
|
1265
|
+
* evolution-status filter → noise → RIF → MMR → version/cluster dedup →
|
|
1266
|
+
* source diversity → limit. `shortQ` must be the value derived from the
|
|
1267
|
+
* path's searchQuery (do not recompute from the raw query here).
|
|
1268
|
+
*/
|
|
1269
|
+
private finalizeResults(
|
|
1270
|
+
results: RetrievalResult[],
|
|
1271
|
+
opts: {
|
|
1272
|
+
shortQ: boolean;
|
|
1273
|
+
minScoreOverride?: number;
|
|
1274
|
+
includeArchived?: boolean;
|
|
1275
|
+
limit: number;
|
|
1276
|
+
trace?: TraceCollector;
|
|
1277
|
+
},
|
|
1278
|
+
): RetrievalResult[] {
|
|
1279
|
+
const { shortQ, minScoreOverride, includeArchived, limit, trace } = opts;
|
|
1280
|
+
const rescored = [...results].sort((a, b) => b.score - a.score);
|
|
1150
1281
|
|
|
1151
1282
|
trace?.startStage("hard_min_score", rescored.length);
|
|
1152
|
-
const hardFiltered =
|
|
1283
|
+
const hardFiltered = minScoreOverride !== undefined
|
|
1284
|
+
? rescored
|
|
1285
|
+
: rescored.filter(r => r.score >= this.minScoreFor(r.entry.category, shortQ));
|
|
1153
1286
|
trace?.endStage(hardFiltered.length, hardFiltered.map(r => r.score));
|
|
1154
1287
|
|
|
1155
1288
|
// Evolution status filter: exclude superseded/archived/consolidated memories
|
|
@@ -1157,11 +1290,14 @@ export class MemoryRetriever {
|
|
|
1157
1290
|
? hardFiltered
|
|
1158
1291
|
: hardFiltered.filter(r => isActiveMemory(r.entry.metadata));
|
|
1159
1292
|
|
|
1160
|
-
//
|
|
1161
|
-
|
|
1293
|
+
// LA-1: 资格层准入 —— 默认让 durable 层回答,evidence 留给 deja / 显式溯源。
|
|
1294
|
+
// 放在各类 dedup 之前:先定"谁有资格进候选集",再谈多样性与去重。
|
|
1295
|
+
const layerAdmitted = this.applyLayerAdmission(evolutionFiltered, trace);
|
|
1296
|
+
|
|
1297
|
+
trace?.startStage("noise_filter", layerAdmitted.length);
|
|
1162
1298
|
const denoised = this.config.filterNoise
|
|
1163
|
-
? filterNoise(
|
|
1164
|
-
:
|
|
1299
|
+
? filterNoise(layerAdmitted, r => r.entry.text)
|
|
1300
|
+
: layerAdmitted;
|
|
1165
1301
|
trace?.endStage(denoised.length, denoised.map(r => r.score));
|
|
1166
1302
|
|
|
1167
1303
|
// RIF: demote near-duplicate weak results to improve diversity
|
|
@@ -1189,6 +1325,16 @@ export class MemoryRetriever {
|
|
|
1189
1325
|
const final = diversified.slice(0, limit);
|
|
1190
1326
|
trace?.endStage(final.length, final.map(r => r.score));
|
|
1191
1327
|
|
|
1328
|
+
// P-omitted: 从 source-diversity 之前的候选池(clusterDeduped)算 limit 截断数。
|
|
1329
|
+
// 若启用 sourceDiversity,applySourceDiversity 会先把 diversified 压到 limit,
|
|
1330
|
+
// 用 diversified.length 会让 omitted 恒为 0(Codex 复审 P3-1,与 vector-only 同类)。
|
|
1331
|
+
// 注意:clusterDeduped 仍可能含将被 retrieve 层 validity filter 过滤的 expired 项,
|
|
1332
|
+
// 故 omitted 是“上界估计”,输出层用 "up to N" 措辞反映(P3-2)。
|
|
1333
|
+
const omittedByLimit = Math.max(0, clusterDeduped.length - final.length);
|
|
1334
|
+
if (omittedByLimit > 0) {
|
|
1335
|
+
(final as RetrievalResultSet).omitted = { count: omittedByLimit, reason: "limit" };
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1192
1338
|
return final;
|
|
1193
1339
|
}
|
|
1194
1340
|
|
|
@@ -1293,9 +1439,13 @@ export class MemoryRetriever {
|
|
|
1293
1439
|
const objectScore = entityScoreMap.get(triple.object) ?? 0;
|
|
1294
1440
|
const tripleScore = Math.max(subjectScore, objectScore) * triple.confidence;
|
|
1295
1441
|
|
|
1296
|
-
if (tripleScore > 0
|
|
1297
|
-
|
|
1298
|
-
|
|
1442
|
+
if (tripleScore > 0) {
|
|
1443
|
+
// All contributing memories share the fact's score — not just the
|
|
1444
|
+
// first contributor (multi-source triples after mention counting)
|
|
1445
|
+
for (const mid of parseSourceIds(triple.source_memory_ids, triple.source_memory_id)) {
|
|
1446
|
+
const existing = memoryScoreMap.get(mid) ?? 0;
|
|
1447
|
+
memoryScoreMap.set(mid, Math.max(existing, tripleScore));
|
|
1448
|
+
}
|
|
1299
1449
|
}
|
|
1300
1450
|
}
|
|
1301
1451
|
}
|
|
@@ -1648,6 +1798,49 @@ export class MemoryRetriever {
|
|
|
1648
1798
|
return weighted.sort((a, b) => b.score - a.score);
|
|
1649
1799
|
}
|
|
1650
1800
|
|
|
1801
|
+
/** LA-1: 一条结果是否属于 evidence 层(transcript 碎片)。判据与 applyBoundaryWeight 保持一致。 */
|
|
1802
|
+
private isEvidenceLayer(r: RetrievalResult): boolean {
|
|
1803
|
+
const boundary = extractBoundaryMetadata(r.entry.metadata);
|
|
1804
|
+
if (boundary) return boundary.layer === "evidence";
|
|
1805
|
+
return isTranscriptScope(r.entry.scope);
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
/**
|
|
1809
|
+
* LA-1: 资格层准入 —— 决定"哪一层来回答这个 query",而非调整分数。
|
|
1810
|
+
*
|
|
1811
|
+
* 与 applyBoundaryWeight 的区别是本方法存在的全部理由:那个改分数(排序层),
|
|
1812
|
+
* 会让来源盖过相关性(`retriever-boundary-weight.test.ts` 明确禁止这点);
|
|
1813
|
+
* 本方法只挑选候选集,层内相关性排序原封不动。
|
|
1814
|
+
*
|
|
1815
|
+
* durable 命中不足 `layerAdmissionMin` 时回退到全量候选池 —— 新主题在结论层
|
|
1816
|
+
* 往往是空的,宁缺毋滥不能变成什么都不给。
|
|
1817
|
+
*/
|
|
1818
|
+
private applyLayerAdmission(
|
|
1819
|
+
results: RetrievalResult[],
|
|
1820
|
+
trace?: TraceCollector,
|
|
1821
|
+
): RetrievalResult[] {
|
|
1822
|
+
const mode = envConfig.layerAdmission();
|
|
1823
|
+
if (mode === "off" || results.length === 0) return results;
|
|
1824
|
+
|
|
1825
|
+
const durable = results.filter(r => !this.isEvidenceLayer(r));
|
|
1826
|
+
const dropped = results.length - durable.length;
|
|
1827
|
+
const thin = durable.length < envConfig.layerAdmissionMin();
|
|
1828
|
+
|
|
1829
|
+
if (mode === "observe") {
|
|
1830
|
+
logInfo(
|
|
1831
|
+
`[layer-admission] observe: durable=${durable.length} evidence=${dropped} ` +
|
|
1832
|
+
`total=${results.length} action=${thin ? "fallback(keep-all)" : "would-drop-evidence"}`,
|
|
1833
|
+
);
|
|
1834
|
+
return results;
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
if (thin) return results;
|
|
1838
|
+
|
|
1839
|
+
trace?.startStage("layer_admission", results.length);
|
|
1840
|
+
trace?.endStage(durable.length, durable.map(r => r.score));
|
|
1841
|
+
return durable;
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1651
1844
|
private applyBoundaryWeight(results: RetrievalResult[]): RetrievalResult[] {
|
|
1652
1845
|
const weighted = results.map(r => {
|
|
1653
1846
|
const boundary = extractBoundaryMetadata(r.entry.metadata);
|
|
@@ -1718,8 +1911,8 @@ export class MemoryRetriever {
|
|
|
1718
1911
|
|
|
1719
1912
|
const now = Date.now();
|
|
1720
1913
|
const decayed = results.map(r => {
|
|
1721
|
-
// HP-7: Exempt core/pinned/recently-accessed from decay
|
|
1722
|
-
if (isDecayExempt(r.entry.metadata, r.entry.importance)) return r;
|
|
1914
|
+
// HP-7: Exempt core/pinned/recently-accessed (and procedural categories) from decay
|
|
1915
|
+
if (isDecayExempt(r.entry.metadata, r.entry.importance, r.entry.category)) return r;
|
|
1723
1916
|
|
|
1724
1917
|
const ts = (r.entry.timestamp && r.entry.timestamp > 0) ? r.entry.timestamp : now;
|
|
1725
1918
|
const ageDays = (now - ts) / 86_400_000;
|
|
@@ -1736,7 +1929,7 @@ export class MemoryRetriever {
|
|
|
1736
1929
|
}
|
|
1737
1930
|
|
|
1738
1931
|
// Resolve tier from metadata (defaults to peripheral)
|
|
1739
|
-
const tier = resolveTier(r.entry.metadata);
|
|
1932
|
+
const tier = resolveTier(r.entry.metadata, r.entry.importance);
|
|
1740
1933
|
|
|
1741
1934
|
// Weibull decay with tier-specific shape and floor
|
|
1742
1935
|
const factor = weibullDecay(ageDays, halfLife, tier);
|
|
@@ -1751,14 +1944,6 @@ export class MemoryRetriever {
|
|
|
1751
1944
|
return decayed.sort((a, b) => b.score - a.score);
|
|
1752
1945
|
}
|
|
1753
1946
|
|
|
1754
|
-
/**
|
|
1755
|
-
* Blend access-frequency hotness into retrieval scores.
|
|
1756
|
-
*
|
|
1757
|
-
* Formula: final = score * (1 - alpha) + hotness * alpha
|
|
1758
|
-
* where hotness = sigmoid(log1p(accessCount)) * exp(-decayRate * ageDays)
|
|
1759
|
-
*
|
|
1760
|
-
* No-op when hotnessWeight is 0 or AccessTracker is absent.
|
|
1761
|
-
*/
|
|
1762
1947
|
/**
|
|
1763
1948
|
* B-1/E-1: Evolution decay blend — factor in composite decay score
|
|
1764
1949
|
* (time × frequency × importance) as a scoring signal.
|
|
@@ -1767,8 +1952,8 @@ export class MemoryRetriever {
|
|
|
1767
1952
|
private applyEvolutionDecayBlend(results: RetrievalResult[]): RetrievalResult[] {
|
|
1768
1953
|
const EVOLUTION_BLEND_WEIGHT = 0.10;
|
|
1769
1954
|
return results.map(r => {
|
|
1770
|
-
// HP-7: Exempt entries get max decay score (1.0) → no penalty
|
|
1771
|
-
if (isDecayExempt(r.entry.metadata, r.entry.importance)) {
|
|
1955
|
+
// HP-7: Exempt entries (incl. procedural categories) get max decay score (1.0) → no penalty
|
|
1956
|
+
if (isDecayExempt(r.entry.metadata, r.entry.importance, r.entry.category)) {
|
|
1772
1957
|
return {
|
|
1773
1958
|
...r,
|
|
1774
1959
|
score: clamp01(r.score * (1 - EVOLUTION_BLEND_WEIGHT) + 1.0 * EVOLUTION_BLEND_WEIGHT, 0),
|
|
@@ -1831,8 +2016,10 @@ export class MemoryRetriever {
|
|
|
1831
2016
|
const existingIds = new Set(results.map(r => r.entry.id));
|
|
1832
2017
|
for (const n of neighborhood) {
|
|
1833
2018
|
for (const t of n.triples) {
|
|
1834
|
-
|
|
1835
|
-
|
|
2019
|
+
// Expand through ALL contributing memories of each fact, not just
|
|
2020
|
+
// the first contributor (multi-source triples after mention counting)
|
|
2021
|
+
for (const mid of parseSourceIds(t.source_memory_ids, t.source_memory_id)) {
|
|
2022
|
+
if (!existingIds.has(mid)) sourceIds.add(mid);
|
|
1836
2023
|
}
|
|
1837
2024
|
}
|
|
1838
2025
|
}
|
|
@@ -1967,6 +2154,15 @@ export class MemoryRetriever {
|
|
|
1967
2154
|
return [...results, ...siblings].sort((a, b) => b.score - a.score);
|
|
1968
2155
|
}
|
|
1969
2156
|
|
|
2157
|
+
/**
|
|
2158
|
+
* Blend access-frequency hotness into retrieval scores.
|
|
2159
|
+
*
|
|
2160
|
+
* Formula: final = score * (1 - alpha) + hotness * alpha
|
|
2161
|
+
* where hotness = sigmoid(log1p(accessCount)) * exp(-decayRate * ageDays)
|
|
2162
|
+
* Zero-access rows keep their incoming score; accessed rows retain this formula.
|
|
2163
|
+
*
|
|
2164
|
+
* No-op when hotnessWeight is 0 or AccessTracker is absent.
|
|
2165
|
+
*/
|
|
1970
2166
|
private applyHotnessBlend(results: RetrievalResult[]): RetrievalResult[] {
|
|
1971
2167
|
const raw = this.config.hotnessWeight;
|
|
1972
2168
|
const alpha = Math.min(1, Math.max(0, Number.isFinite(raw) ? raw : 0));
|
|
@@ -1978,6 +2174,7 @@ export class MemoryRetriever {
|
|
|
1978
2174
|
accessCount,
|
|
1979
2175
|
lastAccessedAt || r.entry.timestamp,
|
|
1980
2176
|
);
|
|
2177
|
+
if (accessCount === 0) return r;
|
|
1981
2178
|
return {
|
|
1982
2179
|
...r,
|
|
1983
2180
|
score: clamp01(r.score * (1 - alpha) + hotness * alpha, 0),
|