mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-13 14:00:36 +00:00
Memory/QMD: make status checks side-effect free
This commit is contained in:
@@ -48,6 +48,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Memory/QMD: query QMD index using exact docid matches before falling back to prefix lookup for better recall correctness and index efficiency.
|
- Memory/QMD: query QMD index using exact docid matches before falling back to prefix lookup for better recall correctness and index efficiency.
|
||||||
- Memory/QMD: make QMD result JSON parsing resilient to noisy command output by extracting the first JSON array from noisy `stdout`.
|
- Memory/QMD: make QMD result JSON parsing resilient to noisy command output by extracting the first JSON array from noisy `stdout`.
|
||||||
- Memory/QMD: treat prefixed `no results found` marker output as an empty result set in qmd JSON parsing. (#11302) Thanks @blazerui.
|
- Memory/QMD: treat prefixed `no results found` marker output as an empty result set in qmd JSON parsing. (#11302) Thanks @blazerui.
|
||||||
|
- Memory/QMD: make `memory status` read-only by skipping QMD boot update/embed side effects for status-only manager checks.
|
||||||
- Memory/QMD: pass result limits to `search`/`vsearch` commands so QMD can cap results earlier.
|
- Memory/QMD: pass result limits to `search`/`vsearch` commands so QMD can cap results earlier.
|
||||||
- Memory/QMD: avoid reading full markdown files when a `from/lines` window is requested in QMD reads.
|
- Memory/QMD: avoid reading full markdown files when a `from/lines` window is requested in QMD reads.
|
||||||
- Memory/QMD: skip rewriting unchanged session export markdown files during sync to reduce disk churn.
|
- Memory/QMD: skip rewriting unchanged session export markdown files during sync to reduce disk churn.
|
||||||
|
|||||||
@@ -253,8 +253,9 @@ export async function runMemoryStatus(opts: MemoryCommandOptions) {
|
|||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
for (const agentId of agentIds) {
|
for (const agentId of agentIds) {
|
||||||
|
const managerPurpose = opts.index ? "default" : "status";
|
||||||
await withManager<MemoryManager>({
|
await withManager<MemoryManager>({
|
||||||
getManager: () => getMemorySearchManager({ cfg, agentId }),
|
getManager: () => getMemorySearchManager({ cfg, agentId, purpose: managerPurpose }),
|
||||||
onMissing: (error) => defaultRuntime.log(error ?? "Memory search disabled."),
|
onMissing: (error) => defaultRuntime.log(error ?? "Memory search disabled."),
|
||||||
onCloseError: (err) =>
|
onCloseError: (err) =>
|
||||||
defaultRuntime.error(`Memory manager close failed: ${formatErrorMessage(err)}`),
|
defaultRuntime.error(`Memory manager close failed: ${formatErrorMessage(err)}`),
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ export async function scanStatus(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const agentId = agentStatus.defaultId ?? "main";
|
const agentId = agentStatus.defaultId ?? "main";
|
||||||
const { manager } = await getMemorySearchManager({ cfg, agentId });
|
const { manager } = await getMemorySearchManager({ cfg, agentId, purpose: "status" });
|
||||||
if (!manager) {
|
if (!manager) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -189,6 +189,26 @@ describe("QmdMemoryManager", () => {
|
|||||||
await manager?.close();
|
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 () => {
|
it("can be configured to block startup on boot update", async () => {
|
||||||
cfg = {
|
cfg = {
|
||||||
...cfg,
|
...cfg,
|
||||||
|
|||||||
@@ -44,18 +44,21 @@ type SessionExporterConfig = {
|
|||||||
collectionName: string;
|
collectionName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type QmdManagerMode = "full" | "status";
|
||||||
|
|
||||||
export class QmdMemoryManager implements MemorySearchManager {
|
export class QmdMemoryManager implements MemorySearchManager {
|
||||||
static async create(params: {
|
static async create(params: {
|
||||||
cfg: OpenClawConfig;
|
cfg: OpenClawConfig;
|
||||||
agentId: string;
|
agentId: string;
|
||||||
resolved: ResolvedMemoryBackendConfig;
|
resolved: ResolvedMemoryBackendConfig;
|
||||||
|
mode?: QmdManagerMode;
|
||||||
}): Promise<QmdMemoryManager | null> {
|
}): Promise<QmdMemoryManager | null> {
|
||||||
const resolved = params.resolved.qmd;
|
const resolved = params.resolved.qmd;
|
||||||
if (!resolved) {
|
if (!resolved) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const manager = new QmdMemoryManager({ cfg: params.cfg, agentId: params.agentId, resolved });
|
const manager = new QmdMemoryManager({ cfg: params.cfg, agentId: params.agentId, resolved });
|
||||||
await manager.initialize();
|
await manager.initialize(params.mode ?? "full");
|
||||||
return manager;
|
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.xdgConfigHome, { recursive: true });
|
||||||
await fs.mkdir(this.xdgCacheHome, { recursive: true });
|
await fs.mkdir(this.xdgCacheHome, { recursive: true });
|
||||||
await fs.mkdir(path.dirname(this.indexPath), { 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.
|
// isolated while models are shared.
|
||||||
await this.symlinkSharedModels();
|
await this.symlinkSharedModels();
|
||||||
|
|
||||||
this.bootstrapCollections();
|
|
||||||
await this.ensureCollections();
|
await this.ensureCollections();
|
||||||
|
|
||||||
if (this.qmd.update.onBoot) {
|
if (this.qmd.update.onBoot) {
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ const fallbackManager = {
|
|||||||
close: vi.fn(async () => {}),
|
close: vi.fn(async () => {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mockMemoryIndexGet = vi.fn(async () => fallbackManager);
|
||||||
|
|
||||||
vi.mock("./qmd-manager.js", () => ({
|
vi.mock("./qmd-manager.js", () => ({
|
||||||
QmdMemoryManager: {
|
QmdMemoryManager: {
|
||||||
create: vi.fn(async () => mockPrimary),
|
create: vi.fn(async () => mockPrimary),
|
||||||
@@ -61,7 +63,7 @@ vi.mock("./qmd-manager.js", () => ({
|
|||||||
|
|
||||||
vi.mock("./manager.js", () => ({
|
vi.mock("./manager.js", () => ({
|
||||||
MemoryIndexManager: {
|
MemoryIndexManager: {
|
||||||
get: vi.fn(async () => fallbackManager),
|
get: mockMemoryIndexGet,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -83,6 +85,8 @@ beforeEach(() => {
|
|||||||
fallbackManager.probeEmbeddingAvailability.mockClear();
|
fallbackManager.probeEmbeddingAvailability.mockClear();
|
||||||
fallbackManager.probeVectorAvailability.mockClear();
|
fallbackManager.probeVectorAvailability.mockClear();
|
||||||
fallbackManager.close.mockClear();
|
fallbackManager.close.mockClear();
|
||||||
|
mockMemoryIndexGet.mockReset();
|
||||||
|
mockMemoryIndexGet.mockResolvedValue(fallbackManager);
|
||||||
QmdMemoryManager.create.mockClear();
|
QmdMemoryManager.create.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -126,6 +130,32 @@ describe("getMemorySearchManager caching", () => {
|
|||||||
expect(QmdMemoryManager.create).toHaveBeenCalledTimes(2);
|
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 () => {
|
it("does not evict a newer cached wrapper when closing an older failed wrapper", async () => {
|
||||||
const retryAgentId = "retry-agent-close";
|
const retryAgentId = "retry-agent-close";
|
||||||
const cfg = {
|
const cfg = {
|
||||||
|
|||||||
@@ -19,13 +19,17 @@ export type MemorySearchManagerResult = {
|
|||||||
export async function getMemorySearchManager(params: {
|
export async function getMemorySearchManager(params: {
|
||||||
cfg: OpenClawConfig;
|
cfg: OpenClawConfig;
|
||||||
agentId: string;
|
agentId: string;
|
||||||
|
purpose?: "default" | "status";
|
||||||
}): Promise<MemorySearchManagerResult> {
|
}): Promise<MemorySearchManagerResult> {
|
||||||
const resolved = resolveMemoryBackendConfig(params);
|
const resolved = resolveMemoryBackendConfig(params);
|
||||||
if (resolved.backend === "qmd" && resolved.qmd) {
|
if (resolved.backend === "qmd" && resolved.qmd) {
|
||||||
|
const statusOnly = params.purpose === "status";
|
||||||
const cacheKey = buildQmdCacheKey(params.agentId, resolved.qmd);
|
const cacheKey = buildQmdCacheKey(params.agentId, resolved.qmd);
|
||||||
const cached = QMD_MANAGER_CACHE.get(cacheKey);
|
if (!statusOnly) {
|
||||||
if (cached) {
|
const cached = QMD_MANAGER_CACHE.get(cacheKey);
|
||||||
return { manager: cached };
|
if (cached) {
|
||||||
|
return { manager: cached };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const { QmdMemoryManager } = await import("./qmd-manager.js");
|
const { QmdMemoryManager } = await import("./qmd-manager.js");
|
||||||
@@ -33,8 +37,12 @@ export async function getMemorySearchManager(params: {
|
|||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
agentId: params.agentId,
|
agentId: params.agentId,
|
||||||
resolved,
|
resolved,
|
||||||
|
mode: statusOnly ? "status" : "full",
|
||||||
});
|
});
|
||||||
if (primary) {
|
if (primary) {
|
||||||
|
if (statusOnly) {
|
||||||
|
return { manager: primary };
|
||||||
|
}
|
||||||
const wrapper = new FallbackMemoryManager(
|
const wrapper = new FallbackMemoryManager(
|
||||||
{
|
{
|
||||||
primary,
|
primary,
|
||||||
|
|||||||
Reference in New Issue
Block a user