mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 20:21:23 +00:00
Memory/QMD: make status checks side-effect free
This commit is contained in:
@@ -253,8 +253,9 @@ export async function runMemoryStatus(opts: MemoryCommandOptions) {
|
||||
}> = [];
|
||||
|
||||
for (const agentId of agentIds) {
|
||||
const managerPurpose = opts.index ? "default" : "status";
|
||||
await withManager<MemoryManager>({
|
||||
getManager: () => getMemorySearchManager({ cfg, agentId }),
|
||||
getManager: () => getMemorySearchManager({ cfg, agentId, purpose: managerPurpose }),
|
||||
onMissing: (error) => defaultRuntime.log(error ?? "Memory search disabled."),
|
||||
onCloseError: (err) =>
|
||||
defaultRuntime.error(`Memory manager close failed: ${formatErrorMessage(err)}`),
|
||||
|
||||
@@ -158,7 +158,7 @@ export async function scanStatus(
|
||||
return null;
|
||||
}
|
||||
const agentId = agentStatus.defaultId ?? "main";
|
||||
const { manager } = await getMemorySearchManager({ cfg, agentId });
|
||||
const { manager } = await getMemorySearchManager({ cfg, agentId, purpose: "status" });
|
||||
if (!manager) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -189,6 +189,26 @@ describe("QmdMemoryManager", () => {
|
||||
await manager?.close();
|
||||
});
|
||||
|
||||
it("skips qmd command side effects in status mode initialization", async () => {
|
||||
cfg = {
|
||||
...cfg,
|
||||
memory: {
|
||||
backend: "qmd",
|
||||
qmd: {
|
||||
includeDefaultMemory: false,
|
||||
update: { interval: "5m", debounceMs: 60_000, onBoot: true },
|
||||
paths: [{ path: workspaceDir, pattern: "**/*.md", name: "workspace" }],
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const resolved = resolveMemoryBackendConfig({ cfg, agentId });
|
||||
const manager = await QmdMemoryManager.create({ cfg, agentId, resolved, mode: "status" });
|
||||
expect(manager).toBeTruthy();
|
||||
expect(spawnMock).not.toHaveBeenCalled();
|
||||
await manager?.close();
|
||||
});
|
||||
|
||||
it("can be configured to block startup on boot update", async () => {
|
||||
cfg = {
|
||||
...cfg,
|
||||
|
||||
@@ -44,18 +44,21 @@ type SessionExporterConfig = {
|
||||
collectionName: string;
|
||||
};
|
||||
|
||||
type QmdManagerMode = "full" | "status";
|
||||
|
||||
export class QmdMemoryManager implements MemorySearchManager {
|
||||
static async create(params: {
|
||||
cfg: OpenClawConfig;
|
||||
agentId: string;
|
||||
resolved: ResolvedMemoryBackendConfig;
|
||||
mode?: QmdManagerMode;
|
||||
}): Promise<QmdMemoryManager | null> {
|
||||
const resolved = params.resolved.qmd;
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
const manager = new QmdMemoryManager({ cfg: params.cfg, agentId: params.agentId, resolved });
|
||||
await manager.initialize();
|
||||
await manager.initialize(params.mode ?? "full");
|
||||
return manager;
|
||||
}
|
||||
|
||||
@@ -143,7 +146,12 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
}
|
||||
}
|
||||
|
||||
private async initialize(): Promise<void> {
|
||||
private async initialize(mode: QmdManagerMode): Promise<void> {
|
||||
this.bootstrapCollections();
|
||||
if (mode === "status") {
|
||||
return;
|
||||
}
|
||||
|
||||
await fs.mkdir(this.xdgConfigHome, { recursive: true });
|
||||
await fs.mkdir(this.xdgCacheHome, { recursive: true });
|
||||
await fs.mkdir(path.dirname(this.indexPath), { recursive: true });
|
||||
@@ -156,7 +164,6 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
// isolated while models are shared.
|
||||
await this.symlinkSharedModels();
|
||||
|
||||
this.bootstrapCollections();
|
||||
await this.ensureCollections();
|
||||
|
||||
if (this.qmd.update.onBoot) {
|
||||
|
||||
@@ -53,6 +53,8 @@ const fallbackManager = {
|
||||
close: vi.fn(async () => {}),
|
||||
};
|
||||
|
||||
const mockMemoryIndexGet = vi.fn(async () => fallbackManager);
|
||||
|
||||
vi.mock("./qmd-manager.js", () => ({
|
||||
QmdMemoryManager: {
|
||||
create: vi.fn(async () => mockPrimary),
|
||||
@@ -61,7 +63,7 @@ vi.mock("./qmd-manager.js", () => ({
|
||||
|
||||
vi.mock("./manager.js", () => ({
|
||||
MemoryIndexManager: {
|
||||
get: vi.fn(async () => fallbackManager),
|
||||
get: mockMemoryIndexGet,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -83,6 +85,8 @@ beforeEach(() => {
|
||||
fallbackManager.probeEmbeddingAvailability.mockClear();
|
||||
fallbackManager.probeVectorAvailability.mockClear();
|
||||
fallbackManager.close.mockClear();
|
||||
mockMemoryIndexGet.mockReset();
|
||||
mockMemoryIndexGet.mockResolvedValue(fallbackManager);
|
||||
QmdMemoryManager.create.mockClear();
|
||||
});
|
||||
|
||||
@@ -126,6 +130,32 @@ describe("getMemorySearchManager caching", () => {
|
||||
expect(QmdMemoryManager.create).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("does not cache status-only qmd managers", async () => {
|
||||
const agentId = "status-agent";
|
||||
const cfg = {
|
||||
memory: { backend: "qmd", qmd: {} },
|
||||
agents: { list: [{ id: agentId, default: true, workspace: "/tmp/workspace" }] },
|
||||
} as const;
|
||||
|
||||
const first = await getMemorySearchManager({ cfg, agentId, purpose: "status" });
|
||||
const second = await getMemorySearchManager({ cfg, agentId, purpose: "status" });
|
||||
|
||||
expect(first.manager).toBeTruthy();
|
||||
expect(second.manager).toBeTruthy();
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
expect(QmdMemoryManager.create).toHaveBeenCalledTimes(2);
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
expect(QmdMemoryManager.create).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({ agentId, mode: "status" }),
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
expect(QmdMemoryManager.create).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({ agentId, mode: "status" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not evict a newer cached wrapper when closing an older failed wrapper", async () => {
|
||||
const retryAgentId = "retry-agent-close";
|
||||
const cfg = {
|
||||
|
||||
@@ -19,13 +19,17 @@ export type MemorySearchManagerResult = {
|
||||
export async function getMemorySearchManager(params: {
|
||||
cfg: OpenClawConfig;
|
||||
agentId: string;
|
||||
purpose?: "default" | "status";
|
||||
}): Promise<MemorySearchManagerResult> {
|
||||
const resolved = resolveMemoryBackendConfig(params);
|
||||
if (resolved.backend === "qmd" && resolved.qmd) {
|
||||
const statusOnly = params.purpose === "status";
|
||||
const cacheKey = buildQmdCacheKey(params.agentId, resolved.qmd);
|
||||
const cached = QMD_MANAGER_CACHE.get(cacheKey);
|
||||
if (cached) {
|
||||
return { manager: cached };
|
||||
if (!statusOnly) {
|
||||
const cached = QMD_MANAGER_CACHE.get(cacheKey);
|
||||
if (cached) {
|
||||
return { manager: cached };
|
||||
}
|
||||
}
|
||||
try {
|
||||
const { QmdMemoryManager } = await import("./qmd-manager.js");
|
||||
@@ -33,8 +37,12 @@ export async function getMemorySearchManager(params: {
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
resolved,
|
||||
mode: statusOnly ? "status" : "full",
|
||||
});
|
||||
if (primary) {
|
||||
if (statusOnly) {
|
||||
return { manager: primary };
|
||||
}
|
||||
const wrapper = new FallbackMemoryManager(
|
||||
{
|
||||
primary,
|
||||
|
||||
Reference in New Issue
Block a user