mirror of https://github.com/openclaw/openclaw.git
Memory/QMD: diversify mixed-source search results
This commit is contained in:
parent
d7a7ebb75a
commit
544c213d42
|
|
@ -63,6 +63,7 @@ Docs: https://docs.openclaw.ai
|
|||
- CLI/Pairing: default `pairing list` and `pairing approve` to the sole available pairing channel when omitted, so TUI-only setups can recover from `pairing required` without guessing channel arguments. (#21527) Thanks @losts1.
|
||||
- TUI/Pairing: show explicit pairing-required recovery guidance after gateway disconnects that return `pairing required`, including approval steps to unblock quickstart TUI hatching on fresh installs. (#21841) Thanks @nicolinux.
|
||||
- TUI/Input: suppress duplicate backspace events arriving in the same input burst window so SSH sessions no longer delete two characters per backspace press in the composer. (#19318) Thanks @eheimer.
|
||||
- Memory/QMD: diversify mixed-source search ranking when both session and memory collections are present so session transcript hits no longer crowd out durable memory-file matches in top results. (#19913) Thanks @alextempr.
|
||||
- Auth/Onboarding: align OAuth profile-id config mapping with stored credential IDs for OpenAI Codex and Chutes flows, preventing `provider:default` mismatches when OAuth returns email-scoped credentials. (#12692) thanks @mudrii.
|
||||
- Docker: pin base images to SHA256 digests in Docker builds to prevent mutable tag drift. (#7734) Thanks @coygeek.
|
||||
- Docker/Security: run E2E and install-sh test images as non-root by adding appuser directives. Thanks @thewilloftheshadow.
|
||||
|
|
|
|||
|
|
@ -933,6 +933,86 @@ describe("QmdMemoryManager", () => {
|
|||
await manager.close();
|
||||
});
|
||||
|
||||
it("diversifies mixed session and memory search results so memory hits are retained", async () => {
|
||||
cfg = {
|
||||
...cfg,
|
||||
memory: {
|
||||
backend: "qmd",
|
||||
qmd: {
|
||||
includeDefaultMemory: false,
|
||||
update: { interval: "0s", debounceMs: 60_000, onBoot: false },
|
||||
sessions: { enabled: true },
|
||||
paths: [{ path: workspaceDir, pattern: "**/*.md", name: "workspace" }],
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
spawnMock.mockImplementation((_cmd: string, args: string[]) => {
|
||||
if (args[0] === "search" && args.includes("workspace-main")) {
|
||||
const child = createMockChild({ autoClose: false });
|
||||
emitAndClose(
|
||||
child,
|
||||
"stdout",
|
||||
JSON.stringify([{ docid: "m1", score: 0.6, snippet: "@@ -1,1\nmemory fact" }]),
|
||||
);
|
||||
return child;
|
||||
}
|
||||
if (args[0] === "search" && args.includes("sessions-main")) {
|
||||
const child = createMockChild({ autoClose: false });
|
||||
emitAndClose(
|
||||
child,
|
||||
"stdout",
|
||||
JSON.stringify([
|
||||
{ docid: "s1", score: 0.99, snippet: "@@ -1,1\nsession top 1" },
|
||||
{ docid: "s2", score: 0.95, snippet: "@@ -1,1\nsession top 2" },
|
||||
{ docid: "s3", score: 0.91, snippet: "@@ -1,1\nsession top 3" },
|
||||
{ docid: "s4", score: 0.88, snippet: "@@ -1,1\nsession top 4" },
|
||||
]),
|
||||
);
|
||||
return child;
|
||||
}
|
||||
return createMockChild();
|
||||
});
|
||||
|
||||
const { manager } = await createManager();
|
||||
const inner = manager as unknown as {
|
||||
db: { prepare: (_query: string) => { all: (arg: unknown) => unknown }; close: () => void };
|
||||
};
|
||||
inner.db = {
|
||||
prepare: (_query: string) => ({
|
||||
all: (arg: unknown) => {
|
||||
switch (arg) {
|
||||
case "m1":
|
||||
return [{ collection: "workspace-main", path: "memory/facts.md" }];
|
||||
case "s1":
|
||||
case "s2":
|
||||
case "s3":
|
||||
case "s4":
|
||||
return [
|
||||
{
|
||||
collection: "sessions-main",
|
||||
path: `${String(arg)}.md`,
|
||||
},
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
},
|
||||
}),
|
||||
close: () => {},
|
||||
};
|
||||
|
||||
const results = await manager.search("fact", {
|
||||
maxResults: 4,
|
||||
sessionKey: "agent:main:slack:dm:u123",
|
||||
});
|
||||
|
||||
expect(results).toHaveLength(4);
|
||||
expect(results.some((entry) => entry.source === "memory")).toBe(true);
|
||||
expect(results.some((entry) => entry.source === "sessions")).toBe(true);
|
||||
await manager.close();
|
||||
});
|
||||
|
||||
it("logs and continues when qmd embed times out", async () => {
|
||||
vi.useFakeTimers();
|
||||
cfg = {
|
||||
|
|
|
|||
|
|
@ -492,7 +492,7 @@ export class QmdMemoryManager implements MemorySearchManager {
|
|||
source: doc.source,
|
||||
});
|
||||
}
|
||||
return this.clampResultsByInjectedChars(results.slice(0, limit));
|
||||
return this.clampResultsByInjectedChars(this.diversifyResultsBySource(results, limit));
|
||||
}
|
||||
|
||||
async sync(params?: {
|
||||
|
|
@ -1271,6 +1271,52 @@ export class QmdMemoryManager implements MemorySearchManager {
|
|||
return clamped;
|
||||
}
|
||||
|
||||
private diversifyResultsBySource(
|
||||
results: MemorySearchResult[],
|
||||
limit: number,
|
||||
): MemorySearchResult[] {
|
||||
const target = Math.max(0, limit);
|
||||
if (target <= 0) {
|
||||
return [];
|
||||
}
|
||||
if (results.length <= 1) {
|
||||
return results.slice(0, target);
|
||||
}
|
||||
const bySource = new Map<MemorySource, MemorySearchResult[]>();
|
||||
for (const entry of results) {
|
||||
const list = bySource.get(entry.source) ?? [];
|
||||
list.push(entry);
|
||||
bySource.set(entry.source, list);
|
||||
}
|
||||
const hasSessions = bySource.has("sessions");
|
||||
const hasMemory = bySource.has("memory");
|
||||
if (!hasSessions || !hasMemory) {
|
||||
return results.slice(0, target);
|
||||
}
|
||||
const sourceOrder = Array.from(bySource.entries())
|
||||
.toSorted((a, b) => (b[1][0]?.score ?? 0) - (a[1][0]?.score ?? 0))
|
||||
.map(([source]) => source);
|
||||
const diversified: MemorySearchResult[] = [];
|
||||
while (diversified.length < target) {
|
||||
let emitted = false;
|
||||
for (const source of sourceOrder) {
|
||||
const next = bySource.get(source)?.shift();
|
||||
if (!next) {
|
||||
continue;
|
||||
}
|
||||
diversified.push(next);
|
||||
emitted = true;
|
||||
if (diversified.length >= target) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!emitted) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return diversified;
|
||||
}
|
||||
|
||||
private shouldSkipUpdate(force?: boolean): boolean {
|
||||
if (force) {
|
||||
return false;
|
||||
|
|
|
|||
Loading…
Reference in New Issue