mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 19:54:32 +00:00
Memory/QMD: diversify mixed-source search results
This commit is contained in:
@@ -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.
|
- 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/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.
|
- 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.
|
- 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: 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.
|
- 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();
|
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 () => {
|
it("logs and continues when qmd embed times out", async () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
cfg = {
|
cfg = {
|
||||||
|
|||||||
@@ -492,7 +492,7 @@ export class QmdMemoryManager implements MemorySearchManager {
|
|||||||
source: doc.source,
|
source: doc.source,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return this.clampResultsByInjectedChars(results.slice(0, limit));
|
return this.clampResultsByInjectedChars(this.diversifyResultsBySource(results, limit));
|
||||||
}
|
}
|
||||||
|
|
||||||
async sync(params?: {
|
async sync(params?: {
|
||||||
@@ -1271,6 +1271,52 @@ export class QmdMemoryManager implements MemorySearchManager {
|
|||||||
return clamped;
|
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 {
|
private shouldSkipUpdate(force?: boolean): boolean {
|
||||||
if (force) {
|
if (force) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
Reference in New Issue
Block a user