fix(memory): isolate qmd boot refresh

This commit is contained in:
Peter Steinberger
2026-04-29 08:08:36 +01:00
parent 7e5d6dba80
commit afc4f06ca3
9 changed files with 115 additions and 24 deletions

View File

@@ -432,6 +432,18 @@ describe("QmdMemoryManager", () => {
};
const initialUpdateCalls = spawnMock.mock.calls.filter((call) => call[1]?.[0] === "update");
expect(initialUpdateCalls).toHaveLength(0);
const [, watchOptions] = watchMock.mock.calls[0] as unknown as [
string[],
{ ignored?: (watchPath: string) => boolean },
];
expect(watchOptions.ignored?.(path.join(workspaceDir, "node_modules", "pkg", "note.md"))).toBe(
true,
);
expect(watchOptions.ignored?.(path.join(workspaceDir, ".cache", "qmd", "note.md"))).toBe(true);
expect(watchOptions.ignored?.(path.join(workspaceDir, "vendor", "pkg", "note.md"))).toBe(true);
expect(watchOptions.ignored?.(path.join(workspaceDir, "dist", "note.md"))).toBe(true);
expect(watchOptions.ignored?.(path.join(workspaceDir, "build", "note.md"))).toBe(true);
expect(watchOptions.ignored?.(path.join(workspaceDir, "notes.md"))).toBe(false);
watcher.emit("change", path.join(workspaceDir, "notes.md"));
expect(manager.status().dirty).toBe(true);

View File

@@ -79,7 +79,11 @@ const QMD_EMBED_QUEUE_KEY = Symbol.for("openclaw.qmdEmbedQueueTail");
const QMD_UPDATE_QUEUE_KEY = Symbol.for("openclaw.qmdUpdateQueueState");
const IGNORED_MEMORY_WATCH_DIR_NAMES = new Set([
".git",
".cache",
"node_modules",
"vendor",
"dist",
"build",
".pnpm-store",
".venv",
"venv",
@@ -402,6 +406,7 @@ export class QmdMemoryManager implements MemorySearchManager {
}
private async initialize(mode: QmdManagerMode): Promise<void> {
const startTime = Date.now();
this.bootstrapCollections();
if (mode === "status") {
return;
@@ -424,10 +429,16 @@ export class QmdMemoryManager implements MemorySearchManager {
await this.ensureCollections();
if (mode === "cli") {
log.info(
`qmd manager initialized for agent "${this.agentId}" mode=cli collections=${this.qmd.collections.length} durationMs=${Date.now() - startTime}`,
);
return;
}
this.ensureWatcher();
log.info(
`qmd manager initialized for agent "${this.agentId}" mode=full collections=${this.qmd.collections.length} durationMs=${Date.now() - startTime}`,
);
if (this.qmd.update.onBoot) {
const bootRun = this.runUpdate("boot", true);
@@ -1459,6 +1470,10 @@ export class QmdMemoryManager implements MemorySearchManager {
return;
}
const run = async () => {
const startTime = Date.now();
log.debug(
`qmd sync started for agent "${this.agentId}" reason=${reason} force=${force === true}`,
);
await this.withQmdUpdateQueue(async () => {
if (this.closed) {
return;
@@ -1492,6 +1507,9 @@ export class QmdMemoryManager implements MemorySearchManager {
}
this.lastUpdateAt = Date.now();
this.docPathCache.clear();
log.info(
`qmd sync completed for agent "${this.agentId}" reason=${reason} durationMs=${Date.now() - startTime}`,
);
};
this.pendingUpdate = run().finally(() => {
this.pendingUpdate = null;
@@ -1513,7 +1531,10 @@ export class QmdMemoryManager implements MemorySearchManager {
if (watchPaths.size === 0) {
return;
}
this.watcher = chokidar.watch(Array.from(watchPaths), {
const watchPathList = Array.from(watchPaths);
const startTime = Date.now();
log.info(`qmd watcher starting for agent "${this.agentId}" paths=${watchPathList.length}`);
this.watcher = chokidar.watch(watchPathList, {
ignoreInitial: true,
ignored: (watchPath) => shouldIgnoreMemoryWatchPath(watchPath),
awaitWriteFinish: {
@@ -1528,6 +1549,11 @@ export class QmdMemoryManager implements MemorySearchManager {
this.watcher.on("add", markDirty);
this.watcher.on("change", markDirty);
this.watcher.on("unlink", markDirty);
this.watcher.once("ready", () => {
log.info(
`qmd watcher ready for agent "${this.agentId}" paths=${watchPathList.length} durationMs=${Date.now() - startTime}`,
);
});
}
private resolveCollectionWatchPath(collection: ManagedCollection): string {