From 3570aa55a74eb3fc599ba9949bd2951c5e2c5f7a Mon Sep 17 00:00:00 2001 From: SYU8384 Date: Sun, 12 Apr 2026 15:44:03 +0900 Subject: [PATCH] fix(qmd): normalize direct file collection paths --- .../host/backend-config.test.ts | 32 +++++++++++++++++++ src/memory-host-sdk/host/backend-config.ts | 18 ++++++++--- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/memory-host-sdk/host/backend-config.test.ts b/src/memory-host-sdk/host/backend-config.test.ts index 60156ad6001e..60ac9c3f4b2a 100644 --- a/src/memory-host-sdk/host/backend-config.test.ts +++ b/src/memory-host-sdk/host/backend-config.test.ts @@ -154,6 +154,38 @@ describe("resolveMemoryBackendConfig", () => { expect(custom?.path).toBe(path.resolve(workspaceRoot, "notes")); }); + it("normalizes direct file paths into parent-root collections", async () => { + const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "qmd-backend-config-file-")); + const workspaceDir = path.join(tmpRoot, "workspace"); + const sharedDir = path.join(tmpRoot, "shared"); + const sharedFile = path.join(sharedDir, "shared-memory.md"); + try { + await fs.mkdir(workspaceDir, { recursive: true }); + await fs.mkdir(sharedDir, { recursive: true }); + await fs.writeFile(sharedFile, "# Shared memory\n", "utf8"); + const cfg = { + agents: { + defaults: { workspace: workspaceDir }, + list: [{ id: "main", default: true, workspace: workspaceDir }], + }, + memory: { + backend: "qmd", + qmd: { + includeDefaultMemory: false, + paths: [{ path: sharedFile, name: "shared-memory" }], + }, + }, + } as OpenClawConfig; + const resolved = resolveMemoryBackendConfig({ cfg, agentId: "main" }); + const collection = resolved.qmd?.collections.find((entry) => entry.name === "shared-memory"); + expect(collection).toBeDefined(); + expect(collection?.path).toBe(sharedDir); + expect(collection?.pattern).toBe("shared-memory.md"); + } finally { + await fs.rm(tmpRoot, { recursive: true, force: true }); + } + }); + it("scopes qmd collection names per agent", () => { const cfg = qmdMultiAgentConfig([{ path: "notes", name: "workspace", pattern: "**/*.md" }]); const mainNames = resolveCollectionNamesForAgent(cfg, "main"); diff --git a/src/memory-host-sdk/host/backend-config.ts b/src/memory-host-sdk/host/backend-config.ts index 05b9f6ca1102..0ca63a982fd3 100644 --- a/src/memory-host-sdk/host/backend-config.ts +++ b/src/memory-host-sdk/host/backend-config.ts @@ -283,21 +283,31 @@ function resolveCustomPaths( } catch { return; } - const pattern = normalizeOptionalString(entry.pattern) || "**/*.md"; - const dedupeKey = `${resolved}\u0000${pattern}`; + let collectionPath = resolved; + let pattern = normalizeOptionalString(entry.pattern) || "**/*.md"; + try { + const stat = fs.statSync(resolved); + if (stat.isFile()) { + collectionPath = path.dirname(resolved); + pattern = path.basename(resolved); + } + } catch { + // Leave unresolved/missing paths as-is; downstream collection setup can decide how to handle them. + } + const dedupeKey = `${collectionPath}\u0000${pattern}`; if (seenRoots.has(dedupeKey)) { return; } seenRoots.add(dedupeKey); const explicitName = entry.name?.trim(); const baseName = - explicitName && !isPathInsideRoot(resolved, workspaceDir) + explicitName && !isPathInsideRoot(collectionPath, workspaceDir) ? explicitName : scopeCollectionBase(explicitName || `custom-${index + 1}`, agentId); const name = ensureUniqueName(baseName, existing); collections.push({ name, - path: resolved, + path: collectionPath, pattern, kind: "custom", });