From efc79f69a268ce3916cd56e12a381c53f3576b4a Mon Sep 17 00:00:00 2001 From: Vignesh Natarajan Date: Sat, 7 Feb 2026 19:38:04 -0800 Subject: [PATCH] Gateway: eager-init QMD backend on startup --- CHANGELOG.md | 1 + docs/concepts/memory.md | 2 + src/gateway/server-startup-memory.test.ts | 65 +++++++++++++++++++++++ src/gateway/server-startup-memory.ts | 24 +++++++++ src/gateway/server-startup.ts | 5 ++ 5 files changed, 97 insertions(+) create mode 100644 src/gateway/server-startup-memory.test.ts create mode 100644 src/gateway/server-startup-memory.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a79ee64b74..0ace1ee966 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,7 @@ Docs: https://docs.openclaw.ai - Memory: disable async batch embeddings by default for memory indexing (opt-in via `agents.defaults.memorySearch.remote.batch.enabled`). (#13069) Thanks @mcinteerj. - Memory/QMD: reuse default model cache across agents instead of re-downloading per agent. (#12114) Thanks @tyler6204. - Memory/QMD: run boot refresh in background by default, add configurable QMD maintenance timeouts, retry QMD after fallback failures, and scope QMD queries to OpenClaw-managed collections. (#9690, #9705, #10042) Thanks @vignesh07. +- Memory/QMD: initialize QMD backend on gateway startup so background update timers restart after process reloads. (#10797) Thanks @vignesh07. - Media understanding: recognize `.caf` audio attachments for transcription. (#10982) Thanks @succ985. - State dir: honor `OPENCLAW_STATE_DIR` for default device identity and canvas storage paths. (#4824) Thanks @kossoy. diff --git a/docs/concepts/memory.md b/docs/concepts/memory.md index 4d4bf7f118..1990436548 100644 --- a/docs/concepts/memory.md +++ b/docs/concepts/memory.md @@ -132,6 +132,8 @@ out to QMD for retrieval. Key points: (plus default workspace memory files), then `qmd update` + `qmd embed` run on boot and on a configurable interval (`memory.qmd.update.interval`, default 5 m). +- The gateway now initializes the QMD manager on startup, so periodic update + timers are armed even before the first `memory_search` call. - Boot refresh now runs in the background by default so chat startup is not blocked; set `memory.qmd.update.waitForBootSync = true` to keep the previous blocking behavior. diff --git a/src/gateway/server-startup-memory.test.ts b/src/gateway/server-startup-memory.test.ts new file mode 100644 index 0000000000..77a4db4d89 --- /dev/null +++ b/src/gateway/server-startup-memory.test.ts @@ -0,0 +1,65 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig } from "../config/config.js"; + +const { getMemorySearchManagerMock } = vi.hoisted(() => ({ + getMemorySearchManagerMock: vi.fn(), +})); + +vi.mock("../memory/index.js", () => ({ + getMemorySearchManager: getMemorySearchManagerMock, +})); + +import { startGatewayMemoryBackend } from "./server-startup-memory.js"; + +describe("startGatewayMemoryBackend", () => { + beforeEach(() => { + getMemorySearchManagerMock.mockReset(); + }); + + it("skips initialization when memory backend is not qmd", async () => { + const cfg = { + agents: { list: [{ id: "main", default: true }] }, + memory: { backend: "builtin" }, + } as OpenClawConfig; + const log = { info: vi.fn(), warn: vi.fn() }; + + await startGatewayMemoryBackend({ cfg, log }); + + expect(getMemorySearchManagerMock).not.toHaveBeenCalled(); + expect(log.info).not.toHaveBeenCalled(); + expect(log.warn).not.toHaveBeenCalled(); + }); + + it("initializes qmd backend for the default agent", async () => { + const cfg = { + agents: { list: [{ id: "ops", default: true }, { id: "main" }] }, + memory: { backend: "qmd", qmd: {} }, + } as OpenClawConfig; + const log = { info: vi.fn(), warn: vi.fn() }; + getMemorySearchManagerMock.mockResolvedValue({ manager: { search: vi.fn() } }); + + await startGatewayMemoryBackend({ cfg, log }); + + expect(getMemorySearchManagerMock).toHaveBeenCalledWith({ cfg, agentId: "ops" }); + expect(log.info).toHaveBeenCalledWith( + 'qmd memory startup initialization armed for agent "ops"', + ); + expect(log.warn).not.toHaveBeenCalled(); + }); + + it("logs a warning when qmd manager init fails", async () => { + const cfg = { + agents: { list: [{ id: "main", default: true }] }, + memory: { backend: "qmd", qmd: {} }, + } as OpenClawConfig; + const log = { info: vi.fn(), warn: vi.fn() }; + getMemorySearchManagerMock.mockResolvedValue({ manager: null, error: "qmd missing" }); + + await startGatewayMemoryBackend({ cfg, log }); + + expect(log.warn).toHaveBeenCalledWith( + 'qmd memory startup initialization failed for agent "main": qmd missing', + ); + expect(log.info).not.toHaveBeenCalled(); + }); +}); diff --git a/src/gateway/server-startup-memory.ts b/src/gateway/server-startup-memory.ts new file mode 100644 index 0000000000..11360e6014 --- /dev/null +++ b/src/gateway/server-startup-memory.ts @@ -0,0 +1,24 @@ +import type { OpenClawConfig } from "../config/config.js"; +import { resolveDefaultAgentId } from "../agents/agent-scope.js"; +import { resolveMemoryBackendConfig } from "../memory/backend-config.js"; +import { getMemorySearchManager } from "../memory/index.js"; + +export async function startGatewayMemoryBackend(params: { + cfg: OpenClawConfig; + log: { info?: (msg: string) => void; warn: (msg: string) => void }; +}): Promise { + const agentId = resolveDefaultAgentId(params.cfg); + const resolved = resolveMemoryBackendConfig({ cfg: params.cfg, agentId }); + if (resolved.backend !== "qmd" || !resolved.qmd) { + return; + } + + const { manager, error } = await getMemorySearchManager({ cfg: params.cfg, agentId }); + if (!manager) { + params.log.warn( + `qmd memory startup initialization failed for agent "${agentId}": ${error ?? "unknown error"}`, + ); + return; + } + params.log.info?.(`qmd memory startup initialization armed for agent "${agentId}"`); +} diff --git a/src/gateway/server-startup.ts b/src/gateway/server-startup.ts index 1971ef8a2d..e9267d855e 100644 --- a/src/gateway/server-startup.ts +++ b/src/gateway/server-startup.ts @@ -22,6 +22,7 @@ import { scheduleRestartSentinelWake, shouldWakeFromRestartSentinel, } from "./server-restart-sentinel.js"; +import { startGatewayMemoryBackend } from "./server-startup-memory.js"; export async function startGatewaySidecars(params: { cfg: ReturnType; @@ -150,6 +151,10 @@ export async function startGatewaySidecars(params: { params.log.warn(`plugin services failed to start: ${String(err)}`); } + void startGatewayMemoryBackend({ cfg: params.cfg, log: params.log }).catch((err) => { + params.log.warn(`qmd memory startup initialization failed: ${String(err)}`); + }); + if (shouldWakeFromRestartSentinel()) { setTimeout(() => { void scheduleRestartSentinelWake({ deps: params.deps });