mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 15:00:41 +00:00
fix(memory): cap per-corpus results in corpus=all to prevent memory-hit starvation (#77337)
Wiki and memory scores use incomparable scales: wiki uses integer point sums (up to ~100+) while memory uses cosine similarity (0-1). A raw-score sort across both corpora allowed wiki hits to fill maxResults slots completely, returning 0 memory results even when memory search found matching content. Fix: when corpus=all and supplement results are present, cap each corpus at ceil(maxResults/2) before the joint sort, guaranteeing both corpora are represented in the final slice. Fixes #77337.
This commit is contained in:
@@ -45,6 +45,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Memory/wiki: preserve representation from both corpora in `corpus=all` searches while backfilling unused result capacity, so memory hits are not starved by numerically higher wiki integer scores. Fixes #77337. Thanks @hclsys.
|
||||
- Telegram: clean up tool-only draft previews after assistant message boundaries so transient `Surfacing...` tool-status bubbles do not linger when no matching final preview arrives. Thanks @BunsDev.
|
||||
- Cron: surface failed isolated-run diagnostics in `cron show`, status, and run history when requested tools are unavailable, so blocked cron runs report the actual tool-policy failure instead of a misleading green result. Fixes #75763. Thanks @RyanSandoval.
|
||||
- TUI/escape abort: track the in-flight runId after `chat.send` resolves so pressing Esc during the gap before the first gateway event aborts the run instead of repeatedly printing `no active run`. Fixes #1296. Thanks @Lukavyi and @romneyda.
|
||||
|
||||
@@ -282,6 +282,88 @@ describe("memory tools", () => {
|
||||
expect(getMemorySearchManagerMockCalls()).toBe(0);
|
||||
});
|
||||
|
||||
it("includes memory results in corpus=all even when wiki scores are numerically higher (#77337)", async () => {
|
||||
// Wiki uses integer point scores (up to ~100+); memory uses cosine similarity (0-1).
|
||||
// Raw-score sort would starve memory hits when maxResults <= number of wiki hits.
|
||||
setMemorySearchImpl(async () => [
|
||||
{
|
||||
path: "memory/note-a.md",
|
||||
startLine: 1,
|
||||
endLine: 2,
|
||||
score: 0.9,
|
||||
snippet: "Memory result A",
|
||||
source: "memory" as const,
|
||||
},
|
||||
{
|
||||
path: "memory/note-b.md",
|
||||
startLine: 1,
|
||||
endLine: 2,
|
||||
score: 0.8,
|
||||
snippet: "Memory result B",
|
||||
source: "memory" as const,
|
||||
},
|
||||
]);
|
||||
registerMemoryCorpusSupplement("memory-wiki", {
|
||||
search: async () => [
|
||||
{
|
||||
corpus: "wiki",
|
||||
path: "w1.md",
|
||||
title: "W1",
|
||||
kind: "entity",
|
||||
score: 50,
|
||||
snippet: "wiki 1",
|
||||
},
|
||||
{
|
||||
corpus: "wiki",
|
||||
path: "w2.md",
|
||||
title: "W2",
|
||||
kind: "entity",
|
||||
score: 40,
|
||||
snippet: "wiki 2",
|
||||
},
|
||||
{
|
||||
corpus: "wiki",
|
||||
path: "w3.md",
|
||||
title: "W3",
|
||||
kind: "entity",
|
||||
score: 30,
|
||||
snippet: "wiki 3",
|
||||
},
|
||||
{
|
||||
corpus: "wiki",
|
||||
path: "w4.md",
|
||||
title: "W4",
|
||||
kind: "entity",
|
||||
score: 20,
|
||||
snippet: "wiki 4",
|
||||
},
|
||||
{
|
||||
corpus: "wiki",
|
||||
path: "w5.md",
|
||||
title: "W5",
|
||||
kind: "entity",
|
||||
score: 10,
|
||||
snippet: "wiki 5",
|
||||
},
|
||||
],
|
||||
get: async () => null,
|
||||
});
|
||||
|
||||
const tool = createMemorySearchToolOrThrow();
|
||||
const result = await tool.execute("call_all_starvation", {
|
||||
query: "note",
|
||||
corpus: "all",
|
||||
maxResults: 5,
|
||||
});
|
||||
const details = result.details as { results: Array<{ corpus: string; path: string }> };
|
||||
const corpora = details.results.map((r) => r.corpus);
|
||||
|
||||
// Memory results must appear despite lower numeric scores.
|
||||
expect(corpora).toContain("memory");
|
||||
expect(corpora).toContain("wiki");
|
||||
expect(details.results.length).toBeLessThanOrEqual(5);
|
||||
});
|
||||
|
||||
it("merges memory and wiki corpus search results for corpus=all", async () => {
|
||||
registerMemoryCorpusSupplement("memory-wiki", {
|
||||
search: async () => [
|
||||
|
||||
@@ -319,14 +319,26 @@ export function createMemorySearchTool(options: {
|
||||
corpus: requestedCorpus,
|
||||
})
|
||||
: [];
|
||||
const results = [...surfacedMemoryResults, ...supplementResults]
|
||||
// When both corpora are present (corpus=all), wiki and memory scores
|
||||
// use incomparable scales (integer point vs cosine similarity), so a
|
||||
// raw-score sort lets wiki hits starve memory hits (#77337).
|
||||
// Cap each corpus at ceil(maxResults/2) before the joint sort so both
|
||||
// corpora are guaranteed representation in the final slice.
|
||||
const effectiveMax = Math.max(1, maxResults ?? 10);
|
||||
const perCorpusCap =
|
||||
requestedCorpus === "all" && supplementResults.length > 0
|
||||
? Math.ceil(effectiveMax / 2)
|
||||
: effectiveMax;
|
||||
const cappedMemory = surfacedMemoryResults.slice(0, perCorpusCap);
|
||||
const cappedSupplements = supplementResults.slice(0, perCorpusCap);
|
||||
const results = [...cappedMemory, ...cappedSupplements]
|
||||
.toSorted((left, right) => {
|
||||
if (left.score !== right.score) {
|
||||
return right.score - left.score;
|
||||
}
|
||||
return left.path.localeCompare(right.path);
|
||||
})
|
||||
.slice(0, Math.max(1, maxResults ?? 10));
|
||||
.slice(0, effectiveMax);
|
||||
return jsonResult({
|
||||
results,
|
||||
provider,
|
||||
|
||||
Reference in New Issue
Block a user