From 9a6bf0fb53facade6a9b174ca7e8af0fca92e55a Mon Sep 17 00:00:00 2001 From: raw34 Date: Sat, 18 Apr 2026 12:45:17 +0000 Subject: [PATCH] feat(retrieval): add fresh memory boost for recently stored memories Memories stored within 30 minutes (configurable) get an additional score boost (+0.15), with an extra +0.05 for reflection/preference categories. This ensures corrections from ongoing conversations outrank stale high-access memories in auto-recall ranking. Configurable via retrieval.freshMemoryBoostMinutes (default: 30, set 0 to disable) and retrieval.freshMemoryBoostWeight (default: 0.15). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/retriever.ts | 24 ++++++++ test/fresh-memory-boost.test.mjs | 99 ++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 test/fresh-memory-boost.test.mjs diff --git a/src/retriever.ts b/src/retriever.ts index 9494a7bf..150372e9 100644 --- a/src/retriever.ts +++ b/src/retriever.ts @@ -40,6 +40,11 @@ export interface RetrievalConfig { recencyHalfLifeDays: number; /** Max recency boost factor (default: 0.10) */ recencyWeight: number; + /** Minutes window for fresh memory boost. Memories stored within this window + * get an additional score bonus (default: 30). Set to 0 to disable. */ + freshMemoryBoostMinutes?: number; + /** Base score boost for fresh memories (default: 0.15) */ + freshMemoryBoostWeight?: number; /** Filter noise from results (default: true) */ filterNoise: boolean; /** Reranker API key (enables cross-encoder reranking) */ @@ -1364,6 +1369,25 @@ export class MemoryRetriever { }; }); + // 新鲜记忆加分:窗口内存储的记忆获得额外分数, + // 确保纠正信息能排在高频旧记忆之前。 + const freshWindowMinutes = this.config.freshMemoryBoostMinutes ?? 30; + const freshWeight = this.config.freshMemoryBoostWeight ?? 0.15; + if (freshWindowMinutes > 0) { + const freshWindowMs = freshWindowMinutes * 60_000; + for (const r of boosted) { + const ts = r.entry.timestamp && r.entry.timestamp > 0 ? r.entry.timestamp : now; + const ageMs = now - ts; + if (ageMs < freshWindowMs) { + let bonus = freshWeight; + if (r.entry.category === "reflection" || r.entry.category === "preference") { + bonus += 0.05; + } + r.score = clamp01(r.score + bonus, r.score); + } + } + } + return boosted.sort((a, b) => b.score - a.score); } diff --git a/test/fresh-memory-boost.test.mjs b/test/fresh-memory-boost.test.mjs new file mode 100644 index 00000000..aad1c89a --- /dev/null +++ b/test/fresh-memory-boost.test.mjs @@ -0,0 +1,99 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; + +/** + * 新鲜记忆 recency boost 测试。 + * + * 验证在 freshMemoryBoostMinutes 窗口内存储的记忆会获得额外加分, + * 确保纠正信息和最新上下文能排在高访问量旧记忆之前。 + */ + +// 模拟检索结果工厂 +function makeResult(id, score, timestampMinutesAgo, category = "fact") { + const now = Date.now(); + return { + entry: { + id, + text: `memory ${id}`, + category, + scope: "agent:test", + importance: 0.8, + timestamp: now - timestampMinutesAgo * 60_000, + vector: [0.1, 0.2, 0.3], + metadata: "{}", + }, + score, + sources: {}, + }; +} + +describe("fresh memory boost", () => { + it("应对窗口内的记忆加分", () => { + const oldMemory = makeResult("old-1", 0.65, 120); + const freshMemory = makeResult("fresh-1", 0.55, 5); + + const freshBoost = 0.15; + const windowMinutes = 30; + + const now = Date.now(); + const results = [oldMemory, freshMemory].map((r) => { + const ageMinutes = (now - r.entry.timestamp) / 60_000; + if (ageMinutes < windowMinutes) { + return { ...r, score: Math.min(1, r.score + freshBoost) }; + } + return r; + }); + + results.sort((a, b) => b.score - a.score); + + assert.equal(results[0].entry.id, "fresh-1"); + assert.ok(results[0].score > results[1].score); + }); + + it("应对 reflection/preference 类别额外加分", () => { + const freshFact = makeResult("fact-1", 0.50, 10, "fact"); + const freshReflection = makeResult("refl-1", 0.50, 10, "reflection"); + + const freshBoost = 0.15; + const categoryBonus = 0.05; + const windowMinutes = 30; + + const now = Date.now(); + const results = [freshFact, freshReflection].map((r) => { + const ageMinutes = (now - r.entry.timestamp) / 60_000; + if (ageMinutes < windowMinutes) { + let boost = freshBoost; + if (r.entry.category === "reflection" || r.entry.category === "preference") { + boost += categoryBonus; + } + return { ...r, score: Math.min(1, r.score + boost) }; + } + return r; + }); + + results.sort((a, b) => b.score - a.score); + + assert.equal(results[0].entry.id, "refl-1"); + assert.ok(results[0].score - results[1].score >= 0.04); + }); + + it("不应对窗口外的记忆加分", () => { + const oldMemory = makeResult("old-1", 0.60, 60); + + const windowMinutes = 30; + const now = Date.now(); + const ageMinutes = (now - oldMemory.entry.timestamp) / 60_000; + + assert.ok(ageMinutes > windowMinutes); + assert.equal(oldMemory.score, 0.60); + }); + + it("freshMemoryBoostMinutes 为 0 时应禁用加分", () => { + const freshMemory = makeResult("fresh-1", 0.55, 5); + const windowMinutes = 0; + + const boosted = windowMinutes > 0; + assert.equal(boosted, false); + assert.equal(freshMemory.score, 0.55); + }); +});