recallnest 3.0.1 → 4.0.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 +203 -0
- package/README.md +42 -17
- package/README_CN.md +31 -15
- package/assets/ui/app.js +6 -1
- package/bin/recallnest.mjs +20 -37
- package/docs/api-reference.md +8 -0
- package/docs/architecture.md +18 -13
- package/eval/cases-canary.json +107 -9
- package/integrations/examples/claude-agent-sdk/memory-agent.ts +2 -0
- package/integrations/examples/langchain/memory-chain.py +4 -0
- package/integrations/examples/openai-agents-sdk/memory-agent.py +4 -0
- package/package.json +10 -2
- package/src/activity-counter.ts +1 -0
- package/src/adaptive-retrieval.ts +23 -0
- package/src/api-server.ts +13 -11
- package/src/asset-sync.ts +26 -2
- package/src/audit-log.ts +2 -0
- package/src/auto-gc.ts +4 -3
- package/src/belief-history.ts +137 -31
- package/src/capture-engine.ts +434 -68
- package/src/cascade-forget.ts +7 -3
- package/src/checkpoint-mirror.ts +417 -0
- package/src/cli.ts +574 -54
- package/src/conflict-engine.ts +43 -15
- package/src/consolidation-engine.ts +24 -3
- package/src/context-collapse-renderer.ts +8 -1
- package/src/context-composer.ts +11 -2
- package/src/distill-lock.ts +246 -74
- package/src/dream-pipeline.ts +3 -1
- package/src/env-config.ts +118 -0
- package/src/flock.ts +97 -0
- package/src/forget-engine.ts +342 -58
- package/src/gateway-server.ts +4 -0
- package/src/ingest-plan.ts +56 -0
- package/src/ingest.ts +69 -104
- package/src/kg-contradiction.ts +753 -0
- package/src/kg-store.ts +1 -1
- package/src/known-item-eval.ts +455 -0
- package/src/llm-client.ts +13 -26
- package/src/llm-consolidation.ts +2 -2
- package/src/mcp-tool-deps.ts +6 -0
- package/src/mcp-tools-advanced.ts +80 -45
- package/src/mcp-tools-core.ts +23 -17
- package/src/mcp-tools-governance.ts +13 -7
- package/src/memory-assets.ts +144 -8
- package/src/memory-boundaries.ts +19 -0
- package/src/memory-evolution.ts +9 -0
- package/src/memory-lint.ts +1 -1
- package/src/memory-output.ts +276 -14
- package/src/memory-reconcile.ts +1437 -0
- package/src/memory-schema.ts +11 -0
- package/src/memory-store-port.ts +1 -0
- package/src/minis-archive.ts +151 -0
- package/src/noise-filter.ts +15 -2
- package/src/pii-detector.ts +31 -1
- package/src/pivot-apply.ts +3 -4
- package/src/preference-matcher.ts +40 -28
- package/src/prospective-memory.ts +7 -4
- package/src/retriever.ts +297 -57
- package/src/retroactive-boost.ts +3 -2
- package/src/runtime-config.ts +6 -1
- package/src/scope-policy.ts +1 -0
- package/src/session-distiller.ts +6 -2
- package/src/session-output.ts +11 -0
- package/src/session-schema.ts +5 -2
- package/src/session-store.ts +57 -9
- package/src/store-receipts.ts +109 -0
- package/src/store-write-budget.ts +37 -0
- package/src/store.ts +298 -57
- package/src/text-fingerprint.ts +25 -0
- package/src/trigger-store.ts +443 -0
- package/src/ui-server.ts +22 -21
- package/src/version-manager.ts +19 -5
- package/src/write-verifier.ts +16 -11
package/src/retriever.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Hybrid Retrieval System
|
|
3
|
-
* Combines vector search + BM25 full-text search with
|
|
3
|
+
* Combines vector search + BM25 full-text search with weighted score fusion
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { MemoryStore, MemorySearchResult } from "./store.js";
|
|
6
|
+
import type { MemoryStore, MemorySearchResult, MemoryEntry } from "./store.js";
|
|
7
7
|
import type { Embedder } from "./embedder.js";
|
|
8
8
|
import { filterNoise } from "./noise-filter.js";
|
|
9
9
|
import * as envConfig from "./env-config.js";
|
|
10
10
|
import { extractErrorSignatures } from "./error-signature.js";
|
|
11
|
-
import {
|
|
11
|
+
import { shouldSkipRetrievalFor } from "./adaptive-retrieval.js";
|
|
12
12
|
import { expandQueryWithAliases } from "./aliases.js";
|
|
13
13
|
import { expandQuery } from "./query-expander.js";
|
|
14
14
|
import { detectLang, tokenizeFts } from "./language-hook.js";
|
|
@@ -16,7 +16,9 @@ import { type AccessTracker, computeHotnessScore, parseAccessMetadata } from "./
|
|
|
16
16
|
import { readUtility } from "./memory-utility.js";
|
|
17
17
|
import { weibullDecay, resolveTier, isDecayExempt, adjustHalfLifeForEmotion, computeArousalBoost } from "./decay-engine.js";
|
|
18
18
|
import { logInfo, logWarn } from "./stderr-log.js";
|
|
19
|
-
import {
|
|
19
|
+
import { matchesScopeFilter } from "./scope-policy.js";
|
|
20
|
+
import type { TriggerStore, TriggerHit } from "./trigger-store.js";
|
|
21
|
+
import { extractBoundaryMetadata, isDurableMemoryScope, isTranscriptScope, resolveExcludedScopes } from "./memory-boundaries.js";
|
|
20
22
|
import type { TraceCollector } from "./retrieval-trace.js";
|
|
21
23
|
import { extractTopicTag } from "./topic-tag.js";
|
|
22
24
|
import { filterInterference } from "./rif.js";
|
|
@@ -24,7 +26,7 @@ import { FrequencyTracker } from "./frequency-tracker.js";
|
|
|
24
26
|
import { applyConfidenceWeight } from "./confidence-tracker.js";
|
|
25
27
|
import { deduplicateByVersionGroup } from "./version-manager.js";
|
|
26
28
|
import { deduplicateByClusterInsight } from "./consolidation-engine.js";
|
|
27
|
-
import { isActiveMemory,
|
|
29
|
+
import { isActiveMemory, recordAccessOnMeta as recordEvolutionAccessOnMeta, parseEvolution, computeDecayScore } from "./memory-evolution.js";
|
|
28
30
|
import {
|
|
29
31
|
isMultiVectorEnabled,
|
|
30
32
|
extractMultiVectorText,
|
|
@@ -125,6 +127,11 @@ export interface RetrievalConfig {
|
|
|
125
127
|
* 注释不会有人定期回来读,那个条目每窗口都会被推到眼前。
|
|
126
128
|
*/
|
|
127
129
|
utilityWeight: number;
|
|
130
|
+
/**
|
|
131
|
+
* γ:`RECALLNEST_POPULARITY_RANKING=bounded` 下链尾那一步流行度加成的上限,
|
|
132
|
+
* score × (1 + γ · h),h ∈ [0,1]。legacy 下不读。(default: 0.2,第二步 plan §一.4)
|
|
133
|
+
*/
|
|
134
|
+
popularityBonusMax: number;
|
|
128
135
|
/**
|
|
129
136
|
* Per-category score thresholds. When a result's category matches a key,
|
|
130
137
|
* that threshold is used instead of hardMinScore. Categories not listed
|
|
@@ -173,6 +180,12 @@ export interface RetrievalContext {
|
|
|
173
180
|
* calls to prevent auto-recall from strengthening noise memories.
|
|
174
181
|
* Defaults to "manual" when unset (backward-compatible). */
|
|
175
182
|
source?: "manual" | "auto-recall" | "cli";
|
|
183
|
+
/**
|
|
184
|
+
* Whether the trivial-query gate (shouldSkipRetrieval: greetings, command-like prefixes, short
|
|
185
|
+
* non-CJK phrases …) applies. Default: only when source is "auto-recall". Explicit searches that
|
|
186
|
+
* use source "auto-recall" just to avoid reinforcement writes pass false.
|
|
187
|
+
*/
|
|
188
|
+
trivialQueryGate?: boolean;
|
|
176
189
|
/** When false (default), archived records are excluded from results. */
|
|
177
190
|
includeArchived?: boolean;
|
|
178
191
|
/** Optional trace collector for per-stage pipeline observability. */
|
|
@@ -189,6 +202,11 @@ export interface RetrievalContext {
|
|
|
189
202
|
validAt?: number;
|
|
190
203
|
/** F3: When true, include expired memories (demoted 80%). Default: false. */
|
|
191
204
|
includeExpired?: boolean;
|
|
205
|
+
/**
|
|
206
|
+
* Scopes kept out of the candidate pool (pushed down into the vector / BM25 queries). A category
|
|
207
|
+
* filter other than `events` also excludes the checkpoint-mirror scope — see resolveExcludedScopes.
|
|
208
|
+
*/
|
|
209
|
+
excludeScopes?: string[];
|
|
192
210
|
}
|
|
193
211
|
|
|
194
212
|
export interface RetrievalResult extends MemorySearchResult {
|
|
@@ -200,6 +218,11 @@ export interface RetrievalResult extends MemorySearchResult {
|
|
|
200
218
|
reranked?: { score: number };
|
|
201
219
|
/** HP-narrative: true when this result was pulled as a narrative sibling */
|
|
202
220
|
narrativeSibling?: boolean;
|
|
221
|
+
/**
|
|
222
|
+
* T-Mem 写入时预演触发器(2026-09-22):经 trigger 到达或被 trigger 加强。
|
|
223
|
+
* text 只用于 explain(「为什么捞到」),不进 context 渲染——trigger 是入口不是证据。
|
|
224
|
+
*/
|
|
225
|
+
trigger?: { score: number; cosine: number; text: string; admittedBy?: "hard" | "soft"; overlap?: number };
|
|
203
226
|
};
|
|
204
227
|
}
|
|
205
228
|
|
|
@@ -238,7 +261,10 @@ export const DEFAULT_CATEGORY_MIN_SCORES: Record<string, number> = {
|
|
|
238
261
|
// P1.2(2026-06-25):默认 vector-only。canary eval 实测个人记忆库 vector 召回质量全面优于
|
|
239
262
|
// hybrid(robustness 带 scope WITH 6/12 + 不带 scope WITHOUT 10/12,完胜 hybrid 的 3/9;BM25 在
|
|
240
263
|
// 高度同质的个人记忆里是净噪声,会把 vector 本应 top 的目标如 dbcb317a 挤掉)。改频率层救低频
|
|
241
|
-
// 目标实测是死结(动 hotness 必伤靠 hotness
|
|
264
|
+
// 目标实测是死结(动 hotness 必伤靠 hotness 上位的记忆)。〔2026-09-25:这个死结现在有一条开关后的出路——
|
|
265
|
+
// RECALLNEST_POPULARITY_RANKING=bounded 把三个流行度加成合成一步、只加不减、上限 +20%,快照上靠热度上位的
|
|
266
|
+
// canary rebuild-pool 仍过(γ=0.1 时掉到第 5,所以取 0.2);正式 shadow 之后 2026-09-25 切成默认,
|
|
267
|
+
// 设 RECALLNEST_POPULARITY_RANKING=legacy 可回旧行为。见 sync-bridge AI产出/2026-09-24-recallnest-新条目排序/。〕hybrid 路径代码全保留,mode 改回
|
|
242
268
|
// "hybrid" 即可切换(注:multiHopExpand 仍硬编码走 hybrid,默认 off,config.retrieval.multiHop
|
|
243
269
|
// 开了会破纯 vector)。数据盲区:canary 全是语义 query,未覆盖 BM25 强项的精确关键词/token 召回
|
|
244
270
|
// (变量名 / commit hash / alias-map 昵称)——待补 exact-token canary 验证。
|
|
@@ -278,6 +304,7 @@ export const DEFAULT_RETRIEVAL_CONFIG: RetrievalConfig = {
|
|
|
278
304
|
timeDecayHalfLifeDays: 60,
|
|
279
305
|
hotnessWeight: 0,
|
|
280
306
|
utilityWeight: 0,
|
|
307
|
+
popularityBonusMax: 0.2,
|
|
281
308
|
};
|
|
282
309
|
|
|
283
310
|
/**
|
|
@@ -399,6 +426,21 @@ function applyAnchorBoost(results: RetrievalResult[], query: string): RetrievalR
|
|
|
399
426
|
});
|
|
400
427
|
}
|
|
401
428
|
|
|
429
|
+
/**
|
|
430
|
+
* T-Mem triggers:hybrid 路径 rerank 之后的地板。cross-encoder 按「query 与正文像不像」打分,
|
|
431
|
+
* 而经 trigger 到达的宿主与 query 恰恰是零词面重合的——不设地板就等于把 T-Mem 自己要对抗的
|
|
432
|
+
* 相似度体制重新加回来(论文原话)。地板取 trigger 分的 0.9,只托底不抬头。
|
|
433
|
+
*/
|
|
434
|
+
const TRIGGER_FLOOR_FACTOR = 0.9;
|
|
435
|
+
function applyTriggerFloor(results: RetrievalResult[]): RetrievalResult[] {
|
|
436
|
+
return results.map(r => {
|
|
437
|
+
const t = r.sources.trigger;
|
|
438
|
+
if (!t) return r;
|
|
439
|
+
const floor = t.score * TRIGGER_FLOOR_FACTOR;
|
|
440
|
+
return r.score >= floor ? r : { ...r, score: floor };
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
402
444
|
/** Multiplicative boost applied to a case on exact error-signature match. */
|
|
403
445
|
const ERROR_SIGNATURE_BOOST_FACTOR = 1.5;
|
|
404
446
|
|
|
@@ -409,9 +451,18 @@ const ERROR_SIGNATURE_BOOST_FACTOR = 1.5;
|
|
|
409
451
|
* (default) returns the input unchanged — bit-identical, so the pure-move eval baseline holds.
|
|
410
452
|
* Only affects category="cases"; read and query side share extractErrorSignatures so the
|
|
411
453
|
* signatures compare apples-to-apples.
|
|
454
|
+
*
|
|
455
|
+
* `clamp: false`(bounded 流行度下用):不在这里截到 1——链尾的分数此时可以超过 1,
|
|
456
|
+
* 在这里截会把精确命中的 1.14 压成 1.00、排到未命中的 1.08 之后(第二步 R1 Codex C2)。
|
|
457
|
+
* 截平统一在 retrieve() 出口做。legacy 调用处不传参,行为不变。
|
|
412
458
|
*/
|
|
413
|
-
export function applyErrorSignatureBoost(
|
|
459
|
+
export function applyErrorSignatureBoost(
|
|
460
|
+
results: RetrievalResult[],
|
|
461
|
+
query: string,
|
|
462
|
+
opts: { clamp?: boolean } = {},
|
|
463
|
+
): RetrievalResult[] {
|
|
414
464
|
if (!envConfig.errorSignatureBoost()) return results;
|
|
465
|
+
const clamp = opts.clamp ?? true;
|
|
415
466
|
const querySignatures = extractErrorSignatures({ problem: query });
|
|
416
467
|
if (querySignatures.length === 0) return results;
|
|
417
468
|
const querySet = new Set(querySignatures.map(s => s.toLowerCase()));
|
|
@@ -421,7 +472,8 @@ export function applyErrorSignatureBoost(results: RetrievalResult[], query: stri
|
|
|
421
472
|
if (!Array.isArray(sigs) || sigs.length === 0) return r;
|
|
422
473
|
const hit = sigs.some(s => typeof s === "string" && querySet.has(s.toLowerCase()));
|
|
423
474
|
if (!hit) return r;
|
|
424
|
-
|
|
475
|
+
const boosted = r.score * ERROR_SIGNATURE_BOOST_FACTOR;
|
|
476
|
+
return { ...r, score: clamp ? Math.min(1, Math.max(0, boosted)) : Math.max(0, boosted) };
|
|
425
477
|
});
|
|
426
478
|
}
|
|
427
479
|
|
|
@@ -741,6 +793,7 @@ export class MemoryRetriever {
|
|
|
741
793
|
private frequencyTracker?: FrequencyTracker;
|
|
742
794
|
private auditLogger?: AuditLogger;
|
|
743
795
|
private llmClient?: ReconstructionLLMClient;
|
|
796
|
+
private triggerStore?: TriggerStore;
|
|
744
797
|
/**
|
|
745
798
|
* Session-scoped suppression list (dopamine-inspired "do not disturb").
|
|
746
799
|
* IDs in this set get a score penalty during retrieval but are NOT deleted.
|
|
@@ -764,6 +817,11 @@ export class MemoryRetriever {
|
|
|
764
817
|
this.kgStore = kgStore;
|
|
765
818
|
}
|
|
766
819
|
|
|
820
|
+
/** T-Mem triggers(2026-09-22):挂上侧表后,检索多一条「经 trigger 到达」的入口。 */
|
|
821
|
+
setTriggerStore(triggerStore: TriggerStore): void {
|
|
822
|
+
this.triggerStore = triggerStore;
|
|
823
|
+
}
|
|
824
|
+
|
|
767
825
|
/** P0.2: Attach a FrequencyTracker for hit-count based boosting. */
|
|
768
826
|
setFrequencyTracker(tracker: FrequencyTracker): void {
|
|
769
827
|
this.frequencyTracker = tracker;
|
|
@@ -810,9 +868,12 @@ export class MemoryRetriever {
|
|
|
810
868
|
async retrieve(context: RetrievalContext): Promise<RetrievalResultSet> {
|
|
811
869
|
const { query: rawQuery, limit, scopeFilter, category, includeArchived, trace, graph } = context;
|
|
812
870
|
const safeLimit = clampInt(limit, 1, 20);
|
|
871
|
+
const excluded = resolveExcludedScopes(context);
|
|
872
|
+
const excludeScopes = excluded.length > 0 ? excluded : undefined;
|
|
813
873
|
|
|
814
|
-
// Adaptive retrieval: skip trivial queries to save embedding API calls
|
|
815
|
-
|
|
874
|
+
// Adaptive retrieval: skip trivial queries to save embedding API calls — auto-recall only;
|
|
875
|
+
// explicit queries skip just empty / whitespace (see shouldSkipRetrievalFor).
|
|
876
|
+
if (shouldSkipRetrievalFor(context)) {
|
|
816
877
|
return [];
|
|
817
878
|
}
|
|
818
879
|
|
|
@@ -834,19 +895,19 @@ export class MemoryRetriever {
|
|
|
834
895
|
|
|
835
896
|
// For vector-only mode, use legacy behavior
|
|
836
897
|
if (this.config.mode === "vector" || !this.store.hasFtsSupport) {
|
|
837
|
-
results = await this.vectorOnlyRetrieval(query, safeLimit, scopeFilter, category, includeArchived, trace);
|
|
898
|
+
results = await this.vectorOnlyRetrieval(query, safeLimit, scopeFilter, category, includeArchived, trace, undefined, excludeScopes);
|
|
838
899
|
} else {
|
|
839
|
-
// Hybrid retrieval with vector + BM25
|
|
840
|
-
results = await this.hybridRetrieval(query, safeLimit, scopeFilter, category, includeArchived, trace, graph);
|
|
900
|
+
// Hybrid retrieval with vector + BM25 weighted score fusion (+ optional PPR graph)
|
|
901
|
+
results = await this.hybridRetrieval(query, safeLimit, scopeFilter, category, includeArchived, trace, graph, undefined, excludeScopes);
|
|
841
902
|
}
|
|
842
903
|
|
|
843
904
|
// 0-hit fallback: minScore filter 把所有候选过滤光时,放宽阈值(=0)重试一次。
|
|
844
905
|
// 只在原 path 0 hit 触发;正常召回不受影响。
|
|
845
906
|
if (results.length === 0) {
|
|
846
907
|
if (this.config.mode === "vector" || !this.store.hasFtsSupport) {
|
|
847
|
-
results = await this.vectorOnlyRetrieval(query, safeLimit, scopeFilter, category, includeArchived, trace, 0);
|
|
908
|
+
results = await this.vectorOnlyRetrieval(query, safeLimit, scopeFilter, category, includeArchived, trace, 0, excludeScopes);
|
|
848
909
|
} else {
|
|
849
|
-
results = await this.hybridRetrieval(query, safeLimit, scopeFilter, category, includeArchived, trace, graph, 0);
|
|
910
|
+
results = await this.hybridRetrieval(query, safeLimit, scopeFilter, category, includeArchived, trace, graph, 0, excludeScopes);
|
|
850
911
|
}
|
|
851
912
|
}
|
|
852
913
|
|
|
@@ -859,7 +920,7 @@ export class MemoryRetriever {
|
|
|
859
920
|
// 字符串匹配,是 embedding 本质,跟语言无关)。普通词(mem0/recallnest/GitHub)不触发,日常
|
|
860
921
|
// 语义召回不受影响。
|
|
861
922
|
if (hasExactToken(rawQuery)) {
|
|
862
|
-
results = await this.supplementExactTokenWithBm25(query, results, safeLimit, scopeFilter, category, includeArchived);
|
|
923
|
+
results = await this.supplementExactTokenWithBm25(query, results, safeLimit, scopeFilter, category, includeArchived, excludeScopes);
|
|
863
924
|
}
|
|
864
925
|
|
|
865
926
|
// LME-2: Multi-hop retrieval — extract entities from first-pass results,
|
|
@@ -886,6 +947,14 @@ export class MemoryRetriever {
|
|
|
886
947
|
});
|
|
887
948
|
}
|
|
888
949
|
|
|
950
|
+
// 第二步(bounded 流行度):检索器内部一律按原值排序,出口才截到 1——下游阈值与显示仍在 [0,1]。
|
|
951
|
+
// 必须在 BM25 补偿 / multiHop / topicTag / 有效期过滤之后:multiHopExpand 会按分数重排再截取,
|
|
952
|
+
// 截早了首轮 1.08 与跟进 1.14 都成 1.0、留下的是首轮那条(第二步 R3 Codex N2)。
|
|
953
|
+
// 也必须在下面挂 omitted / reconstruction 之前:那两个属性是之后才挂上的,这里 map 不会丢(R1 Agy A3)。
|
|
954
|
+
if (envConfig.popularityRanking() === "bounded" && results.length > 0) {
|
|
955
|
+
results = results.map(r => (r.score > 1 ? { ...r, score: 1 } : r));
|
|
956
|
+
}
|
|
957
|
+
|
|
889
958
|
// P0.2: Record frequency hits for returned results (manual queries only)
|
|
890
959
|
// Moved after topicTag filter so filtered-out entries are not reinforced.
|
|
891
960
|
if (this.frequencyTracker && results.length > 0 && context.source !== "auto-recall") {
|
|
@@ -894,14 +963,15 @@ export class MemoryRetriever {
|
|
|
894
963
|
|
|
895
964
|
// A-3: Record evolution access counts (async, non-blocking)
|
|
896
965
|
// Moved after topicTag filter so filtered-out entries are not reinforced.
|
|
897
|
-
|
|
966
|
+
// 2026-09-24:一批行在写锁内读最新元数据再加计数(patchMetadataBatch,一次提交)。原先拿检索时读到的元数据
|
|
967
|
+
// 整行 update 回去,检索与写回之间别的进程写下的状态(对账下架、dream 合并、GC 归档)会被旧值盖掉。
|
|
968
|
+
if (results.length > 0 && context.source !== "auto-recall" && typeof this.store.patchMetadataBatch === "function") {
|
|
969
|
+
const ids = results.map((r) => r.entry.id);
|
|
898
970
|
Promise.resolve().then(async () => {
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
} catch (err) { console.error("[recallnest] Evolution access tracking failed:", err instanceof Error ? err.message : String(err)); }
|
|
904
|
-
}
|
|
971
|
+
try {
|
|
972
|
+
const now = Date.now();
|
|
973
|
+
await this.store.patchMetadataBatch(ids.map((id) => ({ id, patchFn: (meta) => recordEvolutionAccessOnMeta(meta, now) })));
|
|
974
|
+
} catch (err) { console.error("[recallnest] Evolution access tracking failed:", err instanceof Error ? err.message : String(err)); }
|
|
905
975
|
});
|
|
906
976
|
}
|
|
907
977
|
|
|
@@ -989,10 +1059,11 @@ export class MemoryRetriever {
|
|
|
989
1059
|
scopeFilter?: string[],
|
|
990
1060
|
category?: string,
|
|
991
1061
|
includeArchived?: boolean,
|
|
1062
|
+
excludeScopes?: string[],
|
|
992
1063
|
): Promise<RetrievalResult[]> {
|
|
993
1064
|
if (!this.store.hasFtsSupport) return vectorResults;
|
|
994
1065
|
const ftsQuery = tokenizeFts(expandQuery(query), detectLang(query));
|
|
995
|
-
const bm25 = await this.runBM25Search(ftsQuery, Math.max(limit, 5), scopeFilter, category, includeArchived);
|
|
1066
|
+
const bm25 = await this.runBM25Search(ftsQuery, Math.max(limit, 5), scopeFilter, category, includeArchived, excludeScopes);
|
|
996
1067
|
const vecIds = new Set(vectorResults.map(r => r.entry.id));
|
|
997
1068
|
const supplements = bm25
|
|
998
1069
|
.filter(b => !vecIds.has(b.entry.id))
|
|
@@ -1038,6 +1109,8 @@ export class MemoryRetriever {
|
|
|
1038
1109
|
context.includeArchived,
|
|
1039
1110
|
undefined, // no trace for follow-up
|
|
1040
1111
|
context.graph,
|
|
1112
|
+
undefined,
|
|
1113
|
+
resolveExcludedScopes(context).length > 0 ? resolveExcludedScopes(context) : undefined,
|
|
1041
1114
|
).catch(() => [] as RetrievalResult[]),
|
|
1042
1115
|
);
|
|
1043
1116
|
const followUpResults = await Promise.all(followUpPromises);
|
|
@@ -1062,6 +1135,74 @@ export class MemoryRetriever {
|
|
|
1062
1135
|
.slice(0, context.limit);
|
|
1063
1136
|
}
|
|
1064
1137
|
|
|
1138
|
+
/**
|
|
1139
|
+
* T-Mem 写入时预演触发器召回(2026-09-22,借自腾讯 T-Mem / EMNLP 2026)。
|
|
1140
|
+
*
|
|
1141
|
+
* 用 query 向量搜 memory_triggers 侧表,命中的 trigger 把宿主拉进候选池:
|
|
1142
|
+
* - 宿主已在候选里 → 分数取 max(原分, trigger 分),并记 sources.trigger;
|
|
1143
|
+
* - 宿主不在候选里 → 按 id 取回主表行,以 trigger 分入池(T-Mem 的「union 进候选、绕过相似度闸门」)。
|
|
1144
|
+
* 之后照常走 applySharedScoring / finalizeResults——trigger 只决定「能不能到达」,不另起一套分。
|
|
1145
|
+
* 硬闸 RECALLNEST_TRIGGER_GATE(余弦)防噪;侧表缺失或查询失败一律 fail-open 回原候选。
|
|
1146
|
+
* category / archived / scope 三道过滤与主路径同款,trigger 不能越权。
|
|
1147
|
+
*/
|
|
1148
|
+
private async applyTriggerRecall(
|
|
1149
|
+
candidates: RetrievalResult[],
|
|
1150
|
+
queryVector: number[],
|
|
1151
|
+
queryText: string,
|
|
1152
|
+
scopeFilter?: string[],
|
|
1153
|
+
category?: string,
|
|
1154
|
+
includeArchived?: boolean,
|
|
1155
|
+
trace?: TraceCollector,
|
|
1156
|
+
): Promise<RetrievalResult[]> {
|
|
1157
|
+
if (!this.triggerStore || !envConfig.triggerRecall()) return candidates;
|
|
1158
|
+
trace?.startStage("trigger_recall", candidates.length);
|
|
1159
|
+
let hits: TriggerHit[] = [];
|
|
1160
|
+
try {
|
|
1161
|
+
hits = await this.triggerStore.search(queryVector, envConfig.triggerTopK(), scopeFilter, {
|
|
1162
|
+
minCosine: envConfig.triggerGate(),
|
|
1163
|
+
softCosine: envConfig.triggerSoftGate(),
|
|
1164
|
+
minOverlap: envConfig.triggerMinOverlap(),
|
|
1165
|
+
minQueryTokens: envConfig.triggerSoftMinQueryTokens(),
|
|
1166
|
+
queryText,
|
|
1167
|
+
});
|
|
1168
|
+
} catch (err) {
|
|
1169
|
+
logWarn("trigger recall failed, continuing without it:", err);
|
|
1170
|
+
}
|
|
1171
|
+
if (hits.length === 0) {
|
|
1172
|
+
trace?.endStage(candidates.length, candidates.map(r => r.score));
|
|
1173
|
+
return candidates;
|
|
1174
|
+
}
|
|
1175
|
+
const byId = new Map(candidates.map((r, i) => [r.entry.id, i]));
|
|
1176
|
+
const out = [...candidates];
|
|
1177
|
+
for (const hit of hits) {
|
|
1178
|
+
const triggerSource = { score: hit.score, cosine: hit.cosine, text: hit.text, admittedBy: hit.admittedBy, overlap: hit.overlap };
|
|
1179
|
+
const idx = byId.get(hit.memoryId);
|
|
1180
|
+
if (idx !== undefined) {
|
|
1181
|
+
const existing = out[idx];
|
|
1182
|
+
out[idx] = {
|
|
1183
|
+
...existing,
|
|
1184
|
+
score: Math.max(existing.score, hit.score),
|
|
1185
|
+
sources: { ...existing.sources, trigger: triggerSource },
|
|
1186
|
+
};
|
|
1187
|
+
continue;
|
|
1188
|
+
}
|
|
1189
|
+
let entry: MemoryEntry | null = null;
|
|
1190
|
+
try {
|
|
1191
|
+
entry = await this.store.getById(hit.memoryId);
|
|
1192
|
+
} catch (err) {
|
|
1193
|
+
logWarn(`trigger host lookup failed for ${hit.memoryId}:`, err);
|
|
1194
|
+
}
|
|
1195
|
+
if (!entry) continue;
|
|
1196
|
+
if (!matchesScopeFilter(entry.scope, scopeFilter)) continue;
|
|
1197
|
+
if (category && entry.category !== category) continue;
|
|
1198
|
+
if (!includeArchived && parseMetadata(entry.metadata).archived === true) continue;
|
|
1199
|
+
out.push({ entry, score: hit.score, sources: { trigger: triggerSource } });
|
|
1200
|
+
byId.set(entry.id, out.length - 1);
|
|
1201
|
+
}
|
|
1202
|
+
trace?.endStage(out.length, out.map(r => r.score));
|
|
1203
|
+
return out;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1065
1206
|
private async vectorOnlyRetrieval(
|
|
1066
1207
|
query: string,
|
|
1067
1208
|
limit: number,
|
|
@@ -1070,6 +1211,7 @@ export class MemoryRetriever {
|
|
|
1070
1211
|
includeArchived?: boolean,
|
|
1071
1212
|
trace?: TraceCollector,
|
|
1072
1213
|
minScoreOverride?: number,
|
|
1214
|
+
excludeScopes?: string[],
|
|
1073
1215
|
): Promise<RetrievalResult[]> {
|
|
1074
1216
|
// Temporal reasoning for vector-only mode
|
|
1075
1217
|
const temporal = parseTemporalQuery(query);
|
|
@@ -1090,7 +1232,7 @@ export class MemoryRetriever {
|
|
|
1090
1232
|
// plus a few more local scoring passes, no extra API; P1.2's vector-only savings come from
|
|
1091
1233
|
// skipping hybrid BM25/fusion/rerank, not from fetch size.
|
|
1092
1234
|
const fetchLimit = Math.max(limit, this.config.candidatePoolSize ?? limit);
|
|
1093
|
-
const results = await this.store.vectorSearch(queryVector, fetchLimit, vectorMinScore, scopeFilter);
|
|
1235
|
+
const results = await this.store.vectorSearch(queryVector, fetchLimit, vectorMinScore, scopeFilter, undefined, excludeScopes);
|
|
1094
1236
|
trace?.endStage(results.length, results.map(r => r.score));
|
|
1095
1237
|
|
|
1096
1238
|
// Filter by category if specified
|
|
@@ -1118,7 +1260,10 @@ export class MemoryRetriever {
|
|
|
1118
1260
|
// P0.1: Anchor boost for vector-only path
|
|
1119
1261
|
const anchorBoosted = applyAnchorBoost(mapped, searchQuery);
|
|
1120
1262
|
|
|
1121
|
-
|
|
1263
|
+
// T-Mem triggers:联想入口在这里并入候选(相似度那一步之后、打分链之前)
|
|
1264
|
+
const withTriggers = await this.applyTriggerRecall(anchorBoosted, queryVector, searchQuery, scopeFilter, category, includeArchived, trace);
|
|
1265
|
+
|
|
1266
|
+
const scored = this.applySharedScoring(withTriggers, query, trace);
|
|
1122
1267
|
|
|
1123
1268
|
// Session suppression: temporarily penalize "do not disturb" memories (vector-only path)
|
|
1124
1269
|
const afterSuppression = this.applySessionSuppression(scored);
|
|
@@ -1141,6 +1286,7 @@ export class MemoryRetriever {
|
|
|
1141
1286
|
trace?: TraceCollector,
|
|
1142
1287
|
graph?: boolean,
|
|
1143
1288
|
minScoreOverride?: number,
|
|
1289
|
+
excludeScopes?: string[],
|
|
1144
1290
|
): Promise<RetrievalResult[]> {
|
|
1145
1291
|
// Adaptive pool: widen candidate pool for aggregation queries ("how many", "all the")
|
|
1146
1292
|
const multiplier = (this.config.adaptivePoolMultiplier ?? 1) > 1 && isAggregationQuery(query)
|
|
@@ -1170,8 +1316,8 @@ export class MemoryRetriever {
|
|
|
1170
1316
|
|
|
1171
1317
|
trace?.startStage("vector_search", 0);
|
|
1172
1318
|
const [vectorResults, bm25Results, pprResults] = await Promise.all([
|
|
1173
|
-
this.runVectorSearch(queryVector, candidatePoolSize, scopeFilter, category, includeArchived),
|
|
1174
|
-
this.runBM25Search(ftsQuery, candidatePoolSize, scopeFilter, category, includeArchived),
|
|
1319
|
+
this.runVectorSearch(queryVector, candidatePoolSize, scopeFilter, category, includeArchived, excludeScopes),
|
|
1320
|
+
this.runBM25Search(ftsQuery, candidatePoolSize, scopeFilter, category, includeArchived, excludeScopes),
|
|
1175
1321
|
pprPromise,
|
|
1176
1322
|
]);
|
|
1177
1323
|
trace?.endStage(vectorResults.length, vectorResults.map(r => r.score));
|
|
@@ -1181,6 +1327,7 @@ export class MemoryRetriever {
|
|
|
1181
1327
|
|
|
1182
1328
|
// Fuse results using weighted score fusion (async: validates BM25-only entries exist in store)
|
|
1183
1329
|
const shortQ = isShortQuery(searchQuery);
|
|
1330
|
+
// Stage name "rrf_fusion" is historical and kept for log compatibility; fuseResults() does weighted score fusion, not RRF.
|
|
1184
1331
|
trace?.startStage("rrf_fusion", vectorResults.length + bm25Results.length + pprResults.length);
|
|
1185
1332
|
const fusedResults = await this.fuseResults(vectorResults, bm25Results, pprResults, shortQ);
|
|
1186
1333
|
trace?.endStage(fusedResults.length, fusedResults.map(r => r.score));
|
|
@@ -1202,8 +1349,11 @@ export class MemoryRetriever {
|
|
|
1202
1349
|
const anchorBoosted = applyAnchorBoost(multiVecBlended, searchQuery);
|
|
1203
1350
|
trace?.endStage(anchorBoosted.length, anchorBoosted.map(r => r.score));
|
|
1204
1351
|
|
|
1352
|
+
// T-Mem triggers:联想入口并入候选,在 min_score / rerank 之前
|
|
1353
|
+
const withTriggers = await this.applyTriggerRecall(anchorBoosted, queryVector, searchQuery, scopeFilter, category, includeArchived, trace);
|
|
1354
|
+
|
|
1205
1355
|
// Apply minimum score threshold
|
|
1206
|
-
trace?.startStage("min_score_filter",
|
|
1356
|
+
trace?.startStage("min_score_filter", withTriggers.length);
|
|
1207
1357
|
// P0.1: Lower minScore for short queries to widen the candidate pool;
|
|
1208
1358
|
// minScoreOverride 用于 retrieve() 顶层的 0-hit fallback retry。
|
|
1209
1359
|
const effectiveMinScore = minScoreOverride !== undefined
|
|
@@ -1211,7 +1361,7 @@ export class MemoryRetriever {
|
|
|
1211
1361
|
: (isShortQuery(searchQuery)
|
|
1212
1362
|
? this.config.minScore * SHORT_QUERY_MIN_SCORE_FACTOR
|
|
1213
1363
|
: this.config.minScore);
|
|
1214
|
-
const filtered =
|
|
1364
|
+
const filtered = withTriggers.filter(r => r.score >= effectiveMinScore);
|
|
1215
1365
|
trace?.endStage(filtered.length, filtered.map(r => r.score));
|
|
1216
1366
|
|
|
1217
1367
|
// Rerank if enabled
|
|
@@ -1221,7 +1371,10 @@ export class MemoryRetriever {
|
|
|
1221
1371
|
: filtered;
|
|
1222
1372
|
trace?.endStage(reranked.length, reranked.map(r => r.score));
|
|
1223
1373
|
|
|
1224
|
-
|
|
1374
|
+
// T-Mem triggers:rerank 是相似度打分,经 trigger 到达的宿主要托底
|
|
1375
|
+
const triggerFloored = applyTriggerFloor(reranked);
|
|
1376
|
+
|
|
1377
|
+
const scored = this.applySharedScoring(triggerFloored, query, trace, TRIGGER_FLOOR_FACTOR);
|
|
1225
1378
|
|
|
1226
1379
|
return this.finalizeResults(scored, {
|
|
1227
1380
|
shortQ,
|
|
@@ -1238,11 +1391,69 @@ export class MemoryRetriever {
|
|
|
1238
1391
|
* `query` feeds the emotion stage; `trace` carries per-stage trace spans.
|
|
1239
1392
|
* Path-specific stages stay in the caller: anchor / min_score / rerank / multi-vector
|
|
1240
1393
|
* blend run BEFORE this; vector-only session suppression runs AFTER this.
|
|
1394
|
+
*
|
|
1395
|
+
* `triggerPathBase`:开关 2 下「trigger 那一路」以 trigger 分的多少倍为底——vector 路径 1;
|
|
1396
|
+
* hybrid 路径传 TRIGGER_FLOOR_FACTOR,与 applyTriggerFloor 给 rerank 之后托的底同口径。
|
|
1241
1397
|
*/
|
|
1242
1398
|
private applySharedScoring(
|
|
1243
1399
|
results: RetrievalResult[],
|
|
1244
1400
|
query: string,
|
|
1245
1401
|
trace?: TraceCollector,
|
|
1402
|
+
triggerPathBase = 1,
|
|
1403
|
+
): RetrievalResult[] {
|
|
1404
|
+
const assetWeighted = this.applyPreLengthStages(results, query, trace);
|
|
1405
|
+
|
|
1406
|
+
trace?.startStage("length_norm", assetWeighted.length);
|
|
1407
|
+
const normalized = this.applyLengthNormalization(assetWeighted);
|
|
1408
|
+
// 开关 2(2026-09-25 起默认开;设 false 时原样):trigger 那一路不做长度归一,与正文那一路取大
|
|
1409
|
+
const lengthNormalized = envConfig.triggerLengthExempt()
|
|
1410
|
+
? this.applyTriggerPathFloor(normalized, results, query, triggerPathBase)
|
|
1411
|
+
: normalized;
|
|
1412
|
+
trace?.endStage(lengthNormalized.length, lengthNormalized.map(r => r.score));
|
|
1413
|
+
|
|
1414
|
+
trace?.startStage("time_decay", lengthNormalized.length);
|
|
1415
|
+
const timeDecayed = this.applyTimeDecay(lengthNormalized);
|
|
1416
|
+
trace?.endStage(timeDecayed.length, timeDecayed.map(r => r.score));
|
|
1417
|
+
|
|
1418
|
+
// B-1/E-1: Evolution decay blend — boost memories with high composite decay score
|
|
1419
|
+
const evolutionBlended = this.applyEvolutionDecayBlend(timeDecayed);
|
|
1420
|
+
|
|
1421
|
+
if (envConfig.popularityRanking() === "bounded") {
|
|
1422
|
+
// 流行度只进排序、只加不减、有上限、只读一个计数器、只施加一次(第二步 plan §零.1):
|
|
1423
|
+
// 下面 legacy 的访问计数加成、热度混合、频次加成三环都不跑。这一环与错误指纹加成都不截平,
|
|
1424
|
+
// 分数可以超过 1——检索器内部按原值排序,retrieve() 出口才截到 1。
|
|
1425
|
+
trace?.startStage("popularity_bonus", evolutionBlended.length);
|
|
1426
|
+
const bonused = this.applyPopularityBonus(evolutionBlended);
|
|
1427
|
+
trace?.endStage(bonused.length, bonused.map(r => r.score));
|
|
1428
|
+
return applyErrorSignatureBoost(bonused, query, { clamp: false });
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
// E-1: Access count boost — memories retrieved more often get a score nudge
|
|
1432
|
+
const accessBoosted = this.applyAccessCountBoost(evolutionBlended);
|
|
1433
|
+
|
|
1434
|
+
// Hotness blend: boost frequently + recently accessed memories
|
|
1435
|
+
trace?.startStage("hotness_blend", accessBoosted.length);
|
|
1436
|
+
const hotnessBlended = this.applyHotnessBlend(accessBoosted);
|
|
1437
|
+
trace?.endStage(hotnessBlended.length, hotnessBlended.map(r => r.score));
|
|
1438
|
+
|
|
1439
|
+
// P0.2: Frequency boost — repeatedly retrieved memories score higher
|
|
1440
|
+
trace?.startStage("frequency_boost", hotnessBlended.length);
|
|
1441
|
+
const frequencyBoosted = this.applyFrequencyBoost(hotnessBlended);
|
|
1442
|
+
trace?.endStage(frequencyBoosted.length, frequencyBoosted.map(r => r.score));
|
|
1443
|
+
|
|
1444
|
+
// A1 error-signature 精确匹配 boost(flag 门控,默认 off → no-op、bit-identical)
|
|
1445
|
+
return applyErrorSignatureBoost(frequencyBoosted, query);
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
/**
|
|
1449
|
+
* 长度归一之前的 7 个环节(新近度 → importance → utility → confidence → emotion → boundary →
|
|
1450
|
+
* 资产类型),从 applySharedScoring 原样挪出来,顺序与 trace 调用逐字不变。单独成方法是为了
|
|
1451
|
+
* 开关 2 能让「trigger 那一路」走完全相同的前置环节(applyTriggerPathFloor,不带 trace)。
|
|
1452
|
+
*/
|
|
1453
|
+
private applyPreLengthStages(
|
|
1454
|
+
results: RetrievalResult[],
|
|
1455
|
+
query: string,
|
|
1456
|
+
trace?: TraceCollector,
|
|
1246
1457
|
): RetrievalResult[] {
|
|
1247
1458
|
trace?.startStage("recency_boost", results.length);
|
|
1248
1459
|
const boosted = this.applyRecencyBoost(results);
|
|
@@ -1278,32 +1489,59 @@ export class MemoryRetriever {
|
|
|
1278
1489
|
const assetWeighted = this.applyAssetTypeWeight(boundaryWeighted);
|
|
1279
1490
|
trace?.endStage(assetWeighted.length, assetWeighted.map(r => r.score));
|
|
1280
1491
|
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
trace?.endStage(lengthNormalized.length, lengthNormalized.map(r => r.score));
|
|
1284
|
-
|
|
1285
|
-
trace?.startStage("time_decay", lengthNormalized.length);
|
|
1286
|
-
const timeDecayed = this.applyTimeDecay(lengthNormalized);
|
|
1287
|
-
trace?.endStage(timeDecayed.length, timeDecayed.map(r => r.score));
|
|
1288
|
-
|
|
1289
|
-
// B-1/E-1: Evolution decay blend — boost memories with high composite decay score
|
|
1290
|
-
const evolutionBlended = this.applyEvolutionDecayBlend(timeDecayed);
|
|
1291
|
-
|
|
1292
|
-
// E-1: Access count boost — memories retrieved more often get a score nudge
|
|
1293
|
-
const accessBoosted = this.applyAccessCountBoost(evolutionBlended);
|
|
1294
|
-
|
|
1295
|
-
// Hotness blend: boost frequently + recently accessed memories
|
|
1296
|
-
trace?.startStage("hotness_blend", accessBoosted.length);
|
|
1297
|
-
const hotnessBlended = this.applyHotnessBlend(accessBoosted);
|
|
1298
|
-
trace?.endStage(hotnessBlended.length, hotnessBlended.map(r => r.score));
|
|
1492
|
+
return assetWeighted;
|
|
1493
|
+
}
|
|
1299
1494
|
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1495
|
+
/**
|
|
1496
|
+
* 开关 2(RECALLNEST_TRIGGER_LENGTH_EXEMPT):trigger 那一路不做长度归一。
|
|
1497
|
+
*
|
|
1498
|
+
* 带 sources.trigger 的条目另算「只靠 trigger 这一路会是多少分」:以 trigger 分 × base 为底,
|
|
1499
|
+
* 不带 trace 走一遍与正文完全相同的前置环节,不做长度归一;长度归一后的分数取两路较大。
|
|
1500
|
+
* 它的相关度来自那句短 trigger、不来自正文,所以不该按正文长度打折。
|
|
1501
|
+
*
|
|
1502
|
+
* 不用「长度系数取 max(原系数, trigger 分 / 并入后分数)」(第二步 v2 的写法):长度归一之前有
|
|
1503
|
+
* 新近度加法,终分 ≈ (B + r)·M·T/B,向量分 B 变大时反而下降(R3 Codex 实测 0.79→0.81 终分降),
|
|
1504
|
+
* 而且得往 sources 记占比、从 API 原样漏出去。两路各自对向量分、trigger 分单调,取大仍单调。
|
|
1505
|
+
*/
|
|
1506
|
+
private applyTriggerPathFloor(
|
|
1507
|
+
normalized: RetrievalResult[],
|
|
1508
|
+
original: RetrievalResult[],
|
|
1509
|
+
query: string,
|
|
1510
|
+
base: number,
|
|
1511
|
+
): RetrievalResult[] {
|
|
1512
|
+
const twins: RetrievalResult[] = [];
|
|
1513
|
+
for (const r of original) {
|
|
1514
|
+
const t = r.sources.trigger;
|
|
1515
|
+
if (t) twins.push({ ...r, score: t.score * base });
|
|
1516
|
+
}
|
|
1517
|
+
if (twins.length === 0) return normalized;
|
|
1518
|
+
const triggerPath = new Map(this.applyPreLengthStages(twins, query).map(r => [r.entry.id, r.score]));
|
|
1519
|
+
return normalized
|
|
1520
|
+
.map(r => {
|
|
1521
|
+
const floor = triggerPath.get(r.entry.id);
|
|
1522
|
+
return floor !== undefined && floor > r.score ? { ...r, score: floor } : r;
|
|
1523
|
+
})
|
|
1524
|
+
.sort((a, b) => b.score - a.score);
|
|
1525
|
+
}
|
|
1304
1526
|
|
|
1305
|
-
|
|
1306
|
-
|
|
1527
|
+
/**
|
|
1528
|
+
* bounded 流行度(第二步 plan §零.1):score × (1 + γ·h),h = min(1, log2(1 + evolution.accessCount) / 5)。
|
|
1529
|
+
* 只读 evolution.accessCount——每次 manual 检索的每条返回都 +1、写锁内读改写、库内持久;
|
|
1530
|
+
* 频次台账(跨进程丢更新)与顶层 accessCount 都不读。零访问 h = 0 原样放行,只加不减,不截平。
|
|
1531
|
+
* # simplified: 31 次封顶的对数饱和、不看时间;要按「最近常用」加权再换成带衰减的计数(§一.4)
|
|
1532
|
+
*/
|
|
1533
|
+
private applyPopularityBonus(results: RetrievalResult[]): RetrievalResult[] {
|
|
1534
|
+
const raw = this.config.popularityBonusMax;
|
|
1535
|
+
const gamma = Math.max(0, Number.isFinite(raw) ? raw : 0);
|
|
1536
|
+
if (gamma <= 0) return results;
|
|
1537
|
+
return results.map(r => {
|
|
1538
|
+
const n: unknown = parseEvolution(r.entry.metadata, r.entry.timestamp).accessCount;
|
|
1539
|
+
// parseEvolution 不校验类型:元数据里的计数若是字符串 / 负数 / 非有限数,一律按零访问。
|
|
1540
|
+
// 这一环不截平,NaN 会一路漏到出口(legacy 那几环有 clamp01 的非有限兜底,这里没有;第二步实现互审 Codex 实测 "broken" → NaN%)
|
|
1541
|
+
if (typeof n !== "number" || !Number.isFinite(n) || n <= 0) return r;
|
|
1542
|
+
const h = Math.min(1, Math.log2(1 + n) / 5);
|
|
1543
|
+
return { ...r, score: r.score * (1 + gamma * h) };
|
|
1544
|
+
});
|
|
1307
1545
|
}
|
|
1308
1546
|
|
|
1309
1547
|
/**
|
|
@@ -1390,10 +1628,11 @@ export class MemoryRetriever {
|
|
|
1390
1628
|
scopeFilter?: string[],
|
|
1391
1629
|
category?: string,
|
|
1392
1630
|
includeArchived?: boolean,
|
|
1631
|
+
excludeScopes?: string[],
|
|
1393
1632
|
): Promise<Array<MemorySearchResult & { rank: number }>> {
|
|
1394
1633
|
let results: MemorySearchResult[];
|
|
1395
1634
|
try {
|
|
1396
|
-
results = await this.store.vectorSearch(queryVector, limit, 0.1, scopeFilter);
|
|
1635
|
+
results = await this.store.vectorSearch(queryVector, limit, 0.1, scopeFilter, undefined, excludeScopes);
|
|
1397
1636
|
} catch (err) {
|
|
1398
1637
|
// Fail-open: log warning and continue with empty results (backport from v1.0.30)
|
|
1399
1638
|
logWarn("vectorSearch failed, continuing with empty results:", err);
|
|
@@ -1422,8 +1661,9 @@ export class MemoryRetriever {
|
|
|
1422
1661
|
scopeFilter?: string[],
|
|
1423
1662
|
category?: string,
|
|
1424
1663
|
includeArchived?: boolean,
|
|
1664
|
+
excludeScopes?: string[],
|
|
1425
1665
|
): Promise<Array<MemorySearchResult & { rank: number }>> {
|
|
1426
|
-
const results = await this.store.bm25Search(query, limit, scopeFilter);
|
|
1666
|
+
const results = await this.store.bm25Search(query, limit, scopeFilter, excludeScopes);
|
|
1427
1667
|
|
|
1428
1668
|
// Filter by category if specified
|
|
1429
1669
|
const afterCategory = category
|
package/src/retroactive-boost.ts
CHANGED
|
@@ -142,12 +142,13 @@ export async function retroactiveBoost(
|
|
|
142
142
|
meta.tier = "working";
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
await store.update(entry.id, {
|
|
145
|
+
const boosted = await store.update(entry.id, {
|
|
146
146
|
importance: newImportance,
|
|
147
147
|
metadata: JSON.stringify(meta),
|
|
148
148
|
}, [newEntry.scope]);
|
|
149
149
|
|
|
150
|
-
|
|
150
|
+
// update() 返回 null = 这一行在检索之后被删掉了,没有提权成,不计入
|
|
151
|
+
if (boosted) boostedIds.push(entry.id);
|
|
151
152
|
}
|
|
152
153
|
|
|
153
154
|
return {
|
package/src/runtime-config.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { metaDir } from "./compat.js";
|
|
|
6
6
|
import { MemoryStore, validateStoragePath } from "./store.js";
|
|
7
7
|
import { createEmbedder, getVectorDimensions, type EmbeddingConfig } from "./embedder.js";
|
|
8
8
|
import { createRetriever, type RetrievalConfig, DEFAULT_RETRIEVAL_CONFIG } from "./retriever.js";
|
|
9
|
+
import { TriggerStore } from "./trigger-store.js";
|
|
9
10
|
import { applyRetrievalProfile } from "./retrieval-profiles.js";
|
|
10
11
|
import { AccessTracker } from "./access-tracker.js";
|
|
11
12
|
import { createAuditLogger } from "./audit-log.js";
|
|
@@ -182,6 +183,10 @@ export function createComponents(config: LocalMemoryConfig, profileName?: string
|
|
|
182
183
|
const { profile, config: retrieverConfig } = applyRetrievalProfile(baseRetrievalConfig, profileName);
|
|
183
184
|
const retriever = createRetriever(store, embedder, retrieverConfig);
|
|
184
185
|
|
|
186
|
+
// T-Mem triggers(2026-09-22):侧表与主表同库同 dim;挂到 retriever 后检索多一条联想入口
|
|
187
|
+
const triggerStore = new TriggerStore({ dbPath, vectorDim: embedder.dimensions });
|
|
188
|
+
retriever.setTriggerStore(triggerStore);
|
|
189
|
+
|
|
185
190
|
// Attach access tracker for reinforcement-based decay
|
|
186
191
|
const accessTracker = new AccessTracker(store);
|
|
187
192
|
accessTracker.registerExitFlush();
|
|
@@ -220,7 +225,7 @@ export function createComponents(config: LocalMemoryConfig, profileName?: string
|
|
|
220
225
|
}
|
|
221
226
|
}
|
|
222
227
|
|
|
223
|
-
return { store, embedder, retriever, profile, accessTracker, frequencyTracker, llm };
|
|
228
|
+
return { store, embedder, retriever, profile, accessTracker, frequencyTracker, llm, triggerStore };
|
|
224
229
|
}
|
|
225
230
|
|
|
226
231
|
export function createStoreOnly(config: LocalMemoryConfig): MemoryStore {
|