mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-20 16:23:55 +02:00
fix: harden qmd service startup
This commit is contained in:
@@ -77,7 +77,7 @@ Docs: https://docs.openclaw.ai
|
||||
- iMessage: retry transient `watch.subscribe` startup failures before tearing down the monitor, and sanitize startup error logging so brief local transport stalls do not immediately bounce the channel or leak raw imsg RPC payloads into logs. (#65393) Thanks @vincentkoc.
|
||||
- CLI/audio providers: report env-authenticated providers as configured in `openclaw infer audio providers --json`, while keeping trusted workspace provider env lookup defaults stable during auth setup. (#65491)
|
||||
- Plugins/install: reinstall bundled runtime packages when the matching platform native optional child is missing, so packaged Windows installs can recover dependencies that were packed on another host OS.
|
||||
- Memory/QMD: preserve explicit `memory.qmd.command` paths when resolving the QMD backend, so service and gateway environments can use Homebrew installs without falling back to builtin search.
|
||||
- Memory/QMD: preserve explicit `memory.qmd.command` paths, create missing agent workspaces before QMD probes, and keep the current Node binary on QMD subprocess PATH so service and gateway environments do not fall back to builtin search unnecessarily.
|
||||
|
||||
## 2026.4.11
|
||||
|
||||
|
||||
@@ -2752,6 +2752,7 @@ describe("QmdMemoryManager", () => {
|
||||
"/agents/main/qmd/xdg-config/qmd",
|
||||
);
|
||||
expect(normalizePath(spawnOpts?.env?.XDG_CACHE_HOME)).toContain("/agents/main/qmd/xdg-cache");
|
||||
expect(spawnOpts?.env?.PATH?.split(path.delimiter)).toContain(path.dirname(process.execPath));
|
||||
|
||||
await manager.close();
|
||||
});
|
||||
|
||||
@@ -81,6 +81,15 @@ const IGNORED_MEMORY_WATCH_DIR_NAMES = new Set([
|
||||
"__pycache__",
|
||||
]);
|
||||
|
||||
function buildQmdProcessPath(rawPath: string | undefined): string {
|
||||
const nodeBinDir = path.dirname(process.execPath);
|
||||
const entries = rawPath?.split(path.delimiter).filter(Boolean) ?? [];
|
||||
if (entries.includes(nodeBinDir)) {
|
||||
return rawPath ?? nodeBinDir;
|
||||
}
|
||||
return [...entries, nodeBinDir].join(path.delimiter);
|
||||
}
|
||||
|
||||
type McporterState = {
|
||||
coldStartWarned: boolean;
|
||||
daemonStart: Promise<void> | null;
|
||||
@@ -308,6 +317,7 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
|
||||
this.env = {
|
||||
...process.env,
|
||||
PATH: buildQmdProcessPath(process.env.PATH),
|
||||
XDG_CONFIG_HOME: this.xdgConfigHome,
|
||||
// QMD resolves index.yml relative to QMD_CONFIG_DIR rather than XDG_CONFIG_HOME.
|
||||
// Point it at the nested qmd config directory so per-agent collections are visible.
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/memory-core-host-engine-foundation";
|
||||
import type { checkQmdBinaryAvailability as checkQmdBinaryAvailabilityFn } from "openclaw/plugin-sdk/memory-core-host-engine-qmd";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
@@ -256,6 +259,30 @@ describe("getMemorySearchManager caching", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("creates a missing agent workspace before probing qmd availability", async () => {
|
||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-qmd-workspace-"));
|
||||
const workspace = path.join(tempRoot, "missing", "workspace");
|
||||
const agentId = "missing-workspace";
|
||||
const cfg = {
|
||||
memory: { backend: "qmd", qmd: {} },
|
||||
agents: { list: [{ id: agentId, default: true, workspace }] },
|
||||
} as OpenClawConfig;
|
||||
|
||||
try {
|
||||
await getMemorySearchManager({ cfg, agentId });
|
||||
|
||||
const stat = await fs.stat(workspace);
|
||||
expect(stat.isDirectory()).toBe(true);
|
||||
expect(checkQmdBinaryAvailability).toHaveBeenCalledWith({
|
||||
command: "qmd",
|
||||
env: process.env,
|
||||
cwd: workspace,
|
||||
});
|
||||
} finally {
|
||||
await fs.rm(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("returns a cached qmd manager without probing the binary again", async () => {
|
||||
const agentId = "cached-qmd";
|
||||
const cfg = createQmdCfg(agentId);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import fs from "node:fs/promises";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import {
|
||||
createSubsystemLogger,
|
||||
@@ -68,10 +69,20 @@ export async function getMemorySearchManager(params: {
|
||||
}
|
||||
}
|
||||
|
||||
const workspaceDir = resolveAgentWorkspaceDir(params.cfg, params.agentId);
|
||||
try {
|
||||
await fs.mkdir(workspaceDir, { recursive: true });
|
||||
} catch (err) {
|
||||
log.warn(
|
||||
`qmd workspace unavailable (${workspaceDir}); falling back to builtin: ${formatErrorMessage(err)}`,
|
||||
);
|
||||
return await getBuiltinMemorySearchManager(params);
|
||||
}
|
||||
|
||||
const qmdBinary = await checkQmdBinaryAvailability({
|
||||
command: resolved.qmd.command,
|
||||
env: process.env,
|
||||
cwd: resolveAgentWorkspaceDir(params.cfg, params.agentId),
|
||||
cwd: workspaceDir,
|
||||
});
|
||||
if (!qmdBinary.available) {
|
||||
log.warn(
|
||||
@@ -112,6 +123,14 @@ export async function getMemorySearchManager(params: {
|
||||
}
|
||||
}
|
||||
|
||||
return await getBuiltinMemorySearchManager(params);
|
||||
}
|
||||
|
||||
async function getBuiltinMemorySearchManager(params: {
|
||||
cfg: OpenClawConfig;
|
||||
agentId: string;
|
||||
purpose?: "default" | "status";
|
||||
}): Promise<MemorySearchManagerResult> {
|
||||
try {
|
||||
const { MemoryIndexManager } = await loadManagerRuntime();
|
||||
const manager = await MemoryIndexManager.get(params);
|
||||
|
||||
Reference in New Issue
Block a user