fix(gateway): defer implicit qmd memory startup

This commit is contained in:
Vincent Koc
2026-04-26 17:21:50 -07:00
parent d7c173b694
commit 732a5842ee
2 changed files with 110 additions and 19 deletions

View File

@@ -5,13 +5,8 @@ const { getMemorySearchManagerMock } = vi.hoisted(() => ({
getMemorySearchManagerMock: vi.fn(),
}));
const { resolveActiveMemoryBackendConfigMock } = vi.hoisted(() => ({
resolveActiveMemoryBackendConfigMock: vi.fn(),
}));
vi.mock("../plugins/memory-runtime.js", () => ({
getActiveMemorySearchManager: getMemorySearchManagerMock,
resolveActiveMemoryBackendConfig: resolveActiveMemoryBackendConfigMock,
}));
import { startGatewayMemoryBackend } from "./server-startup-memory.js";
@@ -30,11 +25,6 @@ function createGatewayLogMock() {
describe("startGatewayMemoryBackend", () => {
beforeEach(() => {
getMemorySearchManagerMock.mockClear();
resolveActiveMemoryBackendConfigMock.mockReset();
resolveActiveMemoryBackendConfigMock.mockImplementation(({ cfg }: { cfg: OpenClawConfig }) => ({
backend: cfg.memory?.backend === "qmd" ? "qmd" : "builtin",
qmd: cfg.memory?.backend === "qmd" ? {} : undefined,
}));
});
it("skips initialization when memory backend is not qmd", async () => {
@@ -51,8 +41,14 @@ describe("startGatewayMemoryBackend", () => {
expect(log.warn).not.toHaveBeenCalled();
});
it("initializes qmd backend for each configured agent", async () => {
const cfg = createQmdConfig({ list: [{ id: "ops", default: true }, { id: "main" }] });
it("initializes qmd backend for the default and explicitly configured agents", async () => {
const cfg = createQmdConfig({
list: [
{ id: "ops", default: true },
{ id: "main", memorySearch: { enabled: true } },
{ id: "lazy" },
],
});
const log = createGatewayLogMock();
getMemorySearchManagerMock.mockResolvedValue({ manager: { search: vi.fn() } });
@@ -61,15 +57,41 @@ describe("startGatewayMemoryBackend", () => {
expect(getMemorySearchManagerMock).toHaveBeenCalledTimes(2);
expect(getMemorySearchManagerMock).toHaveBeenNthCalledWith(1, { cfg, agentId: "ops" });
expect(getMemorySearchManagerMock).toHaveBeenNthCalledWith(2, { cfg, agentId: "main" });
expect(log.info).toHaveBeenCalledTimes(1);
expect(log.info).toHaveBeenCalledWith(
'qmd memory startup initialization armed for 2 agents: "ops", "main"',
);
expect(log.info).toHaveBeenCalledWith(
'qmd memory startup initialization deferred for 1 agent: "lazy"',
);
expect(log.warn).not.toHaveBeenCalled();
});
it("initializes all qmd agents when memory search is explicitly enabled in defaults", async () => {
const cfg = createQmdConfig({
defaults: { memorySearch: { enabled: true } },
list: [{ id: "ops", default: true }, { id: "main" }],
});
const log = createGatewayLogMock();
getMemorySearchManagerMock.mockResolvedValue({ manager: { search: vi.fn() } });
await startGatewayMemoryBackend({ cfg, log });
expect(getMemorySearchManagerMock).toHaveBeenCalledTimes(2);
expect(getMemorySearchManagerMock).toHaveBeenNthCalledWith(1, { cfg, agentId: "ops" });
expect(getMemorySearchManagerMock).toHaveBeenNthCalledWith(2, { cfg, agentId: "main" });
expect(log.info).toHaveBeenCalledWith(
'qmd memory startup initialization armed for 2 agents: "ops", "main"',
);
expect(log.info).not.toHaveBeenCalledWith(expect.stringContaining("deferred"));
});
it("logs a warning when qmd manager init fails and continues with other agents", async () => {
const cfg = createQmdConfig({ list: [{ id: "main", default: true }, { id: "ops" }] });
const cfg = createQmdConfig({
list: [
{ id: "main", default: true },
{ id: "ops", memorySearch: { enabled: true } },
],
});
const log = createGatewayLogMock();
getMemorySearchManagerMock
.mockResolvedValueOnce({ manager: null, error: "qmd missing" })
@@ -105,4 +127,23 @@ describe("startGatewayMemoryBackend", () => {
);
expect(log.warn).not.toHaveBeenCalled();
});
it("does not initialize qmd managers when background work is disabled", async () => {
const cfg = {
agents: { list: [{ id: "main", default: true }] },
memory: {
backend: "qmd",
qmd: {
update: { onBoot: false, interval: "0s", embedInterval: "0s" },
},
},
} as OpenClawConfig;
const log = createGatewayLogMock();
await startGatewayMemoryBackend({ cfg, log });
expect(getMemorySearchManagerMock).not.toHaveBeenCalled();
expect(log.info).not.toHaveBeenCalled();
expect(log.warn).not.toHaveBeenCalled();
});
});

View File

@@ -1,10 +1,39 @@
import { listAgentIds } from "../agents/agent-scope.js";
import { listAgentEntries, listAgentIds, resolveDefaultAgentId } from "../agents/agent-scope.js";
import { resolveMemorySearchConfig } from "../agents/memory-search.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import {
getActiveMemorySearchManager,
resolveActiveMemoryBackendConfig,
} from "../plugins/memory-runtime.js";
resolveMemoryBackendConfig,
type ResolvedQmdConfig,
} from "../memory-host-sdk/host/backend-config.js";
import { getActiveMemorySearchManager } from "../plugins/memory-runtime.js";
import { normalizeAgentId } from "../routing/session-key.js";
function shouldStartQmdBackgroundWork(qmd: ResolvedQmdConfig): boolean {
return qmd.update.onBoot || qmd.update.intervalMs > 0 || qmd.update.embedIntervalMs > 0;
}
function hasExplicitAgentMemorySearchConfig(cfg: OpenClawConfig, agentId: string): boolean {
return listAgentEntries(cfg).some(
(entry) => normalizeAgentId(entry.id) === agentId && entry.memorySearch != null,
);
}
function shouldEagerlyStartAgentMemory(params: {
cfg: OpenClawConfig;
agentId: string;
agentCount: number;
}): boolean {
if (params.agentCount <= 1) {
return true;
}
if (params.agentId === resolveDefaultAgentId(params.cfg)) {
return true;
}
if (params.cfg.agents?.defaults?.memorySearch?.enabled === true) {
return true;
}
return hasExplicitAgentMemorySearchConfig(params.cfg, params.agentId);
}
export async function startGatewayMemoryBackend(params: {
cfg: OpenClawConfig;
@@ -12,17 +41,31 @@ export async function startGatewayMemoryBackend(params: {
}): Promise<void> {
const agentIds = listAgentIds(params.cfg);
const armedAgentIds: string[] = [];
const deferredAgentIds: string[] = [];
for (const agentId of agentIds) {
if (!resolveMemorySearchConfig(params.cfg, agentId)) {
continue;
}
const resolved = resolveActiveMemoryBackendConfig({ cfg: params.cfg, agentId });
const resolved = resolveMemoryBackendConfig({ cfg: params.cfg, agentId });
if (!resolved) {
continue;
}
if (resolved.backend !== "qmd" || !resolved.qmd) {
continue;
}
if (!shouldStartQmdBackgroundWork(resolved.qmd)) {
continue;
}
if (
!shouldEagerlyStartAgentMemory({
cfg: params.cfg,
agentId,
agentCount: agentIds.length,
})
) {
deferredAgentIds.push(agentId);
continue;
}
const { manager, error } = await getActiveMemorySearchManager({ cfg: params.cfg, agentId });
if (!manager) {
@@ -40,6 +83,13 @@ export async function startGatewayMemoryBackend(params: {
.join(", ")}`,
);
}
if (deferredAgentIds.length > 0) {
params.log.info?.(
`qmd memory startup initialization deferred for ${formatAgentCount(deferredAgentIds.length)}: ${deferredAgentIds
.map((agentId) => `"${agentId}"`)
.join(", ")}`,
);
}
}
function formatAgentCount(count: number): string {