mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-04 14:50:24 +00:00
fix: wire memorySearch.extraPaths to QMD indexing (#57315)
* fix: wire memorySearch.extraPaths to QMD indexing
The 'agents.defaults.memorySearch.extraPaths' config field was documented
to add extra directories to the memory index, but the paths were never
actually passed to the QMD backend. Only 'memory.qmd.paths' worked.
This fix reads extraPaths from the memorySearch config and maps them
to QMD custom path collections, so users can simply configure:
memorySearch:
extraPaths:
- odd-vault
- /Users/odd/workspace
- /Users/odd/docs
And have those directories indexed alongside the default memory files.
Closes #57302
* fix: handle per-agent memorySearch.extraPaths overrides + add tests
- Read per-agent overrides from agents.list[].memorySearch.extraPaths
- Agent-specific overrides take priority over defaults
- Falls back to defaults when agent has no overrides
- Added 3 test cases for the feature
* fix: merge defaults + agent overrides instead of replacing
* fix: remove any types from tests, fix merge behavior assertion
* fix(memory): merge qmd extra path collections
* fix(memory): normalize qmd extra path resolution
* fix(memory): type qmd extra path merge
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
committed by
GitHub
parent
cbceb1db76
commit
219d4f03bd
@@ -144,3 +144,174 @@ describe("resolveMemoryBackendConfig", () => {
|
||||
expect(resolved.qmd?.searchMode).toBe("vsearch");
|
||||
});
|
||||
});
|
||||
|
||||
describe("memorySearch.extraPaths integration", () => {
|
||||
it("maps agents.defaults.memorySearch.extraPaths to QMD collections", () => {
|
||||
const cfg = {
|
||||
memory: { backend: "qmd" },
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: "/workspace/root",
|
||||
memorySearch: {
|
||||
extraPaths: ["/home/user/docs", "/home/user/vault"],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const result = resolveMemoryBackendConfig({ cfg, agentId: "test-agent" });
|
||||
expect(result.backend).toBe("qmd");
|
||||
const customCollections = (result.qmd?.collections ?? []).filter(
|
||||
(collection) => collection.kind === "custom",
|
||||
);
|
||||
expect(customCollections.length).toBeGreaterThanOrEqual(2);
|
||||
expect(customCollections.map((collection) => collection.path)).toEqual(
|
||||
expect.arrayContaining(["/home/user/docs", "/home/user/vault"]),
|
||||
);
|
||||
});
|
||||
|
||||
it("merges default and per-agent memorySearch.extraPaths for QMD collections", () => {
|
||||
const cfg = {
|
||||
memory: { backend: "qmd" },
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: "/workspace/root",
|
||||
memorySearch: {
|
||||
extraPaths: ["/default/path"],
|
||||
},
|
||||
},
|
||||
list: [
|
||||
{
|
||||
id: "my-agent",
|
||||
memorySearch: {
|
||||
extraPaths: ["/agent/specific/path"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const result = resolveMemoryBackendConfig({ cfg, agentId: "my-agent" });
|
||||
expect(result.backend).toBe("qmd");
|
||||
const customCollections = (result.qmd?.collections ?? []).filter(
|
||||
(collection) => collection.kind === "custom",
|
||||
);
|
||||
const paths = customCollections.map((collection) => collection.path);
|
||||
expect(paths).toContain("/agent/specific/path");
|
||||
expect(paths).toContain("/default/path");
|
||||
});
|
||||
|
||||
it("falls back to defaults when agent has no overrides", () => {
|
||||
const cfg = {
|
||||
memory: { backend: "qmd" },
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: "/workspace/root",
|
||||
memorySearch: {
|
||||
extraPaths: ["/default/path"],
|
||||
},
|
||||
},
|
||||
list: [
|
||||
{
|
||||
id: "other-agent",
|
||||
memorySearch: {
|
||||
extraPaths: ["/other/path"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const result = resolveMemoryBackendConfig({ cfg, agentId: "my-agent" });
|
||||
expect(result.backend).toBe("qmd");
|
||||
const customCollections = (result.qmd?.collections ?? []).filter(
|
||||
(collection) => collection.kind === "custom",
|
||||
);
|
||||
const paths = customCollections.map((collection) => collection.path);
|
||||
expect(paths).toContain("/default/path");
|
||||
});
|
||||
|
||||
it("deduplicates merged memorySearch.extraPaths for QMD collections", () => {
|
||||
const cfg = {
|
||||
memory: { backend: "qmd" },
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: "/workspace/root",
|
||||
memorySearch: {
|
||||
extraPaths: ["/shared/path", " /shared/path "],
|
||||
},
|
||||
},
|
||||
list: [
|
||||
{
|
||||
id: "my-agent",
|
||||
memorySearch: {
|
||||
extraPaths: ["/shared/path", "/agent-only"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const result = resolveMemoryBackendConfig({ cfg, agentId: "my-agent" });
|
||||
const customCollections = (result.qmd?.collections ?? []).filter(
|
||||
(collection) => collection.kind === "custom",
|
||||
);
|
||||
const paths = customCollections.map((collection) => collection.path);
|
||||
|
||||
expect(paths.filter((collectionPath) => collectionPath === "/shared/path")).toHaveLength(1);
|
||||
expect(paths).toContain("/agent-only");
|
||||
});
|
||||
|
||||
it("matches per-agent memorySearch.extraPaths using normalized agent ids", () => {
|
||||
const cfg = {
|
||||
memory: { backend: "qmd" },
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: "/workspace/root",
|
||||
},
|
||||
list: [
|
||||
{
|
||||
id: "My-Agent",
|
||||
memorySearch: {
|
||||
extraPaths: ["/agent/mixed-case"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const result = resolveMemoryBackendConfig({ cfg, agentId: "my-agent" });
|
||||
const customCollections = (result.qmd?.collections ?? []).filter(
|
||||
(collection) => collection.kind === "custom",
|
||||
);
|
||||
|
||||
expect(customCollections.map((collection) => collection.path)).toContain("/agent/mixed-case");
|
||||
});
|
||||
|
||||
it("deduplicates identical roots shared by memory.qmd.paths and memorySearch.extraPaths", () => {
|
||||
const cfg = {
|
||||
memory: {
|
||||
backend: "qmd",
|
||||
qmd: {
|
||||
paths: [{ path: "docs", pattern: "**/*.md", name: "workspace-docs" }],
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: "/workspace/root",
|
||||
memorySearch: {
|
||||
extraPaths: ["./docs"],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const result = resolveMemoryBackendConfig({ cfg, agentId: "main" });
|
||||
const customCollections = (result.qmd?.collections ?? []).filter(
|
||||
(collection) => collection.kind === "custom",
|
||||
);
|
||||
const docsCollections = customCollections.filter(
|
||||
(collection) =>
|
||||
collection.path === "/workspace/root/docs" && collection.pattern === "**/*.md",
|
||||
);
|
||||
|
||||
expect(docsCollections).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
MemoryQmdMcporterConfig,
|
||||
MemoryQmdSearchMode,
|
||||
} from "../../../../src/config/types.memory.js";
|
||||
import { normalizeAgentId } from "../../../../src/routing/session-key.js";
|
||||
import { resolveUserPath } from "../../../../src/utils.js";
|
||||
import { splitShellArgs } from "../../../../src/utils/shell-argv.js";
|
||||
|
||||
@@ -227,6 +228,7 @@ function resolveCustomPaths(
|
||||
return [];
|
||||
}
|
||||
const collections: ResolvedQmdCollection[] = [];
|
||||
const seenRoots = new Set<string>();
|
||||
rawPaths.forEach((entry, index) => {
|
||||
const trimmedPath = entry?.path?.trim();
|
||||
if (!trimmedPath) {
|
||||
@@ -239,6 +241,11 @@ function resolveCustomPaths(
|
||||
return;
|
||||
}
|
||||
const pattern = entry.pattern?.trim() || "**/*.md";
|
||||
const dedupeKey = `${resolved}\u0000${pattern}`;
|
||||
if (seenRoots.has(dedupeKey)) {
|
||||
return;
|
||||
}
|
||||
seenRoots.add(dedupeKey);
|
||||
const baseName = scopeCollectionBase(entry.name?.trim() || `custom-${index + 1}`, agentId);
|
||||
const name = ensureUniqueName(baseName, existing);
|
||||
collections.push({
|
||||
@@ -298,19 +305,38 @@ export function resolveMemoryBackendConfig(params: {
|
||||
cfg: OpenClawConfig;
|
||||
agentId: string;
|
||||
}): ResolvedMemoryBackendConfig {
|
||||
const normalizedAgentId = normalizeAgentId(params.agentId);
|
||||
const backend = params.cfg.memory?.backend ?? DEFAULT_BACKEND;
|
||||
const citations = params.cfg.memory?.citations ?? DEFAULT_CITATIONS;
|
||||
if (backend !== "qmd") {
|
||||
return { backend: "builtin", citations };
|
||||
}
|
||||
|
||||
const workspaceDir = resolveAgentWorkspaceDir(params.cfg, params.agentId);
|
||||
const workspaceDir = resolveAgentWorkspaceDir(params.cfg, normalizedAgentId);
|
||||
const qmdCfg = params.cfg.memory?.qmd;
|
||||
const includeDefaultMemory = qmdCfg?.includeDefaultMemory !== false;
|
||||
const nameSet = new Set<string>();
|
||||
const agentEntry = params.cfg.agents?.list?.find(
|
||||
(entry) => normalizeAgentId(entry?.id) === normalizedAgentId,
|
||||
);
|
||||
const mergedExtraPaths = [
|
||||
...(params.cfg.agents?.defaults?.memorySearch?.extraPaths ?? []),
|
||||
...(agentEntry?.memorySearch?.extraPaths ?? []),
|
||||
]
|
||||
.filter((value): value is string => typeof value === "string")
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean);
|
||||
const dedupedExtraPaths = Array.from(new Set(mergedExtraPaths));
|
||||
const searchExtraPaths = dedupedExtraPaths.map(
|
||||
(pathValue): { path: string; pattern?: string; name?: string } => ({ path: pathValue }),
|
||||
);
|
||||
|
||||
// Combine QMD-specific paths with memorySearch extraPaths
|
||||
const allQmdPaths: MemoryQmdIndexPath[] = [...(qmdCfg?.paths ?? []), ...searchExtraPaths];
|
||||
|
||||
const collections = [
|
||||
...resolveDefaultCollections(includeDefaultMemory, workspaceDir, nameSet, params.agentId),
|
||||
...resolveCustomPaths(qmdCfg?.paths, workspaceDir, nameSet, params.agentId),
|
||||
...resolveDefaultCollections(includeDefaultMemory, workspaceDir, nameSet, normalizedAgentId),
|
||||
...resolveCustomPaths(allQmdPaths, workspaceDir, nameSet, normalizedAgentId),
|
||||
];
|
||||
|
||||
const rawCommand = qmdCfg?.command?.trim() || "qmd";
|
||||
|
||||
Reference in New Issue
Block a user