mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-11 05:24:32 +00:00
fix(memory): harden qmd collection recovery
This commit is contained in:
@@ -304,29 +304,9 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
const result = await this.runQmd(["collection", "list", "--json"], {
|
||||
timeoutMs: this.qmd.update.commandTimeoutMs,
|
||||
});
|
||||
const parsed = JSON.parse(result.stdout) as unknown;
|
||||
if (Array.isArray(parsed)) {
|
||||
for (const entry of parsed) {
|
||||
if (typeof entry === "string") {
|
||||
existing.set(entry, {});
|
||||
} else if (entry && typeof entry === "object") {
|
||||
const name = (entry as { name?: unknown }).name;
|
||||
if (typeof name === "string") {
|
||||
const listedPath = (entry as { path?: unknown }).path;
|
||||
const listedPattern = (entry as { pattern?: unknown; mask?: unknown }).pattern;
|
||||
const listedMask = (entry as { mask?: unknown }).mask;
|
||||
existing.set(name, {
|
||||
path: typeof listedPath === "string" ? listedPath : undefined,
|
||||
pattern:
|
||||
typeof listedPattern === "string"
|
||||
? listedPattern
|
||||
: typeof listedMask === "string"
|
||||
? listedMask
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const parsed = this.parseListedCollections(result.stdout);
|
||||
for (const [name, details] of parsed) {
|
||||
existing.set(name, details);
|
||||
}
|
||||
} catch {
|
||||
// ignore; older qmd versions might not support list --json.
|
||||
@@ -444,6 +424,22 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
);
|
||||
}
|
||||
|
||||
private isMissingCollectionSearchError(err: unknown): boolean {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return this.isCollectionMissingError(message) && message.toLowerCase().includes("collection");
|
||||
}
|
||||
|
||||
private async tryRepairMissingCollectionSearch(err: unknown): Promise<boolean> {
|
||||
if (!this.isMissingCollectionSearchError(err)) {
|
||||
return false;
|
||||
}
|
||||
log.warn(
|
||||
"qmd search failed because a managed collection is missing; repairing collections and retrying once",
|
||||
);
|
||||
await this.ensureCollections();
|
||||
return true;
|
||||
}
|
||||
|
||||
private async addCollection(pathArg: string, name: string, pattern: string): Promise<void> {
|
||||
await this.runQmd(["collection", "add", pathArg, "--name", name, "--mask", pattern], {
|
||||
timeoutMs: this.qmd.update.commandTimeoutMs,
|
||||
@@ -456,6 +452,92 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
});
|
||||
}
|
||||
|
||||
private parseListedCollections(output: string): Map<string, ListedCollection> {
|
||||
const listed = new Map<string, ListedCollection>();
|
||||
const trimmed = output.trim();
|
||||
if (!trimmed) {
|
||||
return listed;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed) as unknown;
|
||||
if (Array.isArray(parsed)) {
|
||||
for (const entry of parsed) {
|
||||
if (typeof entry === "string") {
|
||||
listed.set(entry, {});
|
||||
continue;
|
||||
}
|
||||
if (!entry || typeof entry !== "object") {
|
||||
continue;
|
||||
}
|
||||
const name = (entry as { name?: unknown }).name;
|
||||
if (typeof name !== "string") {
|
||||
continue;
|
||||
}
|
||||
const listedPath = (entry as { path?: unknown }).path;
|
||||
const listedPattern = (entry as { pattern?: unknown; mask?: unknown }).pattern;
|
||||
const listedMask = (entry as { mask?: unknown }).mask;
|
||||
listed.set(name, {
|
||||
path: typeof listedPath === "string" ? listedPath : undefined,
|
||||
pattern:
|
||||
typeof listedPattern === "string"
|
||||
? listedPattern
|
||||
: typeof listedMask === "string"
|
||||
? listedMask
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
return listed;
|
||||
}
|
||||
} catch {
|
||||
// Some qmd builds ignore `--json` and still print table output.
|
||||
}
|
||||
|
||||
let currentName: string | null = null;
|
||||
for (const rawLine of output.split(/\r?\n/)) {
|
||||
const line = rawLine.trimEnd();
|
||||
if (!line.trim()) {
|
||||
currentName = null;
|
||||
continue;
|
||||
}
|
||||
const collectionLine = /^\s*([a-z0-9._-]+)\s+\(qmd:\/\/[^)]+\)\s*$/i.exec(line);
|
||||
if (collectionLine) {
|
||||
currentName = collectionLine[1];
|
||||
if (!listed.has(currentName)) {
|
||||
listed.set(currentName, {});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (/^\s*collections\b/i.test(line)) {
|
||||
continue;
|
||||
}
|
||||
const bareNameLine = /^\s*([a-z0-9._-]+)\s*$/i.exec(line);
|
||||
if (bareNameLine && !line.includes(":")) {
|
||||
currentName = bareNameLine[1];
|
||||
if (!listed.has(currentName)) {
|
||||
listed.set(currentName, {});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!currentName) {
|
||||
continue;
|
||||
}
|
||||
const patternLine = /^\s*(?:pattern|mask)\s*:\s*(.+?)\s*$/i.exec(line);
|
||||
if (patternLine) {
|
||||
const existing = listed.get(currentName) ?? {};
|
||||
existing.pattern = patternLine[1].trim();
|
||||
listed.set(currentName, existing);
|
||||
continue;
|
||||
}
|
||||
const pathLine = /^\s*path\s*:\s*(.+?)\s*$/i.exec(line);
|
||||
if (pathLine) {
|
||||
const existing = listed.get(currentName) ?? {};
|
||||
existing.path = pathLine[1].trim();
|
||||
listed.set(currentName, existing);
|
||||
}
|
||||
}
|
||||
return listed;
|
||||
}
|
||||
|
||||
private shouldRebindCollection(collection: ManagedCollection, listed: ListedCollection): boolean {
|
||||
if (!listed.path) {
|
||||
// Older qmd versions may only return names from `collection list --json`.
|
||||
@@ -547,26 +629,28 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
}
|
||||
const qmdSearchCommand = this.qmd.searchMode;
|
||||
const mcporterEnabled = this.qmd.mcporter.enabled;
|
||||
let parsed: QmdQueryResult[];
|
||||
try {
|
||||
if (mcporterEnabled) {
|
||||
const tool: "search" | "vector_search" | "deep_search" =
|
||||
qmdSearchCommand === "search"
|
||||
? "search"
|
||||
: qmdSearchCommand === "vsearch"
|
||||
? "vector_search"
|
||||
: "deep_search";
|
||||
const minScore = opts?.minScore ?? 0;
|
||||
if (collectionNames.length > 1) {
|
||||
parsed = await this.runMcporterAcrossCollections({
|
||||
tool,
|
||||
query: trimmed,
|
||||
limit,
|
||||
minScore,
|
||||
collectionNames,
|
||||
});
|
||||
} else {
|
||||
parsed = await this.runQmdSearchViaMcporter({
|
||||
const runSearchAttempt = async (
|
||||
allowMissingCollectionRepair: boolean,
|
||||
): Promise<QmdQueryResult[]> => {
|
||||
try {
|
||||
if (mcporterEnabled) {
|
||||
const tool: "search" | "vector_search" | "deep_search" =
|
||||
qmdSearchCommand === "search"
|
||||
? "search"
|
||||
: qmdSearchCommand === "vsearch"
|
||||
? "vector_search"
|
||||
: "deep_search";
|
||||
const minScore = opts?.minScore ?? 0;
|
||||
if (collectionNames.length > 1) {
|
||||
return await this.runMcporterAcrossCollections({
|
||||
tool,
|
||||
query: trimmed,
|
||||
limit,
|
||||
minScore,
|
||||
collectionNames,
|
||||
});
|
||||
}
|
||||
return await this.runQmdSearchViaMcporter({
|
||||
mcporter: this.qmd.mcporter,
|
||||
tool,
|
||||
query: trimmed,
|
||||
@@ -576,50 +660,61 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
timeoutMs: this.qmd.limits.timeoutMs,
|
||||
});
|
||||
}
|
||||
} else if (collectionNames.length > 1) {
|
||||
parsed = await this.runQueryAcrossCollections(
|
||||
trimmed,
|
||||
limit,
|
||||
collectionNames,
|
||||
qmdSearchCommand,
|
||||
);
|
||||
} else {
|
||||
if (collectionNames.length > 1) {
|
||||
return await this.runQueryAcrossCollections(
|
||||
trimmed,
|
||||
limit,
|
||||
collectionNames,
|
||||
qmdSearchCommand,
|
||||
);
|
||||
}
|
||||
const args = this.buildSearchArgs(qmdSearchCommand, trimmed, limit);
|
||||
args.push(...this.buildCollectionFilterArgs(collectionNames));
|
||||
// Always scope to managed collections (default + custom). Even for `search`/`vsearch`,
|
||||
// pass collection filters; if a given QMD build rejects these flags, we fall back to `query`.
|
||||
const result = await this.runQmd(args, { timeoutMs: this.qmd.limits.timeoutMs });
|
||||
parsed = parseQmdQueryJson(result.stdout, result.stderr);
|
||||
}
|
||||
} catch (err) {
|
||||
if (
|
||||
!mcporterEnabled &&
|
||||
qmdSearchCommand !== "query" &&
|
||||
this.isUnsupportedQmdOptionError(err)
|
||||
) {
|
||||
log.warn(
|
||||
`qmd ${qmdSearchCommand} does not support configured flags; retrying search with qmd query`,
|
||||
);
|
||||
try {
|
||||
if (collectionNames.length > 1) {
|
||||
parsed = await this.runQueryAcrossCollections(trimmed, limit, collectionNames, "query");
|
||||
} else {
|
||||
return parseQmdQueryJson(result.stdout, result.stderr);
|
||||
} catch (err) {
|
||||
if (allowMissingCollectionRepair && this.isMissingCollectionSearchError(err)) {
|
||||
throw err;
|
||||
}
|
||||
if (
|
||||
!mcporterEnabled &&
|
||||
qmdSearchCommand !== "query" &&
|
||||
this.isUnsupportedQmdOptionError(err)
|
||||
) {
|
||||
log.warn(
|
||||
`qmd ${qmdSearchCommand} does not support configured flags; retrying search with qmd query`,
|
||||
);
|
||||
try {
|
||||
if (collectionNames.length > 1) {
|
||||
return await this.runQueryAcrossCollections(trimmed, limit, collectionNames, "query");
|
||||
}
|
||||
const fallbackArgs = this.buildSearchArgs("query", trimmed, limit);
|
||||
fallbackArgs.push(...this.buildCollectionFilterArgs(collectionNames));
|
||||
const fallback = await this.runQmd(fallbackArgs, {
|
||||
timeoutMs: this.qmd.limits.timeoutMs,
|
||||
});
|
||||
parsed = parseQmdQueryJson(fallback.stdout, fallback.stderr);
|
||||
return parseQmdQueryJson(fallback.stdout, fallback.stderr);
|
||||
} catch (fallbackErr) {
|
||||
log.warn(`qmd query fallback failed: ${String(fallbackErr)}`);
|
||||
throw fallbackErr instanceof Error ? fallbackErr : new Error(String(fallbackErr));
|
||||
}
|
||||
} catch (fallbackErr) {
|
||||
log.warn(`qmd query fallback failed: ${String(fallbackErr)}`);
|
||||
throw fallbackErr instanceof Error ? fallbackErr : new Error(String(fallbackErr));
|
||||
}
|
||||
} else {
|
||||
const label = mcporterEnabled ? "mcporter/qmd" : `qmd ${qmdSearchCommand}`;
|
||||
log.warn(`${label} failed: ${String(err)}`);
|
||||
throw err instanceof Error ? err : new Error(String(err));
|
||||
}
|
||||
};
|
||||
|
||||
let parsed: QmdQueryResult[];
|
||||
try {
|
||||
parsed = await runSearchAttempt(true);
|
||||
} catch (err) {
|
||||
if (!(await this.tryRepairMissingCollectionSearch(err))) {
|
||||
throw err instanceof Error ? err : new Error(String(err));
|
||||
}
|
||||
parsed = await runSearchAttempt(false);
|
||||
}
|
||||
const results: MemorySearchResult[] = [];
|
||||
for (const entry of parsed) {
|
||||
|
||||
Reference in New Issue
Block a user