mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 16:28:26 +00:00
Memory/QMD: self-heal null-byte collection metadata on update
This commit is contained in:
@@ -32,6 +32,7 @@ const log = createSubsystemLogger("memory");
|
|||||||
const SNIPPET_HEADER_RE = /@@\s*-([0-9]+),([0-9]+)/;
|
const SNIPPET_HEADER_RE = /@@\s*-([0-9]+),([0-9]+)/;
|
||||||
const SEARCH_PENDING_UPDATE_WAIT_MS = 500;
|
const SEARCH_PENDING_UPDATE_WAIT_MS = 500;
|
||||||
const MAX_QMD_OUTPUT_CHARS = 200_000;
|
const MAX_QMD_OUTPUT_CHARS = 200_000;
|
||||||
|
const NUL_MARKER_RE = /(?:\^@|\\0|\\x00|\\u0000|null\s*byte|nul\s*byte)/i;
|
||||||
|
|
||||||
type CollectionRoot = {
|
type CollectionRoot = {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -97,6 +98,7 @@ export class QmdMemoryManager implements MemorySearchManager {
|
|||||||
private db: SqliteDatabase | null = null;
|
private db: SqliteDatabase | null = null;
|
||||||
private lastUpdateAt: number | null = null;
|
private lastUpdateAt: number | null = null;
|
||||||
private lastEmbedAt: number | null = null;
|
private lastEmbedAt: number | null = null;
|
||||||
|
private attemptedNullByteCollectionRepair = false;
|
||||||
|
|
||||||
private constructor(params: {
|
private constructor(params: {
|
||||||
cfg: OpenClawConfig;
|
cfg: OpenClawConfig;
|
||||||
@@ -228,27 +230,10 @@ export class QmdMemoryManager implements MemorySearchManager {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await this.runQmd(
|
await this.addCollection(collection.path, collection.name, collection.pattern);
|
||||||
[
|
|
||||||
"collection",
|
|
||||||
"add",
|
|
||||||
collection.path,
|
|
||||||
"--name",
|
|
||||||
collection.name,
|
|
||||||
"--mask",
|
|
||||||
collection.pattern,
|
|
||||||
],
|
|
||||||
{
|
|
||||||
timeoutMs: this.qmd.update.commandTimeoutMs,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
// Idempotency: qmd exits non-zero if the collection name already exists.
|
if (this.isCollectionAlreadyExistsError(message)) {
|
||||||
if (message.toLowerCase().includes("already exists")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (message.toLowerCase().includes("exists")) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
log.warn(`qmd collection add failed for ${collection.name}: ${message}`);
|
log.warn(`qmd collection add failed for ${collection.name}: ${message}`);
|
||||||
@@ -256,6 +241,71 @@ export class QmdMemoryManager implements MemorySearchManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isCollectionAlreadyExistsError(message: string): boolean {
|
||||||
|
const lower = message.toLowerCase();
|
||||||
|
return lower.includes("already exists") || lower.includes("exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
private isCollectionMissingError(message: string): boolean {
|
||||||
|
const lower = message.toLowerCase();
|
||||||
|
return (
|
||||||
|
lower.includes("not found") || lower.includes("does not exist") || lower.includes("missing")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async removeCollection(name: string): Promise<void> {
|
||||||
|
await this.runQmd(["collection", "remove", name], {
|
||||||
|
timeoutMs: this.qmd.update.commandTimeoutMs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private shouldRepairNullByteCollectionError(err: unknown): boolean {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
const lower = message.toLowerCase();
|
||||||
|
return (
|
||||||
|
(lower.includes("enotdir") || lower.includes("not a directory")) &&
|
||||||
|
NUL_MARKER_RE.test(message)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async tryRepairNullByteCollections(err: unknown, reason: string): Promise<boolean> {
|
||||||
|
if (this.attemptedNullByteCollectionRepair) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!this.shouldRepairNullByteCollectionError(err)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.attemptedNullByteCollectionRepair = true;
|
||||||
|
log.warn(
|
||||||
|
`qmd update failed with suspected null-byte collection metadata (${reason}); rebuilding managed collections and retrying once`,
|
||||||
|
);
|
||||||
|
for (const collection of this.qmd.collections) {
|
||||||
|
try {
|
||||||
|
await this.removeCollection(collection.name);
|
||||||
|
} catch (removeErr) {
|
||||||
|
const removeMessage = removeErr instanceof Error ? removeErr.message : String(removeErr);
|
||||||
|
if (!this.isCollectionMissingError(removeMessage)) {
|
||||||
|
log.warn(`qmd collection remove failed for ${collection.name}: ${removeMessage}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await this.addCollection(collection.path, collection.name, collection.pattern);
|
||||||
|
} catch (addErr) {
|
||||||
|
const addMessage = addErr instanceof Error ? addErr.message : String(addErr);
|
||||||
|
if (!this.isCollectionAlreadyExistsError(addMessage)) {
|
||||||
|
log.warn(`qmd collection add failed for ${collection.name}: ${addMessage}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
async search(
|
async search(
|
||||||
query: string,
|
query: string,
|
||||||
opts?: { maxResults?: number; minScore?: number; sessionKey?: string },
|
opts?: { maxResults?: number; minScore?: number; sessionKey?: string },
|
||||||
@@ -470,7 +520,14 @@ export class QmdMemoryManager implements MemorySearchManager {
|
|||||||
if (this.sessionExporter) {
|
if (this.sessionExporter) {
|
||||||
await this.exportSessions();
|
await this.exportSessions();
|
||||||
}
|
}
|
||||||
await this.runQmd(["update"], { timeoutMs: this.qmd.update.updateTimeoutMs });
|
try {
|
||||||
|
await this.runQmd(["update"], { timeoutMs: this.qmd.update.updateTimeoutMs });
|
||||||
|
} catch (err) {
|
||||||
|
if (!(await this.tryRepairNullByteCollections(err, reason))) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
await this.runQmd(["update"], { timeoutMs: this.qmd.update.updateTimeoutMs });
|
||||||
|
}
|
||||||
const embedIntervalMs = this.qmd.update.embedIntervalMs;
|
const embedIntervalMs = this.qmd.update.embedIntervalMs;
|
||||||
const shouldEmbed =
|
const shouldEmbed =
|
||||||
Boolean(force) ||
|
Boolean(force) ||
|
||||||
|
|||||||
Reference in New Issue
Block a user