mirror of
https://github.com/farcasclaudiu/openclaw.git
synced 2026-06-28 19:01:47 +03:00
Memory/QMD: add configurable search mode
This commit is contained in:
committed by
Vignesh
parent
c2f9f2e1cd
commit
6d9d4d04ed
@@ -25,6 +25,7 @@ describe("resolveMemoryBackendConfig", () => {
|
||||
expect(resolved.backend).toBe("qmd");
|
||||
expect(resolved.qmd?.collections.length).toBeGreaterThanOrEqual(3);
|
||||
expect(resolved.qmd?.command).toBe("qmd");
|
||||
expect(resolved.qmd?.searchMode).toBe("query");
|
||||
expect(resolved.qmd?.update.intervalMs).toBeGreaterThan(0);
|
||||
expect(resolved.qmd?.update.waitForBootSync).toBe(false);
|
||||
expect(resolved.qmd?.update.commandTimeoutMs).toBe(30_000);
|
||||
@@ -93,4 +94,18 @@ describe("resolveMemoryBackendConfig", () => {
|
||||
expect(resolved.qmd?.update.updateTimeoutMs).toBe(480_000);
|
||||
expect(resolved.qmd?.update.embedTimeoutMs).toBe(360_000);
|
||||
});
|
||||
|
||||
it("resolves qmd search mode override", () => {
|
||||
const cfg = {
|
||||
agents: { defaults: { workspace: "/tmp/memory-test" } },
|
||||
memory: {
|
||||
backend: "qmd",
|
||||
qmd: {
|
||||
searchMode: "vsearch",
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const resolved = resolveMemoryBackendConfig({ cfg, agentId: "main" });
|
||||
expect(resolved.qmd?.searchMode).toBe("vsearch");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
MemoryCitationsMode,
|
||||
MemoryQmdConfig,
|
||||
MemoryQmdIndexPath,
|
||||
MemoryQmdSearchMode,
|
||||
} from "../config/types.memory.js";
|
||||
import { resolveAgentWorkspaceDir } from "../agents/agent-scope.js";
|
||||
import { parseDurationMs } from "../cli/parse-duration.js";
|
||||
@@ -51,6 +52,7 @@ export type ResolvedQmdSessionConfig = {
|
||||
|
||||
export type ResolvedQmdConfig = {
|
||||
command: string;
|
||||
searchMode: MemoryQmdSearchMode;
|
||||
collections: ResolvedQmdCollection[];
|
||||
sessions: ResolvedQmdSessionConfig;
|
||||
update: ResolvedQmdUpdateConfig;
|
||||
@@ -64,6 +66,7 @@ const DEFAULT_CITATIONS: MemoryCitationsMode = "auto";
|
||||
const DEFAULT_QMD_INTERVAL = "5m";
|
||||
const DEFAULT_QMD_DEBOUNCE_MS = 15_000;
|
||||
const DEFAULT_QMD_TIMEOUT_MS = 4_000;
|
||||
const DEFAULT_QMD_SEARCH_MODE: MemoryQmdSearchMode = "query";
|
||||
const DEFAULT_QMD_EMBED_INTERVAL = "60m";
|
||||
const DEFAULT_QMD_COMMAND_TIMEOUT_MS = 30_000;
|
||||
const DEFAULT_QMD_UPDATE_TIMEOUT_MS = 120_000;
|
||||
@@ -171,6 +174,13 @@ function resolveLimits(raw?: MemoryQmdConfig["limits"]): ResolvedQmdLimitsConfig
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function resolveSearchMode(raw?: MemoryQmdConfig["searchMode"]): MemoryQmdSearchMode {
|
||||
if (raw === "search" || raw === "vsearch" || raw === "query") {
|
||||
return raw;
|
||||
}
|
||||
return DEFAULT_QMD_SEARCH_MODE;
|
||||
}
|
||||
|
||||
function resolveSessionConfig(
|
||||
cfg: MemoryQmdConfig["sessions"],
|
||||
workspaceDir: string,
|
||||
@@ -265,6 +275,7 @@ export function resolveMemoryBackendConfig(params: {
|
||||
const command = parsedCommand?.[0] || rawCommand.split(/\s+/)[0] || "qmd";
|
||||
const resolved: ResolvedQmdConfig = {
|
||||
command,
|
||||
searchMode: resolveSearchMode(qmdCfg?.searchMode),
|
||||
collections,
|
||||
includeDefaultMemory,
|
||||
sessions: resolveSessionConfig(qmdCfg?.sessions, workspaceDir),
|
||||
|
||||
@@ -285,6 +285,47 @@ describe("QmdMemoryManager", () => {
|
||||
await manager.close();
|
||||
});
|
||||
|
||||
it("uses configured qmd search mode command", 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.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");
|
||||
}
|
||||
|
||||
await expect(
|
||||
manager.search("test", { sessionKey: "agent:main:slack:dm:u123" }),
|
||||
).resolves.toEqual([]);
|
||||
|
||||
expect(spawnMock.mock.calls.some((call) => call[1]?.[0] === "search")).toBe(true);
|
||||
expect(spawnMock.mock.calls.some((call) => call[1]?.[0] === "query")).toBe(false);
|
||||
await manager.close();
|
||||
});
|
||||
|
||||
it("queues a forced sync behind an in-flight update", async () => {
|
||||
cfg = {
|
||||
...cfg,
|
||||
|
||||
@@ -260,7 +260,8 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
log.warn("qmd query skipped: no managed collections configured");
|
||||
return [];
|
||||
}
|
||||
const args = ["query", trimmed, "--json", "-n", String(limit), ...collectionFilterArgs];
|
||||
const qmdSearchCommand = this.qmd.searchMode;
|
||||
const args = this.buildSearchArgs(qmdSearchCommand, trimmed, limit, collectionFilterArgs);
|
||||
let stdout: string;
|
||||
let stderr: string;
|
||||
try {
|
||||
@@ -268,8 +269,25 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
stdout = result.stdout;
|
||||
stderr = result.stderr;
|
||||
} catch (err) {
|
||||
log.warn(`qmd query failed: ${String(err)}`);
|
||||
throw err instanceof Error ? err : new Error(String(err));
|
||||
if (qmdSearchCommand !== "query" && this.isUnsupportedQmdOptionError(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, collectionFilterArgs),
|
||||
{ timeoutMs: this.qmd.limits.timeoutMs },
|
||||
);
|
||||
stdout = fallback.stdout;
|
||||
stderr = fallback.stderr;
|
||||
} 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));
|
||||
}
|
||||
}
|
||||
const parsed = parseQmdQueryJson(stdout, stderr);
|
||||
const results: MemorySearchResult[] = [];
|
||||
@@ -953,6 +971,18 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
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 {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return new Error(`qmd index busy while reading results: ${message}`);
|
||||
@@ -976,4 +1006,16 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
}
|
||||
return names.flatMap((name) => ["-c", name]);
|
||||
}
|
||||
|
||||
private buildSearchArgs(
|
||||
command: "query" | "search" | "vsearch",
|
||||
query: string,
|
||||
limit: number,
|
||||
collectionFilterArgs: string[],
|
||||
): string[] {
|
||||
if (command === "query") {
|
||||
return ["query", query, "--json", "-n", String(limit), ...collectionFilterArgs];
|
||||
}
|
||||
return [command, query, "--json"];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user