refactor: consolidate qmd mcporter state

This commit is contained in:
Peter Steinberger
2026-03-22 18:08:51 +00:00
parent 23a6e0ccd3
commit 13c239039a
2 changed files with 23 additions and 16 deletions

View File

@@ -11,6 +11,7 @@ const { logWarnMock, logDebugMock, logInfoMock } = vi.hoisted(() => ({
logDebugMock: vi.fn(),
logInfoMock: vi.fn(),
}));
const MCPORTER_STATE_KEY = Symbol.for("openclaw.mcporterState");
type MockChild = EventEmitter & {
stdout: EventEmitter;
@@ -196,8 +197,7 @@ describe("QmdMemoryManager", () => {
} else {
(process.env as NodeJS.ProcessEnv & { Path?: string }).Path = originalWindowsPath;
}
delete (globalThis as Record<string, unknown>).__openclawMcporterDaemonStart;
delete (globalThis as Record<string, unknown>).__openclawMcporterColdStartWarned;
delete (globalThis as Record<PropertyKey, unknown>)[MCPORTER_STATE_KEY];
});
it("debounces back-to-back sync calls", async () => {

View File

@@ -7,6 +7,7 @@ import type { OpenClawConfig } from "../config/config.js";
import { resolveStateDir } from "../config/paths.js";
import { writeFileWithinRoot } from "../infra/fs-safe.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { resolveGlobalSingleton } from "../shared/global-singleton.js";
import { isFileMissingError, statRegularFile } from "./fs-utils.js";
import { resolveCliSpawnInvocation, runCliCommand } from "./qmd-process.js";
import { deriveQmdScopeChannel, deriveQmdScopeChatType, isQmdScopeAllowed } from "./qmd-scope.js";
@@ -44,9 +45,22 @@ const QMD_EMBED_BACKOFF_BASE_MS = 60_000;
const QMD_EMBED_BACKOFF_MAX_MS = 60 * 60 * 1000;
const HAN_SCRIPT_RE = /[\u3400-\u9fff]/u;
const QMD_BM25_HAN_KEYWORD_LIMIT = 12;
const MCPORTER_STATE_KEY = Symbol.for("openclaw.mcporterState");
type McporterState = {
coldStartWarned: boolean;
daemonStart: Promise<void> | null;
};
let qmdEmbedQueueTail: Promise<void> = Promise.resolve();
function getMcporterState(): McporterState {
return resolveGlobalSingleton<McporterState>(MCPORTER_STATE_KEY, () => ({
coldStartWarned: false,
daemonStart: null,
}));
}
function hasHanScript(value: string): boolean {
return HAN_SCRIPT_RE.test(value);
}
@@ -1209,35 +1223,28 @@ export class QmdMemoryManager implements MemorySearchManager {
if (!mcporter.enabled) {
return;
}
const state = getMcporterState();
if (!mcporter.startDaemon) {
type McporterWarnGlobal = typeof globalThis & {
__openclawMcporterColdStartWarned?: boolean;
};
const g: McporterWarnGlobal = globalThis;
if (!g.__openclawMcporterColdStartWarned) {
g.__openclawMcporterColdStartWarned = true;
if (!state.coldStartWarned) {
state.coldStartWarned = true;
log.warn(
"mcporter qmd bridge enabled but startDaemon=false; each query may cold-start QMD MCP. Consider setting memory.qmd.mcporter.startDaemon=true to keep it warm.",
);
}
return;
}
type McporterGlobal = typeof globalThis & {
__openclawMcporterDaemonStart?: Promise<void>;
};
const g: McporterGlobal = globalThis;
if (!g.__openclawMcporterDaemonStart) {
g.__openclawMcporterDaemonStart = (async () => {
if (!state.daemonStart) {
state.daemonStart = (async () => {
try {
await this.runMcporter(["daemon", "start"], { timeoutMs: 10_000 });
} catch (err) {
log.warn(`mcporter daemon start failed: ${String(err)}`);
// Allow future searches to retry daemon start on transient failures.
delete g.__openclawMcporterDaemonStart;
state.daemonStart = null;
}
})();
}
await g.__openclawMcporterDaemonStart;
await state.daemonStart;
}
private async runMcporter(