mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-09 05:19:32 +08:00
Memory: make qmd search-mode flags compatible
This commit is contained in:
@@ -135,9 +135,10 @@ out to QMD for retrieval. Key points:
|
|||||||
blocked; set `memory.qmd.update.waitForBootSync = true` to keep the previous
|
blocked; set `memory.qmd.update.waitForBootSync = true` to keep the previous
|
||||||
blocking behavior.
|
blocking behavior.
|
||||||
- Searches run via `memory.qmd.searchMode` (default `qmd query --json`; also
|
- Searches run via `memory.qmd.searchMode` (default `qmd query --json`; also
|
||||||
supports `search` and `vsearch`). If QMD fails or the binary is missing,
|
supports `search` and `vsearch`). If the selected mode rejects flags on your
|
||||||
OpenClaw automatically falls back to the builtin SQLite manager so memory tools
|
QMD build, OpenClaw retries with `qmd query`. If QMD fails or the binary is
|
||||||
keep working.
|
missing, OpenClaw automatically falls back to the builtin SQLite manager so
|
||||||
|
memory tools keep working.
|
||||||
- OpenClaw does not expose QMD embed batch-size tuning today; batch behavior is
|
- OpenClaw does not expose QMD embed batch-size tuning today; batch behavior is
|
||||||
controlled by QMD itself.
|
controlled by QMD itself.
|
||||||
- **First search may be slow**: QMD may download local GGUF models (reranker/query
|
- **First search may be slow**: QMD may download local GGUF models (reranker/query
|
||||||
|
|||||||
@@ -295,13 +295,79 @@ describe("QmdMemoryManager", () => {
|
|||||||
if (!manager) {
|
if (!manager) {
|
||||||
throw new Error("manager missing");
|
throw new Error("manager missing");
|
||||||
}
|
}
|
||||||
|
const maxResults = resolved.qmd?.limits.maxResults;
|
||||||
|
if (!maxResults) {
|
||||||
|
throw new Error("qmd maxResults missing");
|
||||||
|
}
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
manager.search("test", { sessionKey: "agent:main:slack:dm:u123" }),
|
manager.search("test", { sessionKey: "agent:main:slack:dm:u123" }),
|
||||||
).resolves.toEqual([]);
|
).resolves.toEqual([]);
|
||||||
|
|
||||||
expect(spawnMock.mock.calls.some((call) => call[1]?.[0] === "search")).toBe(true);
|
const searchCall = spawnMock.mock.calls.find((call) => call[1]?.[0] === "search");
|
||||||
|
expect(searchCall?.[1]).toEqual(["search", "test", "--json"]);
|
||||||
expect(spawnMock.mock.calls.some((call) => call[1]?.[0] === "query")).toBe(false);
|
expect(spawnMock.mock.calls.some((call) => call[1]?.[0] === "query")).toBe(false);
|
||||||
|
expect(maxResults).toBeGreaterThan(0);
|
||||||
|
await manager.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("retries search with qmd query when configured mode rejects flags", async () => {
|
||||||
|
cfg = {
|
||||||
|
...cfg,
|
||||||
|
memory: {
|
||||||
|
backend: "qmd",
|
||||||
|
qmd: {
|
||||||
|
includeDefaultMemory: false,
|
||||||
|
searchMode: "search",
|
||||||
|
update: { interval: "0s", debounceMs: 60_000, onBoot: false },
|
||||||
|
paths: [{ path: workspaceDir, pattern: "**/*.md", name: "workspace" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as OpenClawConfig;
|
||||||
|
spawnMock.mockImplementation((_cmd: string, args: string[]) => {
|
||||||
|
if (args[0] === "search") {
|
||||||
|
const child = createMockChild({ autoClose: false });
|
||||||
|
setTimeout(() => {
|
||||||
|
child.stderr.emit("data", "unknown flag: --json");
|
||||||
|
child.closeWith(2);
|
||||||
|
}, 0);
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
if (args[0] === "query") {
|
||||||
|
const child = createMockChild({ autoClose: false });
|
||||||
|
setTimeout(() => {
|
||||||
|
child.stdout.emit("data", "[]");
|
||||||
|
child.closeWith(0);
|
||||||
|
}, 0);
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
return createMockChild();
|
||||||
|
});
|
||||||
|
|
||||||
|
const resolved = resolveMemoryBackendConfig({ cfg, agentId });
|
||||||
|
const manager = await QmdMemoryManager.create({ cfg, agentId, resolved });
|
||||||
|
expect(manager).toBeTruthy();
|
||||||
|
if (!manager) {
|
||||||
|
throw new Error("manager missing");
|
||||||
|
}
|
||||||
|
const maxResults = resolved.qmd?.limits.maxResults;
|
||||||
|
if (!maxResults) {
|
||||||
|
throw new Error("qmd maxResults missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
manager.search("test", { sessionKey: "agent:main:slack:dm:u123" }),
|
||||||
|
).resolves.toEqual([]);
|
||||||
|
|
||||||
|
const searchAndQueryCalls = spawnMock.mock.calls
|
||||||
|
.map((call) => call[1])
|
||||||
|
.filter(
|
||||||
|
(args): args is string[] => Array.isArray(args) && ["search", "query"].includes(args[0]),
|
||||||
|
);
|
||||||
|
expect(searchAndQueryCalls).toEqual([
|
||||||
|
["search", "test", "--json"],
|
||||||
|
["query", "test", "--json", "-n", String(maxResults)],
|
||||||
|
]);
|
||||||
await manager.close();
|
await manager.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -254,14 +254,29 @@ export class QmdMemoryManager implements MemorySearchManager {
|
|||||||
opts?.maxResults ?? this.qmd.limits.maxResults,
|
opts?.maxResults ?? this.qmd.limits.maxResults,
|
||||||
);
|
);
|
||||||
const qmdSearchCommand = this.qmd.searchMode;
|
const qmdSearchCommand = this.qmd.searchMode;
|
||||||
const args = [qmdSearchCommand, trimmed, "--json", "-n", String(limit)];
|
const args = this.buildSearchArgs(qmdSearchCommand, trimmed, limit);
|
||||||
let stdout: string;
|
let stdout: string;
|
||||||
try {
|
try {
|
||||||
const result = await this.runQmd(args, { timeoutMs: this.qmd.limits.timeoutMs });
|
const result = await this.runQmd(args, { timeoutMs: this.qmd.limits.timeoutMs });
|
||||||
stdout = result.stdout;
|
stdout = result.stdout;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.warn(`qmd ${qmdSearchCommand} failed: ${String(err)}`);
|
if (qmdSearchCommand !== "query" && this.isUnsupportedQmdOptionError(err)) {
|
||||||
throw err instanceof Error ? err : new Error(String(err));
|
log.warn(
|
||||||
|
`qmd ${qmdSearchCommand} does not support configured flags; retrying search with qmd query`,
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
const fallback = await this.runQmd(this.buildSearchArgs("query", trimmed, limit), {
|
||||||
|
timeoutMs: this.qmd.limits.timeoutMs,
|
||||||
|
});
|
||||||
|
stdout = fallback.stdout;
|
||||||
|
} catch (fallbackErr) {
|
||||||
|
log.warn(`qmd query fallback failed: ${String(fallbackErr)}`);
|
||||||
|
throw fallbackErr instanceof Error ? fallbackErr : new Error(String(fallbackErr));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn(`qmd ${qmdSearchCommand} failed: ${String(err)}`);
|
||||||
|
throw err instanceof Error ? err : new Error(String(err));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let parsed: QmdQueryResult[] = [];
|
let parsed: QmdQueryResult[] = [];
|
||||||
try {
|
try {
|
||||||
@@ -881,6 +896,18 @@ export class QmdMemoryManager implements MemorySearchManager {
|
|||||||
return normalized.includes("sqlite_busy") || normalized.includes("database is locked");
|
return normalized.includes("sqlite_busy") || normalized.includes("database is locked");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isUnsupportedQmdOptionError(err: unknown): boolean {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
const normalized = message.toLowerCase();
|
||||||
|
return (
|
||||||
|
normalized.includes("unknown flag") ||
|
||||||
|
normalized.includes("unknown option") ||
|
||||||
|
normalized.includes("unrecognized option") ||
|
||||||
|
normalized.includes("flag provided but not defined") ||
|
||||||
|
normalized.includes("unexpected argument")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private createQmdBusyError(err: unknown): Error {
|
private createQmdBusyError(err: unknown): Error {
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
return new Error(`qmd index busy while reading results: ${message}`);
|
return new Error(`qmd index busy while reading results: ${message}`);
|
||||||
@@ -896,4 +923,15 @@ export class QmdMemoryManager implements MemorySearchManager {
|
|||||||
new Promise<void>((resolve) => setTimeout(resolve, SEARCH_PENDING_UPDATE_WAIT_MS)),
|
new Promise<void>((resolve) => setTimeout(resolve, SEARCH_PENDING_UPDATE_WAIT_MS)),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private buildSearchArgs(
|
||||||
|
command: "query" | "search" | "vsearch",
|
||||||
|
query: string,
|
||||||
|
limit: number,
|
||||||
|
): string[] {
|
||||||
|
if (command === "query") {
|
||||||
|
return ["query", query, "--json", "-n", String(limit)];
|
||||||
|
}
|
||||||
|
return [command, query, "--json"];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user