mirror of https://github.com/openclaw/openclaw.git
fix(memory/qmd): honor embedInterval independent of update interval
This commit is contained in:
parent
acbdafc4f4
commit
19e52a1ba2
|
|
@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai
|
|||
- Security/LINE: make webhook signature validation run the timing-safe compare even when the supplied signature length is wrong, closing a small timing side-channel. (#55663) Thanks @gavyngong.
|
||||
- LINE/status: stop `openclaw status` from warning about missing credentials when sanitized LINE snapshots are already configured, while still surfacing whether the missing field is the token or secret. (#45701) Thanks @tamaosamu.
|
||||
- Gateway/health: carry webhook-vs-polling account mode from channel descriptors into runtime snapshots so passive channels like LINE and BlueBubbles skip false stale-socket health failures. (#47488) Thanks @karesansui-u.
|
||||
- Memory/QMD: honor `memory.qmd.update.embedInterval` even when regular QMD update cadence is disabled or slower by arming a dedicated embed-cadence maintenance timer, while avoiding redundant timers when regular updates are already frequent enough. (#37326) Thanks @barronlroth.
|
||||
|
||||
## 2026.3.28-beta.1
|
||||
|
||||
|
|
|
|||
|
|
@ -2025,6 +2025,140 @@ describe("QmdMemoryManager", () => {
|
|||
await manager.close();
|
||||
});
|
||||
|
||||
it("runs periodic embed maintenance even when regular update scheduling is disabled", async () => {
|
||||
vi.useFakeTimers();
|
||||
cfg = {
|
||||
...cfg,
|
||||
memory: {
|
||||
backend: "qmd",
|
||||
qmd: {
|
||||
includeDefaultMemory: false,
|
||||
searchMode: "query",
|
||||
update: {
|
||||
interval: "0s",
|
||||
debounceMs: 0,
|
||||
onBoot: false,
|
||||
embedInterval: "5m",
|
||||
},
|
||||
paths: [{ path: workspaceDir, pattern: "**/*.md", name: "workspace" }],
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const { manager } = await createManager({ mode: "full" });
|
||||
|
||||
const commandCallsBefore = spawnMock.mock.calls.filter((call: unknown[]) => {
|
||||
const args = call[1] as string[];
|
||||
return args[0] === "update" || args[0] === "embed";
|
||||
});
|
||||
expect(commandCallsBefore).toHaveLength(0);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(5 * 60_000);
|
||||
|
||||
const commandCalls = spawnMock.mock.calls
|
||||
.map((call: unknown[]) => call[1] as string[])
|
||||
.filter((args: string[]) => args[0] === "update" || args[0] === "embed");
|
||||
expect(commandCalls).toEqual([["update"], ["embed"]]);
|
||||
|
||||
await manager.close();
|
||||
});
|
||||
|
||||
it("runs periodic embed maintenance when embed cadence is faster than update cadence", async () => {
|
||||
vi.useFakeTimers();
|
||||
cfg = {
|
||||
...cfg,
|
||||
memory: {
|
||||
backend: "qmd",
|
||||
qmd: {
|
||||
includeDefaultMemory: false,
|
||||
searchMode: "query",
|
||||
update: {
|
||||
interval: "20m",
|
||||
debounceMs: 0,
|
||||
onBoot: false,
|
||||
embedInterval: "5m",
|
||||
},
|
||||
paths: [{ path: workspaceDir, pattern: "**/*.md", name: "workspace" }],
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const { manager } = await createManager({ mode: "full" });
|
||||
|
||||
await vi.advanceTimersByTimeAsync(5 * 60_000);
|
||||
|
||||
const commandCalls = spawnMock.mock.calls
|
||||
.map((call: unknown[]) => call[1] as string[])
|
||||
.filter((args: string[]) => args[0] === "update" || args[0] === "embed");
|
||||
expect(commandCalls).toEqual([["update"], ["embed"]]);
|
||||
|
||||
await manager.close();
|
||||
});
|
||||
|
||||
it("does not schedule redundant embed maintenance when regular updates are already more frequent", async () => {
|
||||
vi.useFakeTimers();
|
||||
cfg = {
|
||||
...cfg,
|
||||
memory: {
|
||||
backend: "qmd",
|
||||
qmd: {
|
||||
includeDefaultMemory: false,
|
||||
searchMode: "query",
|
||||
update: {
|
||||
interval: "5m",
|
||||
debounceMs: 0,
|
||||
onBoot: false,
|
||||
embedInterval: "20m",
|
||||
},
|
||||
paths: [{ path: workspaceDir, pattern: "**/*.md", name: "workspace" }],
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const { manager } = await createManager({ mode: "full" });
|
||||
|
||||
await vi.advanceTimersByTimeAsync(6 * 60_000);
|
||||
|
||||
const commandCalls = spawnMock.mock.calls
|
||||
.map((call: unknown[]) => call[1] as string[])
|
||||
.filter((args: string[]) => args[0] === "update" || args[0] === "embed");
|
||||
expect(commandCalls).toEqual([["update"], ["embed"]]);
|
||||
|
||||
await manager.close();
|
||||
});
|
||||
|
||||
it("does not arm periodic embed maintenance in search mode", async () => {
|
||||
vi.useFakeTimers();
|
||||
cfg = {
|
||||
...cfg,
|
||||
memory: {
|
||||
backend: "qmd",
|
||||
qmd: {
|
||||
includeDefaultMemory: false,
|
||||
searchMode: "search",
|
||||
update: {
|
||||
interval: "0s",
|
||||
debounceMs: 0,
|
||||
onBoot: false,
|
||||
embedInterval: "5m",
|
||||
},
|
||||
paths: [{ path: workspaceDir, pattern: "**/*.md", name: "workspace" }],
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const { manager } = await createManager({ mode: "full" });
|
||||
|
||||
await vi.advanceTimersByTimeAsync(5 * 60_000);
|
||||
|
||||
const commandCalls = spawnMock.mock.calls
|
||||
.map((call: unknown[]) => call[1] as string[])
|
||||
.filter((args: string[]) => args[0] === "update" || args[0] === "embed");
|
||||
expect(commandCalls).toEqual([]);
|
||||
|
||||
await manager.close();
|
||||
});
|
||||
|
||||
it("skips qmd embed in search mode even for forced sync", async () => {
|
||||
cfg = {
|
||||
...cfg,
|
||||
|
|
|
|||
|
|
@ -184,6 +184,7 @@ export class QmdMemoryManager implements MemorySearchManager {
|
|||
private readonly maxQmdOutputChars = MAX_QMD_OUTPUT_CHARS;
|
||||
private readonly sessionExporter: SessionExporterConfig | null;
|
||||
private updateTimer: NodeJS.Timeout | null = null;
|
||||
private embedTimer: NodeJS.Timeout | null = null;
|
||||
private pendingUpdate: Promise<void> | null = null;
|
||||
private queuedForcedUpdate: Promise<void> | null = null;
|
||||
private queuedForcedRuns = 0;
|
||||
|
|
@ -290,6 +291,13 @@ export class QmdMemoryManager implements MemorySearchManager {
|
|||
});
|
||||
}, this.qmd.update.intervalMs);
|
||||
}
|
||||
if (this.shouldScheduleEmbedTimer()) {
|
||||
this.embedTimer = setInterval(() => {
|
||||
void this.runUpdate("embed-interval").catch((err) => {
|
||||
log.warn(`qmd embed interval update failed (${String(err)})`);
|
||||
});
|
||||
}, this.qmd.update.embedIntervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
private bootstrapCollections(): void {
|
||||
|
|
@ -985,6 +993,10 @@ export class QmdMemoryManager implements MemorySearchManager {
|
|||
clearInterval(this.updateTimer);
|
||||
this.updateTimer = null;
|
||||
}
|
||||
if (this.embedTimer) {
|
||||
clearInterval(this.embedTimer);
|
||||
this.embedTimer = null;
|
||||
}
|
||||
this.queuedForcedRuns = 0;
|
||||
await this.pendingUpdate?.catch(() => undefined);
|
||||
await this.queuedForcedUpdate?.catch(() => undefined);
|
||||
|
|
@ -1111,6 +1123,18 @@ export class QmdMemoryManager implements MemorySearchManager {
|
|||
);
|
||||
}
|
||||
|
||||
private shouldScheduleEmbedTimer(): boolean {
|
||||
if (this.qmd.searchMode === "search") {
|
||||
return false;
|
||||
}
|
||||
const embedIntervalMs = this.qmd.update.embedIntervalMs;
|
||||
if (embedIntervalMs <= 0) {
|
||||
return false;
|
||||
}
|
||||
const updateIntervalMs = this.qmd.update.intervalMs;
|
||||
return updateIntervalMs <= 0 || updateIntervalMs > embedIntervalMs;
|
||||
}
|
||||
|
||||
private noteEmbedFailure(reason: string, err: unknown): void {
|
||||
this.embedFailureCount += 1;
|
||||
const delayMs = Math.min(
|
||||
|
|
|
|||
Loading…
Reference in New Issue