memory-lancedb-pro 1.0.10 → 1.0.12
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 +11 -0
- package/README.md +30 -1
- package/README_CN.md +30 -1
- package/index.ts +57 -53
- package/openclaw.plugin.json +15 -2
- package/package.json +2 -2
- package/src/adaptive-retrieval.ts +15 -5
- package/src/retriever.ts +20 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.12
|
|
4
|
+
|
|
5
|
+
- Fix: ghost memories stuck in autoRecall after deletion (#15). BM25-only results from stale FTS index are now validated via `store.hasId()` before inclusion in fused results. Removed the BM25-only floor score of 0.5 that allowed deleted entries to survive `hardMinScore` filtering.
|
|
6
|
+
- Fix: HEARTBEAT pattern now matches anywhere in the prompt (not just at start), preventing autoRecall from triggering on prefixed HEARTBEAT messages.
|
|
7
|
+
- Add: `autoRecallMinLength` config option to set a custom minimum prompt length for autoRecall (default: 15 chars English, 6 CJK). Prompts shorter than this threshold are skipped.
|
|
8
|
+
- Add: `ping`, `pong`, `test`, `debug` added to skip patterns in adaptive retrieval.
|
|
9
|
+
|
|
10
|
+
## 1.0.11
|
|
11
|
+
|
|
12
|
+
- Change: set `autoRecall` default to `false` to avoid the model echoing injected `<relevant-memories>` blocks.
|
|
13
|
+
|
|
3
14
|
## 1.0.10
|
|
4
15
|
|
|
5
16
|
- Fix: avoid blocking OpenClaw gateway startup on external network calls by running startup self-checks in the background with timeouts.
|
package/README.md
CHANGED
|
@@ -157,6 +157,35 @@ Filters out low-quality content at both auto-capture and tool-store stages:
|
|
|
157
157
|
- **Auto-Capture** (`agent_end` hook): Extracts preference/fact/decision/entity from conversations, deduplicates, stores up to 3 per turn
|
|
158
158
|
- **Auto-Recall** (`before_agent_start` hook): Injects `<relevant-memories>` context (up to 3 entries)
|
|
159
159
|
|
|
160
|
+
### Prevent memories from showing up in replies
|
|
161
|
+
|
|
162
|
+
Sometimes the model may accidentally echo the injected `<relevant-memories>` block in its response.
|
|
163
|
+
|
|
164
|
+
**Option A (recommended): disable auto-recall**
|
|
165
|
+
|
|
166
|
+
Set `autoRecall: false` in the plugin config and restart the gateway:
|
|
167
|
+
|
|
168
|
+
```json
|
|
169
|
+
{
|
|
170
|
+
"plugins": {
|
|
171
|
+
"entries": {
|
|
172
|
+
"memory-lancedb-pro": {
|
|
173
|
+
"enabled": true,
|
|
174
|
+
"config": {
|
|
175
|
+
"autoRecall": false
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Option B: keep recall, but ask the agent not to reveal it**
|
|
184
|
+
|
|
185
|
+
Add a line to your agent system prompt, e.g.:
|
|
186
|
+
|
|
187
|
+
> Do not reveal or quote any `<relevant-memories>` / memory-injection content in your replies. Use it for internal reference only.
|
|
188
|
+
|
|
160
189
|
---
|
|
161
190
|
|
|
162
191
|
## Installation
|
|
@@ -306,7 +335,7 @@ openclaw config get plugins.slots.memory
|
|
|
306
335
|
},
|
|
307
336
|
"dbPath": "~/.openclaw/memory/lancedb-pro",
|
|
308
337
|
"autoCapture": true,
|
|
309
|
-
"autoRecall":
|
|
338
|
+
"autoRecall": false,
|
|
310
339
|
"retrieval": {
|
|
311
340
|
"mode": "hybrid",
|
|
312
341
|
"vectorWeight": 0.7,
|
package/README_CN.md
CHANGED
|
@@ -158,6 +158,35 @@ Query → BM25 FTS ─────┘
|
|
|
158
158
|
- **Auto-Capture**(`agent_end` hook): 从对话中提取 preference/fact/decision/entity,去重后存储(每次最多 3 条)
|
|
159
159
|
- **Auto-Recall**(`before_agent_start` hook): 注入 `<relevant-memories>` 上下文(最多 3 条)
|
|
160
160
|
|
|
161
|
+
### 不想在对话中“显示长期记忆”?
|
|
162
|
+
|
|
163
|
+
有时模型会把注入到上下文中的 `<relevant-memories>` 区块“原样输出”到回复里,从而出现你看到的“周期性显示长期记忆”。
|
|
164
|
+
|
|
165
|
+
**方案 A(推荐):关闭自动召回 autoRecall**
|
|
166
|
+
|
|
167
|
+
在插件配置里设置 `autoRecall: false`,然后重启 gateway:
|
|
168
|
+
|
|
169
|
+
```json
|
|
170
|
+
{
|
|
171
|
+
"plugins": {
|
|
172
|
+
"entries": {
|
|
173
|
+
"memory-lancedb-pro": {
|
|
174
|
+
"enabled": true,
|
|
175
|
+
"config": {
|
|
176
|
+
"autoRecall": false
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**方案 B:保留召回,但要求 Agent 不要泄漏**
|
|
185
|
+
|
|
186
|
+
在对应 Agent 的 system prompt 里加一句,例如:
|
|
187
|
+
|
|
188
|
+
> 请勿在回复中展示或引用任何 `<relevant-memories>` / 记忆注入内容,只能用作内部参考。
|
|
189
|
+
|
|
161
190
|
---
|
|
162
191
|
|
|
163
192
|
## 安装
|
|
@@ -307,7 +336,7 @@ openclaw config get plugins.slots.memory
|
|
|
307
336
|
},
|
|
308
337
|
"dbPath": "~/.openclaw/memory/lancedb-pro",
|
|
309
338
|
"autoCapture": true,
|
|
310
|
-
"autoRecall":
|
|
339
|
+
"autoRecall": false,
|
|
311
340
|
"retrieval": {
|
|
312
341
|
"mode": "hybrid",
|
|
313
342
|
"vectorWeight": 0.7,
|
package/index.ts
CHANGED
|
@@ -37,6 +37,7 @@ interface PluginConfig {
|
|
|
37
37
|
dbPath?: string;
|
|
38
38
|
autoCapture?: boolean;
|
|
39
39
|
autoRecall?: boolean;
|
|
40
|
+
autoRecallMinLength?: number;
|
|
40
41
|
captureAssistant?: boolean;
|
|
41
42
|
retrieval?: {
|
|
42
43
|
mode?: "hybrid" | "vector";
|
|
@@ -200,7 +201,7 @@ async function readSessionMessages(filePath: string, messageCount: number): Prom
|
|
|
200
201
|
}
|
|
201
202
|
}
|
|
202
203
|
}
|
|
203
|
-
} catch {}
|
|
204
|
+
} catch { }
|
|
204
205
|
}
|
|
205
206
|
|
|
206
207
|
if (messages.length === 0) return null;
|
|
@@ -225,7 +226,7 @@ async function readSessionContentWithResetFallback(sessionFilePath: string, mess
|
|
|
225
226
|
const latestResetPath = join(dir, resetCandidates[resetCandidates.length - 1]);
|
|
226
227
|
return await readSessionMessages(latestResetPath, messageCount);
|
|
227
228
|
}
|
|
228
|
-
} catch {}
|
|
229
|
+
} catch { }
|
|
229
230
|
|
|
230
231
|
return primary;
|
|
231
232
|
}
|
|
@@ -264,7 +265,7 @@ async function findPreviousSessionFile(sessionsDir: string, currentSessionFile?:
|
|
|
264
265
|
.sort().reverse();
|
|
265
266
|
if (nonReset.length > 0) return join(sessionsDir, nonReset[0]);
|
|
266
267
|
}
|
|
267
|
-
} catch {}
|
|
268
|
+
} catch { }
|
|
268
269
|
}
|
|
269
270
|
|
|
270
271
|
// ============================================================================
|
|
@@ -364,9 +365,10 @@ const memoryLanceDBProPlugin = {
|
|
|
364
365
|
// ========================================================================
|
|
365
366
|
|
|
366
367
|
// Auto-recall: inject relevant memories before agent starts
|
|
367
|
-
|
|
368
|
+
// Default is OFF to prevent the model from accidentally echoing injected context.
|
|
369
|
+
if (config.autoRecall === true) {
|
|
368
370
|
api.on("before_agent_start", async (event, ctx) => {
|
|
369
|
-
if (!event.prompt || shouldSkipRetrieval(event.prompt)) {
|
|
371
|
+
if (!event.prompt || shouldSkipRetrieval(event.prompt, config.autoRecallMinLength)) {
|
|
370
372
|
return;
|
|
371
373
|
}
|
|
372
374
|
|
|
@@ -622,7 +624,7 @@ const memoryLanceDBProPlugin = {
|
|
|
622
624
|
if (files.length > 7) {
|
|
623
625
|
const { unlink } = await import("node:fs/promises");
|
|
624
626
|
for (const old of files.slice(0, files.length - 7)) {
|
|
625
|
-
await unlink(join(backupDir, old)).catch(() => {});
|
|
627
|
+
await unlink(join(backupDir, old)).catch(() => { });
|
|
626
628
|
}
|
|
627
629
|
}
|
|
628
630
|
|
|
@@ -663,10 +665,10 @@ const memoryLanceDBProPlugin = {
|
|
|
663
665
|
|
|
664
666
|
api.logger.info(
|
|
665
667
|
`memory-lancedb-pro: initialized successfully ` +
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
668
|
+
`(embedding: ${embedTest.success ? "OK" : "FAIL"}, ` +
|
|
669
|
+
`retrieval: ${retrievalTest.success ? "OK" : "FAIL"}, ` +
|
|
670
|
+
`mode: ${retrievalTest.mode}, ` +
|
|
671
|
+
`FTS: ${retrievalTest.hasFtsSupport ? "enabled" : "disabled"})`
|
|
670
672
|
);
|
|
671
673
|
|
|
672
674
|
if (!embedTest.success) {
|
|
@@ -700,53 +702,55 @@ const memoryLanceDBProPlugin = {
|
|
|
700
702
|
};
|
|
701
703
|
|
|
702
704
|
function parsePluginConfig(value: unknown): PluginConfig {
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
705
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
706
|
+
throw new Error("memory-lancedb-pro config required");
|
|
707
|
+
}
|
|
708
|
+
const cfg = value as Record<string, unknown>;
|
|
707
709
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
710
|
+
const embedding = cfg.embedding as Record<string, unknown> | undefined;
|
|
711
|
+
if (!embedding) {
|
|
712
|
+
throw new Error("embedding config is required");
|
|
713
|
+
}
|
|
712
714
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
715
|
+
const apiKey = typeof embedding.apiKey === "string"
|
|
716
|
+
? embedding.apiKey
|
|
717
|
+
: process.env.OPENAI_API_KEY || "";
|
|
716
718
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
719
|
+
if (!apiKey) {
|
|
720
|
+
throw new Error("embedding.apiKey is required (set directly or via OPENAI_API_KEY env var)");
|
|
721
|
+
}
|
|
720
722
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
723
|
+
return {
|
|
724
|
+
embedding: {
|
|
725
|
+
provider: "openai-compatible",
|
|
726
|
+
apiKey,
|
|
727
|
+
model: typeof embedding.model === "string" ? embedding.model : "text-embedding-3-small",
|
|
728
|
+
baseURL: typeof embedding.baseURL === "string" ? resolveEnvVars(embedding.baseURL) : undefined,
|
|
729
|
+
// Accept number, numeric string, or env-var string (e.g. "${EMBED_DIM}").
|
|
730
|
+
// Also accept legacy top-level `dimensions` for convenience.
|
|
731
|
+
dimensions: parsePositiveInt(embedding.dimensions ?? cfg.dimensions),
|
|
732
|
+
taskQuery: typeof embedding.taskQuery === "string" ? embedding.taskQuery : undefined,
|
|
733
|
+
taskPassage: typeof embedding.taskPassage === "string" ? embedding.taskPassage : undefined,
|
|
734
|
+
normalized: typeof embedding.normalized === "boolean" ? embedding.normalized : undefined,
|
|
735
|
+
},
|
|
736
|
+
dbPath: typeof cfg.dbPath === "string" ? cfg.dbPath : undefined,
|
|
737
|
+
autoCapture: cfg.autoCapture !== false,
|
|
738
|
+
// Default OFF: only enable when explicitly set to true.
|
|
739
|
+
autoRecall: cfg.autoRecall === true,
|
|
740
|
+
autoRecallMinLength: parsePositiveInt(cfg.autoRecallMinLength),
|
|
741
|
+
captureAssistant: cfg.captureAssistant === true,
|
|
742
|
+
retrieval: typeof cfg.retrieval === "object" && cfg.retrieval !== null ? cfg.retrieval as any : undefined,
|
|
743
|
+
scopes: typeof cfg.scopes === "object" && cfg.scopes !== null ? cfg.scopes as any : undefined,
|
|
744
|
+
enableManagementTools: cfg.enableManagementTools === true,
|
|
745
|
+
sessionMemory: typeof cfg.sessionMemory === "object" && cfg.sessionMemory !== null
|
|
746
|
+
? {
|
|
747
|
+
enabled: (cfg.sessionMemory as Record<string, unknown>).enabled !== false,
|
|
748
|
+
messageCount: typeof (cfg.sessionMemory as Record<string, unknown>).messageCount === "number"
|
|
749
|
+
? (cfg.sessionMemory as Record<string, unknown>).messageCount as number
|
|
750
|
+
: undefined,
|
|
751
|
+
}
|
|
752
|
+
: undefined,
|
|
753
|
+
};
|
|
750
754
|
}
|
|
751
755
|
|
|
752
756
|
export default memoryLanceDBProPlugin;
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "memory-lancedb-pro",
|
|
3
3
|
"name": "Memory (LanceDB Pro)",
|
|
4
4
|
"description": "Enhanced LanceDB-backed long-term memory with hybrid retrieval, multi-scope isolation, and management CLI",
|
|
5
|
-
"version": "1.0.
|
|
5
|
+
"version": "1.0.12",
|
|
6
6
|
"kind": "memory",
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
|
@@ -58,7 +58,15 @@
|
|
|
58
58
|
"type": "boolean"
|
|
59
59
|
},
|
|
60
60
|
"autoRecall": {
|
|
61
|
-
"type": "boolean"
|
|
61
|
+
"type": "boolean",
|
|
62
|
+
"default": false
|
|
63
|
+
},
|
|
64
|
+
"autoRecallMinLength": {
|
|
65
|
+
"type": "integer",
|
|
66
|
+
"minimum": 1,
|
|
67
|
+
"maximum": 200,
|
|
68
|
+
"default": 15,
|
|
69
|
+
"description": "Minimum prompt length (in characters) to trigger auto-recall. Prompts shorter than this are skipped. Default: 15 for English, 6 for CJK."
|
|
62
70
|
},
|
|
63
71
|
"captureAssistant": {
|
|
64
72
|
"type": "boolean"
|
|
@@ -265,6 +273,11 @@
|
|
|
265
273
|
"label": "Auto-Recall",
|
|
266
274
|
"help": "Automatically inject relevant memories into context"
|
|
267
275
|
},
|
|
276
|
+
"autoRecallMinLength": {
|
|
277
|
+
"label": "Auto-Recall Min Length",
|
|
278
|
+
"help": "Minimum prompt length to trigger auto-recall (shorter prompts are skipped). Default: 15 chars for English, 6 for CJK.",
|
|
279
|
+
"advanced": true
|
|
280
|
+
},
|
|
268
281
|
"captureAssistant": {
|
|
269
282
|
"label": "Capture Assistant Messages",
|
|
270
283
|
"help": "Also auto-capture assistant messages (default false to reduce memory pollution)",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memory-lancedb-pro",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.12",
|
|
4
4
|
"description": "OpenClaw enhanced LanceDB memory plugin with hybrid retrieval (Vector + BM25), cross-encoder rerank, multi-scope isolation, and management CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -40,4 +40,4 @@
|
|
|
40
40
|
"jiti": "^2.6.0",
|
|
41
41
|
"typescript": "^5.9.3"
|
|
42
42
|
}
|
|
43
|
-
}
|
|
43
|
+
}
|
|
@@ -18,9 +18,11 @@ const SKIP_PATTERNS = [
|
|
|
18
18
|
/^(go ahead|continue|proceed|do it|start|begin|next|实施|开始|继续|好的|可以|行)\s*[.!]?$/i,
|
|
19
19
|
// Pure emoji
|
|
20
20
|
/^[\p{Emoji}\s]+$/u,
|
|
21
|
-
// Heartbeat/system
|
|
22
|
-
|
|
21
|
+
// Heartbeat/system (match anywhere, not just at start, to handle prefixed formats)
|
|
22
|
+
/HEARTBEAT/i,
|
|
23
23
|
/^\[System/i,
|
|
24
|
+
// Single-word utility pings
|
|
25
|
+
/^(ping|pong|test|debug)\s*[.!?]?$/i,
|
|
24
26
|
];
|
|
25
27
|
|
|
26
28
|
// Queries that SHOULD trigger retrieval even if short
|
|
@@ -61,8 +63,10 @@ function normalizeQuery(query: string): string {
|
|
|
61
63
|
/**
|
|
62
64
|
* Determine if a query should skip memory retrieval.
|
|
63
65
|
* Returns true if retrieval should be skipped.
|
|
66
|
+
* @param query The raw prompt text
|
|
67
|
+
* @param minLength Optional minimum length override (if set, overrides built-in thresholds)
|
|
64
68
|
*/
|
|
65
|
-
export function shouldSkipRetrieval(query: string): boolean {
|
|
69
|
+
export function shouldSkipRetrieval(query: string, minLength?: number): boolean {
|
|
66
70
|
const trimmed = normalizeQuery(query);
|
|
67
71
|
|
|
68
72
|
// Force retrieve if query has memory-related intent (checked FIRST,
|
|
@@ -75,11 +79,17 @@ export function shouldSkipRetrieval(query: string): boolean {
|
|
|
75
79
|
// Skip if matches any skip pattern
|
|
76
80
|
if (SKIP_PATTERNS.some(p => p.test(trimmed))) return true;
|
|
77
81
|
|
|
82
|
+
// If caller provides a custom minimum length, use it
|
|
83
|
+
if (minLength !== undefined && minLength > 0) {
|
|
84
|
+
if (trimmed.length < minLength && !trimmed.includes('?') && !trimmed.includes('?')) return true;
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
78
88
|
// Skip very short non-question messages (likely commands or affirmations)
|
|
79
89
|
// CJK characters carry more meaning per character, so use a lower threshold
|
|
80
90
|
const hasCJK = /[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/.test(trimmed);
|
|
81
|
-
const
|
|
82
|
-
if (trimmed.length <
|
|
91
|
+
const defaultMinLength = hasCJK ? 6 : 15;
|
|
92
|
+
if (trimmed.length < defaultMinLength && !trimmed.includes('?') && !trimmed.includes('?')) return true;
|
|
83
93
|
|
|
84
94
|
// Default: do retrieve
|
|
85
95
|
return false;
|
package/src/retriever.ts
CHANGED
|
@@ -282,8 +282,8 @@ export class MemoryRetriever {
|
|
|
282
282
|
this.runBM25Search(query, candidatePoolSize, scopeFilter, category),
|
|
283
283
|
]);
|
|
284
284
|
|
|
285
|
-
// Fuse results using RRF
|
|
286
|
-
const fusedResults = this.fuseResults(vectorResults, bm25Results);
|
|
285
|
+
// Fuse results using RRF (async: validates BM25-only entries exist in store)
|
|
286
|
+
const fusedResults = await this.fuseResults(vectorResults, bm25Results);
|
|
287
287
|
|
|
288
288
|
// Apply minimum score threshold
|
|
289
289
|
const filtered = fusedResults.filter(r => r.score >= this.config.minScore);
|
|
@@ -357,10 +357,10 @@ export class MemoryRetriever {
|
|
|
357
357
|
}));
|
|
358
358
|
}
|
|
359
359
|
|
|
360
|
-
private fuseResults(
|
|
360
|
+
private async fuseResults(
|
|
361
361
|
vectorResults: Array<MemorySearchResult & { rank: number }>,
|
|
362
362
|
bm25Results: Array<MemorySearchResult & { rank: number }>
|
|
363
|
-
): RetrievalResult[] {
|
|
363
|
+
): Promise<RetrievalResult[]> {
|
|
364
364
|
// Create maps for quick lookup
|
|
365
365
|
const vectorMap = new Map<string, MemorySearchResult & { rank: number }>();
|
|
366
366
|
const bm25Map = new Map<string, MemorySearchResult & { rank: number }>();
|
|
@@ -383,6 +383,18 @@ export class MemoryRetriever {
|
|
|
383
383
|
const vectorResult = vectorMap.get(id);
|
|
384
384
|
const bm25Result = bm25Map.get(id);
|
|
385
385
|
|
|
386
|
+
// FIX(#15): BM25-only results may be "ghost" entries whose vector data was
|
|
387
|
+
// deleted but whose FTS index entry lingers until the next index rebuild.
|
|
388
|
+
// Validate that the entry actually exists in the store before including it.
|
|
389
|
+
if (!vectorResult && bm25Result) {
|
|
390
|
+
try {
|
|
391
|
+
const exists = await this.store.hasId(id);
|
|
392
|
+
if (!exists) continue; // Skip ghost entry
|
|
393
|
+
} catch {
|
|
394
|
+
// If hasId fails, keep the result (fail-open)
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
386
398
|
// Use the result with more complete data (prefer vector result if both exist)
|
|
387
399
|
const baseResult = vectorResult || bm25Result!;
|
|
388
400
|
|
|
@@ -392,12 +404,12 @@ export class MemoryRetriever {
|
|
|
392
404
|
const bm25Hit = bm25Result ? 1 : 0;
|
|
393
405
|
|
|
394
406
|
// Base = vector score; BM25 hit boosts by up to 15%
|
|
395
|
-
// BM25-only results use their
|
|
396
|
-
//
|
|
397
|
-
//
|
|
407
|
+
// BM25-only results use their raw BM25 score so exact keyword matches
|
|
408
|
+
// (e.g. searching "JINA_API_KEY") still surface. The previous floor of 0.5
|
|
409
|
+
// was too generous and allowed ghost entries to survive hardMinScore (0.35).
|
|
398
410
|
const fusedScore = vectorResult
|
|
399
411
|
? clamp01(vectorScore + (bm25Hit * 0.15 * vectorScore), 0.1)
|
|
400
|
-
: clamp01(
|
|
412
|
+
: clamp01(bm25Result!.score, 0.1);
|
|
401
413
|
|
|
402
414
|
fusedResults.push({
|
|
403
415
|
entry: baseResult.entry,
|