mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 08:42:43 +00:00
Memory: keep keyword hits when hybrid vector misses
This commit is contained in:
@@ -81,6 +81,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Web UI/Assistant text: strip internal `<relevant-memories>...</relevant-memories>` scaffolding from rendered assistant messages (while preserving code-fence literals), preventing memory-context leakage in chat output for models that echo internal blocks. (#29851) Thanks @Valkster70.
|
- Web UI/Assistant text: strip internal `<relevant-memories>...</relevant-memories>` scaffolding from rendered assistant messages (while preserving code-fence literals), preventing memory-context leakage in chat output for models that echo internal blocks. (#29851) Thanks @Valkster70.
|
||||||
- Dashboard/Sessions: allow authenticated Control UI clients to delete and patch sessions while still blocking regular webchat clients from session mutation RPCs, fixing Dashboard session delete failures. (#21264) Thanks @jskoiz.
|
- Dashboard/Sessions: allow authenticated Control UI clients to delete and patch sessions while still blocking regular webchat clients from session mutation RPCs, fixing Dashboard session delete failures. (#21264) Thanks @jskoiz.
|
||||||
- TUI/Session model status: clear stale runtime model identity when model overrides change so `/model` updates are reflected immediately in `sessions.patch` responses and `sessions.list` status surfaces. (#28619) Thanks @lejean2000.
|
- TUI/Session model status: clear stale runtime model identity when model overrides change so `/model` updates are reflected immediately in `sessions.patch` responses and `sessions.list` status surfaces. (#28619) Thanks @lejean2000.
|
||||||
|
- Memory/Hybrid recall: when strict hybrid scoring yields no hits, preserve keyword-backed matches using a text-weight floor so freshly indexed lexical canaries no longer disappear behind `minScore` filtering. (#29112) Thanks @ceo-nada.
|
||||||
- Podman/Quadlet setup: fix `sed` escaping and UID mismatch in Podman Quadlet setup. (#26414) Thanks @KnHack and @vincentkoc.
|
- Podman/Quadlet setup: fix `sed` escaping and UID mismatch in Podman Quadlet setup. (#26414) Thanks @KnHack and @vincentkoc.
|
||||||
- Browser/Navigate: resolve the correct `targetId` in navigate responses after renderer swaps. (#25326) Thanks @stone-jin and @vincentkoc.
|
- Browser/Navigate: resolve the correct `targetId` in navigate responses after renderer swaps. (#25326) Thanks @stone-jin and @vincentkoc.
|
||||||
- Agents/Ollama discovery: skip Ollama discovery when explicit models are configured. (#28827) Thanks @Kansodata and @vincentkoc.
|
- Agents/Ollama discovery: skip Ollama discovery when explicit models are configured. (#28827) Thanks @Kansodata and @vincentkoc.
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ describe("memory index", () => {
|
|||||||
model?: string;
|
model?: string;
|
||||||
vectorEnabled?: boolean;
|
vectorEnabled?: boolean;
|
||||||
cacheEnabled?: boolean;
|
cacheEnabled?: boolean;
|
||||||
|
minScore?: number;
|
||||||
hybrid?: { enabled: boolean; vectorWeight?: number; textWeight?: number };
|
hybrid?: { enabled: boolean; vectorWeight?: number; textWeight?: number };
|
||||||
}): TestCfg {
|
}): TestCfg {
|
||||||
return {
|
return {
|
||||||
@@ -112,7 +113,7 @@ describe("memory index", () => {
|
|||||||
chunking: { tokens: 4000, overlap: 0 },
|
chunking: { tokens: 4000, overlap: 0 },
|
||||||
sync: { watch: false, onSessionStart: false, onSearch: true },
|
sync: { watch: false, onSessionStart: false, onSearch: true },
|
||||||
query: {
|
query: {
|
||||||
minScore: 0,
|
minScore: params.minScore ?? 0,
|
||||||
hybrid: params.hybrid ?? { enabled: false },
|
hybrid: params.hybrid ?? { enabled: false },
|
||||||
},
|
},
|
||||||
cache: params.cacheEnabled ? { enabled: true } : undefined,
|
cache: params.cacheEnabled ? { enabled: true } : undefined,
|
||||||
@@ -367,6 +368,25 @@ describe("memory index", () => {
|
|||||||
expect(results[0]?.path).toContain("memory/2026-01-12.md");
|
expect(results[0]?.path).toContain("memory/2026-01-12.md");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("preserves keyword-only hybrid hits when minScore exceeds text weight", async () => {
|
||||||
|
const cfg = createCfg({
|
||||||
|
storePath: indexMainPath,
|
||||||
|
minScore: 0.35,
|
||||||
|
hybrid: { enabled: true, vectorWeight: 0.7, textWeight: 0.3 },
|
||||||
|
});
|
||||||
|
const manager = await getPersistentManager(cfg);
|
||||||
|
|
||||||
|
const status = manager.status();
|
||||||
|
if (!status.fts?.available) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await manager.sync({ reason: "test" });
|
||||||
|
const results = await manager.search("zebra");
|
||||||
|
expect(results.length).toBeGreaterThan(0);
|
||||||
|
expect(results[0]?.path).toContain("memory/2026-01-12.md");
|
||||||
|
});
|
||||||
|
|
||||||
it("reports vector availability after probe", async () => {
|
it("reports vector availability after probe", async () => {
|
||||||
const cfg = createCfg({ storePath: indexVectorPath, vectorEnabled: true });
|
const cfg = createCfg({ storePath: indexVectorPath, vectorEnabled: true });
|
||||||
const manager = await getPersistentManager(cfg);
|
const manager = await getPersistentManager(cfg);
|
||||||
|
|||||||
@@ -311,8 +311,28 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem
|
|||||||
mmr: hybrid.mmr,
|
mmr: hybrid.mmr,
|
||||||
temporalDecay: hybrid.temporalDecay,
|
temporalDecay: hybrid.temporalDecay,
|
||||||
});
|
});
|
||||||
|
const strict = merged.filter((entry) => entry.score >= minScore);
|
||||||
|
if (strict.length > 0 || keywordResults.length === 0) {
|
||||||
|
return strict.slice(0, maxResults);
|
||||||
|
}
|
||||||
|
|
||||||
return merged.filter((entry) => entry.score >= minScore).slice(0, maxResults);
|
// Hybrid defaults can produce keyword-only matches with max score equal to
|
||||||
|
// textWeight (for example 0.3). If minScore is higher (for example 0.35),
|
||||||
|
// these exact lexical hits get filtered out even when they are the only
|
||||||
|
// relevant results.
|
||||||
|
const relaxedMinScore = Math.min(minScore, hybrid.textWeight);
|
||||||
|
const keywordKeys = new Set(
|
||||||
|
keywordResults.map(
|
||||||
|
(entry) => `${entry.source}:${entry.path}:${entry.startLine}:${entry.endLine}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return merged
|
||||||
|
.filter(
|
||||||
|
(entry) =>
|
||||||
|
keywordKeys.has(`${entry.source}:${entry.path}:${entry.startLine}:${entry.endLine}`) &&
|
||||||
|
entry.score >= relaxedMinScore,
|
||||||
|
)
|
||||||
|
.slice(0, maxResults);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async searchVector(
|
private async searchVector(
|
||||||
|
|||||||
Reference in New Issue
Block a user