From 910134b702834b032fda02a9ef1bbf193fa744b1 Mon Sep 17 00:00:00 2001 From: Altay Date: Mon, 30 Mar 2026 22:41:21 +0300 Subject: [PATCH] fix(memory): stabilize qmd collection scoping --- .../qmd-manager.slugified-paths.test.ts | 8 ++--- .../src/memory/qmd-manager.test.ts | 14 ++++---- .../src/host/backend-config.test.ts | 33 +++++++++++++++++++ .../src/host/backend-config.ts | 18 +++++++--- 4 files changed, 58 insertions(+), 15 deletions(-) diff --git a/extensions/memory-core/src/memory/qmd-manager.slugified-paths.test.ts b/extensions/memory-core/src/memory/qmd-manager.slugified-paths.test.ts index 3c69cdcd262..928a0defafc 100644 --- a/extensions/memory-core/src/memory/qmd-manager.slugified-paths.test.ts +++ b/extensions/memory-core/src/memory/qmd-manager.slugified-paths.test.ts @@ -267,7 +267,7 @@ describe("QmdMemoryManager slugified path resolution", () => { "stdout", JSON.stringify([ { - file: "qmd://vault-main/topics/sub-category/topic-name.md", + file: "qmd://vault/topics/sub-category/topic-name.md", score: 0.81, snippet: "@@ -1,1\nvault memory", }, @@ -281,7 +281,7 @@ describe("QmdMemoryManager slugified path resolution", () => { const { manager } = await createManager({ cfg }); installIndexedPathStub({ manager, - collection: "vault-main", + collection: "vault", normalizedPath: "topics/sub-category/topic-name.md", actualPath: actualRelative, }); @@ -291,7 +291,7 @@ describe("QmdMemoryManager slugified path resolution", () => { }); expect(results).toEqual([ { - path: `qmd/vault-main/${actualRelative}`, + path: `qmd/vault/${actualRelative}`, startLine: 1, endLine: 1, score: 0.81, @@ -301,7 +301,7 @@ describe("QmdMemoryManager slugified path resolution", () => { ]); await expect(manager.readFile({ relPath: results[0]!.path })).resolves.toEqual({ - path: `qmd/vault-main/${actualRelative}`, + path: `qmd/vault/${actualRelative}`, text: "vault memory", }); }); diff --git a/extensions/memory-core/src/memory/qmd-manager.test.ts b/extensions/memory-core/src/memory/qmd-manager.test.ts index 3031b295df8..9a6782e954c 100644 --- a/extensions/memory-core/src/memory/qmd-manager.test.ts +++ b/extensions/memory-core/src/memory/qmd-manager.test.ts @@ -1742,7 +1742,7 @@ describe("QmdMemoryManager", () => { .filter((args: string[]) => args[0] === "search"); expect(searchCalls).toEqual([ ["search", "test", "--json", "-n", String(maxResults), "-c", "workspace-main"], - ["search", "test", "--json", "-n", String(maxResults), "-c", "notes"], + ["search", "test", "--json", "-n", String(maxResults), "-c", "notes-main"], ]); await manager.close(); }); @@ -1828,7 +1828,7 @@ describe("QmdMemoryManager", () => { .filter((args: string[]) => args[0] === "query"); expect(queryCalls).toEqual([ ["query", "test", "--json", "-n", String(maxResults), "-c", "workspace-main"], - ["query", "test", "--json", "-n", String(maxResults), "-c", "notes"], + ["query", "test", "--json", "-n", String(maxResults), "-c", "notes-main"], ]); await manager.close(); }); @@ -1880,7 +1880,7 @@ describe("QmdMemoryManager", () => { expect(searchAndQueryCalls).toEqual([ ["search", "test", "--json", "-n", String(maxResults), "-c", "workspace-main"], ["query", "test", "--json", "-n", String(maxResults), "-c", "workspace-main"], - ["query", "test", "--json", "-n", String(maxResults), "-c", "notes"], + ["query", "test", "--json", "-n", String(maxResults), "-c", "notes-main"], ]); await manager.close(); }); @@ -2217,7 +2217,7 @@ describe("QmdMemoryManager", () => { await manager.search("hello", { sessionKey: "agent:main:slack:dm:u123" }); expect(selectors).toEqual(["qmd.hybrid_search", "qmd.hybrid_search"]); - expect(collections).toEqual(["workspace-a", "workspace-b"]); + expect(collections).toEqual(["workspace-a-main", "workspace-b-main"]); await manager.close(); }); @@ -3326,7 +3326,7 @@ describe("QmdMemoryManager", () => { ); return child; } - if (args[0] === "search" && args.includes("notes")) { + if (args[0] === "search" && args.includes("notes-main")) { const child = createMockChild({ autoClose: false }); emitAndClose(child, "stdout", "[]"); return child; @@ -3542,14 +3542,14 @@ describe("QmdMemoryManager", () => { ); return child; } - if (args[0] === "search" && args.includes("notes")) { + if (args[0] === "search" && args.includes("notes-main")) { const child = createMockChild({ autoClose: false }); emitAndClose( child, "stdout", JSON.stringify([ { - file: "qmd://notes/guide.md", + file: "qmd://notes-main/guide.md", score: 0.7, snippet: "@@ -1,1\nnotes guide", }, diff --git a/packages/memory-host-sdk/src/host/backend-config.test.ts b/packages/memory-host-sdk/src/host/backend-config.test.ts index 9b944760f22..8cef299158e 100644 --- a/packages/memory-host-sdk/src/host/backend-config.test.ts +++ b/packages/memory-host-sdk/src/host/backend-config.test.ts @@ -173,6 +173,39 @@ describe("resolveMemoryBackendConfig", () => { } }); + it("keeps unresolved child paths under a symlinked workspace agent-scoped", async () => { + const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "qmd-backend-config-")); + const realRootDir = path.join(tmpRoot, "real-root"); + const aliasRootDir = path.join(tmpRoot, "alias-root"); + const workspaceDir = path.join(realRootDir, "workspace"); + const workspaceAliasDir = path.join(aliasRootDir, "workspace"); + try { + await fs.mkdir(workspaceDir, { recursive: true }); + await fs.symlink(realRootDir, aliasRootDir); + const cfg = { + agents: { + defaults: { workspace: workspaceDir }, + list: [{ id: "main", default: true, workspace: workspaceDir }], + }, + memory: { + backend: "qmd", + qmd: { + includeDefaultMemory: false, + paths: [ + { path: path.join(workspaceAliasDir, "notes"), name: "notes", pattern: "**/*.md" }, + ], + }, + }, + } as OpenClawConfig; + const resolved = resolveMemoryBackendConfig({ cfg, agentId: "main" }); + const names = new Set((resolved.qmd?.collections ?? []).map((collection) => collection.name)); + expect(names.has("notes-main")).toBe(true); + expect(names.has("notes")).toBe(false); + } finally { + await fs.rm(tmpRoot, { recursive: true, force: true }); + } + }); + it("resolves qmd update timeout overrides", () => { const cfg = { agents: { defaults: { workspace: "/tmp/memory-test" } }, diff --git a/packages/memory-host-sdk/src/host/backend-config.ts b/packages/memory-host-sdk/src/host/backend-config.ts index 8d69d7b2f8b..0d3c343278e 100644 --- a/packages/memory-host-sdk/src/host/backend-config.ts +++ b/packages/memory-host-sdk/src/host/backend-config.ts @@ -118,10 +118,20 @@ function scopeCollectionBase(base: string, agentId: string): string { function canonicalizePathForContainment(rawPath: string): string { const resolved = path.resolve(rawPath); - try { - return path.normalize(fs.realpathSync.native(resolved)); - } catch { - return path.normalize(resolved); + let current = resolved; + const suffix: string[] = []; + while (true) { + try { + const canonical = path.normalize(fs.realpathSync.native(current)); + return path.normalize(path.join(canonical, ...suffix)); + } catch { + const parent = path.dirname(current); + if (parent === current) { + return path.normalize(resolved); + } + suffix.unshift(path.basename(current)); + current = parent; + } } }