fix(memory): keep qmd embeddings active in search mode (#54509)

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
hnshah
2026-03-29 16:59:50 -07:00
committed by GitHub
parent 0033f64e19
commit 19113637e8
3 changed files with 11 additions and 14 deletions

View File

@@ -28,6 +28,7 @@ Docs: https://docs.openclaw.ai
- ACPX/runtime: derive the bundled ACPX expected version from the extension package metadata instead of hardcoding a separate literal, so plugin-local ACPX installs stop drifting out of health-check parity after version bumps. (#49089) Thanks @jiejiesks and @vincentkoc.
- Gateway/auth: make local-direct `trusted-proxy` fallback require the configured shared token instead of silently authenticating same-host callers, while keeping same-host reverse proxy identity-header flows on the normal trusted-proxy path. Thanks @zhangning-agent and @vincentkoc.
- Memory/QMD: send MCP `query` collection filters as the upstream `collections` array instead of the legacy singular `collection` field, so mcporter-backed QMD 1.1+ searches still scope correctly after the unified `query` tool migration. (#54728) Thanks @armanddp and @vincentkoc.
- Memory/QMD: keep `qmd embed` active in `search` mode too, so BM25-first setups still build a complete index for later vector and hybrid retrieval. (#54509) Thanks @hnshah and @vincentkoc.
- Memory/QMD: point `QMD_CONFIG_DIR` at the nested `xdg-config/qmd` directory so per-agent collection config resolves correctly. (#39078) Thanks @smart-tinker and @vincentkoc.
- Memory/QMD: include deduplicated default plus per-agent `memorySearch.extraPaths` when building QMD custom collections, so shared and agent-specific extra roots both get indexed consistently. (#57315) Thanks @Vitalcheffe and @vincentkoc.
- Agents/sandbox: honor `tools.sandbox.tools.alsoAllow`, let explicit sandbox re-allows remove matching built-in default-deny tools, and keep sandbox explain/error guidance aligned with the effective sandbox tool policy. (#54492) Thanks @ngutman.

View File

@@ -215,17 +215,16 @@ describe("QmdMemoryManager", () => {
const baselineCalls = spawnMock.mock.calls.length;
await manager.sync({ reason: "manual" });
expect(spawnMock.mock.calls.length).toBe(baselineCalls + 1);
expect(spawnMock.mock.calls.length).toBe(baselineCalls + 2);
await manager.sync({ reason: "manual-again" });
expect(spawnMock.mock.calls.length).toBe(baselineCalls + 1);
expect(spawnMock.mock.calls.length).toBe(baselineCalls + 2);
(manager as unknown as { lastUpdateAt: number | null }).lastUpdateAt =
Date.now() - (resolved.qmd?.update.debounceMs ?? 0) - 10;
await manager.sync({ reason: "after-wait" });
// `search` mode does not require qmd embed side effects.
expect(spawnMock.mock.calls.length).toBe(baselineCalls + 2);
expect(spawnMock.mock.calls.length).toBe(baselineCalls + 3);
await manager.close();
});
@@ -2366,7 +2365,7 @@ describe("QmdMemoryManager", () => {
await manager.close();
});
it("does not arm periodic embed maintenance in search mode", async () => {
it("arms periodic embed maintenance in search mode", async () => {
vi.useFakeTimers();
cfg = {
...cfg,
@@ -2393,12 +2392,12 @@ describe("QmdMemoryManager", () => {
const commandCalls = spawnMock.mock.calls
.map((call: unknown[]) => call[1] as string[])
.filter((args: string[]) => args[0] === "update" || args[0] === "embed");
expect(commandCalls).toEqual([]);
expect(commandCalls).toEqual([["update"], ["embed"]]);
await manager.close();
});
it("skips qmd embed in search mode even for forced sync", async () => {
it("runs qmd embed in search mode for forced sync", async () => {
cfg = {
...cfg,
memory: {
@@ -2418,7 +2417,7 @@ describe("QmdMemoryManager", () => {
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"]]);
expect(commandCalls).toEqual([["update"], ["embed"]]);
await manager.close();
});

View File

@@ -1105,9 +1105,9 @@ export class QmdMemoryManager implements MemorySearchManager {
}
private shouldRunEmbed(force?: boolean): boolean {
if (this.qmd.searchMode === "search") {
return false;
}
// Keep embeddings current regardless of the active retrieval mode.
// Search-mode indexing still needs vectors so later mode switches and
// hybrid flows do not inherit an incomplete QMD index.
const now = Date.now();
if (this.embedBackoffUntil !== null && now < this.embedBackoffUntil) {
return false;
@@ -1121,9 +1121,6 @@ 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;