From da35718cb2bf1ba18bb5aa21d380b9635d4c4f74 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 29 Mar 2026 20:07:32 -0700 Subject: [PATCH] fix(memory): add qmd mcporter search tool override (#57363) * fix(memory): add qmd mcporter search tool override * fix(memory): tighten qmd search tool override guards * chore(config): drop generated docs baselines from qmd pr * fix(memory): keep explicit qmd query override on v2 args * docs(changelog): normalize qmd search tool attribution * fix(memory): reuse v1 qmd tool after query fallback --- CHANGELOG.md | 1 + .../src/memory/qmd-manager.test.ts | 183 ++++++++++++++++++ .../memory-core/src/memory/qmd-manager.ts | 180 ++++++++++++----- .../src/host/backend-config.test.ts | 16 ++ .../src/host/backend-config.ts | 7 + src/config/schema.base.generated.ts | 9 + src/config/schema.help.quality.test.ts | 1 + src/config/schema.help.ts | 2 + src/config/schema.labels.ts | 1 + src/config/types.memory.ts | 1 + src/config/zod-schema.ts | 1 + 11 files changed, 350 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 603fdf0e839..66af7ecefb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ Docs: https://docs.openclaw.ai - Gateway/health: carry webhook-vs-polling account mode from channel descriptors into runtime snapshots so passive channels like LINE and BlueBubbles skip false stale-socket health failures. (#47488) Thanks @karesansui-u. - Agents/MCP: reuse bundled MCP runtimes across turns in the same session, while recreating them when MCP config changes and disposing stale runtimes cleanly on session rollover. (#55090) Thanks @allan0509. - Memory/QMD: honor `memory.qmd.update.embedInterval` even when regular QMD update cadence is disabled or slower by arming a dedicated embed-cadence maintenance timer, while avoiding redundant timers when regular updates are already frequent enough. (#37326) Thanks @barronlroth. +- Memory/QMD: add `memory.qmd.searchTool` as an exact mcporter tool override, so custom QMD MCP tools such as `hybrid_search` can be used without weakening the validated `searchMode` config surface. (#27801) Thanks @keramblock. - Agents/memory flush: keep daily memory flush files append-only during embedded attempts so compaction writes do not overwrite earlier notes. (#53725) Thanks @HPluseven. - Web UI/markdown: stop bare auto-links from swallowing adjacent CJK text while preserving valid mixed-script path and query characters in rendered links. (#48410) Thanks @jnuyao. - BlueBubbles/iMessage: coalesce URL-only inbound messages with their link-preview balloon again so sharing a bare link no longer drops the URL from agent context. Thanks @vincentkoc. diff --git a/extensions/memory-core/src/memory/qmd-manager.test.ts b/extensions/memory-core/src/memory/qmd-manager.test.ts index 75595182a3b..1978f8e44be 100644 --- a/extensions/memory-core/src/memory/qmd-manager.test.ts +++ b/extensions/memory-core/src/memory/qmd-manager.test.ts @@ -1941,6 +1941,189 @@ describe("QmdMemoryManager", () => { await manager.close(); }); + it("uses an explicit mcporter search tool override with flat query args", async () => { + cfg = { + ...cfg, + memory: { + backend: "qmd", + qmd: { + includeDefaultMemory: false, + searchMode: "query", + searchTool: "hybrid_search", + update: { interval: "0s", debounceMs: 60_000, onBoot: false }, + paths: [{ path: workspaceDir, pattern: "**/*.md", name: "workspace" }], + mcporter: { enabled: true, serverName: "qmd", startDaemon: false }, + }, + }, + } as OpenClawConfig; + + spawnMock.mockImplementation((cmd: string, args: string[]) => { + const child = createMockChild({ autoClose: false }); + if (isMcporterCommand(cmd) && args[0] === "call") { + expect(args[1]).toBe("qmd.hybrid_search"); + const callArgs = JSON.parse(args[args.indexOf("--args") + 1]); + expect(callArgs).toMatchObject({ + query: "hello", + limit: 6, + minScore: 0, + collection: "workspace-main", + }); + expect(callArgs).not.toHaveProperty("searches"); + expect(callArgs).not.toHaveProperty("collections"); + emitAndClose(child, "stdout", JSON.stringify({ results: [] })); + return child; + } + emitAndClose(child, "stdout", "[]"); + return child; + }); + + const { manager } = await createManager(); + await manager.search("hello", { sessionKey: "agent:main:slack:dm:u123" }); + await manager.close(); + }); + + it('uses unified v2 args when the explicit mcporter search tool override is "query"', async () => { + cfg = { + ...cfg, + memory: { + backend: "qmd", + qmd: { + includeDefaultMemory: false, + searchMode: "search", + searchTool: "query", + update: { interval: "0s", debounceMs: 60_000, onBoot: false }, + paths: [{ path: workspaceDir, pattern: "**/*.md", name: "workspace" }], + mcporter: { enabled: true, serverName: "qmd", startDaemon: false }, + }, + }, + } as OpenClawConfig; + + spawnMock.mockImplementation((cmd: string, args: string[]) => { + const child = createMockChild({ autoClose: false }); + if (isMcporterCommand(cmd) && args[0] === "call") { + expect(args[1]).toBe("qmd.query"); + const callArgs = JSON.parse(args[args.indexOf("--args") + 1]); + expect(callArgs).toHaveProperty("searches", [{ type: "lex", query: "hello" }]); + expect(callArgs).toHaveProperty("collections", ["workspace-main"]); + expect(callArgs).not.toHaveProperty("query"); + expect(callArgs).not.toHaveProperty("minScore"); + expect(callArgs).not.toHaveProperty("collection"); + emitAndClose(child, "stdout", JSON.stringify({ results: [] })); + return child; + } + emitAndClose(child, "stdout", "[]"); + return child; + }); + + const { manager } = await createManager(); + await manager.search("hello", { sessionKey: "agent:main:slack:dm:u123" }); + await manager.close(); + }); + + it('reuses the cached v1 tool across collections when the explicit mcporter override is "query"', async () => { + cfg = { + ...cfg, + memory: { + backend: "qmd", + qmd: { + includeDefaultMemory: false, + searchMode: "search", + searchTool: "query", + update: { interval: "0s", debounceMs: 60_000, onBoot: false }, + paths: [ + { path: path.join(workspaceDir, "notes-a"), pattern: "**/*.md", name: "workspace-a" }, + { path: path.join(workspaceDir, "notes-b"), pattern: "**/*.md", name: "workspace-b" }, + ], + mcporter: { enabled: true, serverName: "qmd", startDaemon: false }, + }, + }, + } as OpenClawConfig; + + const selectors: string[] = []; + spawnMock.mockImplementation((cmd: string, args: string[]) => { + const child = createMockChild({ autoClose: false }); + if (isMcporterCommand(cmd) && args[0] === "call") { + const selector = args[1] ?? ""; + selectors.push(selector); + if (selector === "qmd.query") { + queueMicrotask(() => { + child.stderr.emit("data", "MCP error -32602: Tool query not found"); + child.closeWith(1); + }); + return child; + } + const callArgs = JSON.parse(args[args.indexOf("--args") + 1]); + expect(selector).toBe("qmd.search"); + expect(callArgs).toMatchObject({ + query: "hello", + limit: 6, + minScore: 0, + }); + emitAndClose(child, "stdout", JSON.stringify({ results: [] })); + return child; + } + emitAndClose(child, "stdout", "[]"); + return child; + }); + + const { manager } = await createManager(); + await manager.search("hello", { sessionKey: "agent:main:slack:dm:u123" }); + + expect(selectors).toEqual(["qmd.query", "qmd.search", "qmd.search"]); + + await manager.close(); + }); + + it("uses an explicit mcporter search tool override across multiple collections", async () => { + cfg = { + ...cfg, + memory: { + backend: "qmd", + qmd: { + includeDefaultMemory: false, + searchMode: "query", + searchTool: "hybrid_search", + update: { interval: "0s", debounceMs: 60_000, onBoot: false }, + paths: [ + { path: path.join(workspaceDir, "notes-a"), pattern: "**/*.md", name: "workspace-a" }, + { path: path.join(workspaceDir, "notes-b"), pattern: "**/*.md", name: "workspace-b" }, + ], + mcporter: { enabled: true, serverName: "qmd", startDaemon: false }, + }, + }, + } as OpenClawConfig; + + const selectors: string[] = []; + const collections: string[] = []; + spawnMock.mockImplementation((cmd: string, args: string[]) => { + const child = createMockChild({ autoClose: false }); + if (isMcporterCommand(cmd) && args[0] === "call") { + selectors.push(args[1] ?? ""); + const callArgs = JSON.parse(args[args.indexOf("--args") + 1]); + collections.push(String(callArgs.collection ?? "")); + expect(callArgs).toMatchObject({ + query: "hello", + limit: 6, + minScore: 0, + }); + expect(callArgs).not.toHaveProperty("searches"); + expect(callArgs).not.toHaveProperty("collections"); + emitAndClose(child, "stdout", JSON.stringify({ results: [] })); + return child; + } + emitAndClose(child, "stdout", "[]"); + return child; + }); + + const { manager } = await createManager(); + await manager.search("hello", { sessionKey: "agent:main:slack:dm:u123" }); + + expect(selectors).toEqual(["qmd.hybrid_search", "qmd.hybrid_search"]); + expect(collections).toEqual(["workspace-a-main", "workspace-b-main"]); + + await manager.close(); + }); + it("does not pin v1 fallback when only the serialized query text contains tool-not-found words", async () => { cfg = { ...cfg, diff --git a/extensions/memory-core/src/memory/qmd-manager.ts b/extensions/memory-core/src/memory/qmd-manager.ts index 4230a407498..b8161e1c9d0 100644 --- a/extensions/memory-core/src/memory/qmd-manager.ts +++ b/extensions/memory-core/src/memory/qmd-manager.ts @@ -158,6 +158,49 @@ type ManagedCollection = { type QmdManagerMode = "full" | "status"; type QmdCollectionPatternFlag = "--glob" | "--mask"; +type BuiltinQmdMcpTool = "query" | "search" | "vector_search" | "deep_search"; +type QmdMcporterSearchParams = + | { + mcporter: ResolvedQmdMcporterConfig; + tool: string; + searchCommand?: string; + explicitToolOverride: true; + query: string; + limit: number; + minScore: number; + collection?: string; + timeoutMs: number; + } + | { + mcporter: ResolvedQmdMcporterConfig; + tool: BuiltinQmdMcpTool; + searchCommand?: string; + explicitToolOverride: false; + query: string; + limit: number; + minScore: number; + collection?: string; + timeoutMs: number; + }; +type QmdMcporterAcrossCollectionsParams = + | { + tool: string; + searchCommand?: string; + explicitToolOverride: true; + query: string; + limit: number; + minScore: number; + collectionNames: string[]; + } + | { + tool: BuiltinQmdMcpTool; + searchCommand?: string; + explicitToolOverride: false; + query: string; + limit: number; + minScore: number; + collectionNames: string[]; + }; export class QmdMemoryManager implements MemorySearchManager { static async create(params: { @@ -816,18 +859,44 @@ export class QmdMemoryManager implements MemorySearchManager { return []; } const qmdSearchCommand = this.qmd.searchMode; + const explicitSearchTool = this.qmd.searchTool; const mcporterEnabled = this.qmd.mcporter.enabled; const runSearchAttempt = async ( allowMissingCollectionRepair: boolean, ): Promise => { try { if (mcporterEnabled) { - const tool = this.resolveQmdMcpTool(qmdSearchCommand); const minScore = opts?.minScore ?? 0; + if (explicitSearchTool) { + if (collectionNames.length > 1) { + return await this.runMcporterAcrossCollections({ + tool: explicitSearchTool, + searchCommand: qmdSearchCommand, + explicitToolOverride: true, + query: trimmed, + limit, + minScore, + collectionNames, + }); + } + return await this.runQmdSearchViaMcporter({ + mcporter: this.qmd.mcporter, + tool: explicitSearchTool, + searchCommand: qmdSearchCommand, + explicitToolOverride: true, + query: trimmed, + limit, + minScore, + collection: collectionNames[0], + timeoutMs: this.qmd.limits.timeoutMs, + }); + } + const tool = this.resolveQmdMcpTool(qmdSearchCommand); if (collectionNames.length > 1) { return await this.runMcporterAcrossCollections({ tool, searchCommand: qmdSearchCommand, + explicitToolOverride: false, query: trimmed, limit, minScore, @@ -838,6 +907,7 @@ export class QmdMemoryManager implements MemorySearchManager { mcporter: this.qmd.mcporter, tool, searchCommand: qmdSearchCommand, + explicitToolOverride: false, query: trimmed, limit, minScore, @@ -1378,9 +1448,7 @@ export class QmdMemoryManager implements MemorySearchManager { */ private qmdMcpToolVersion: "v2" | "v1" | null = null; - private resolveQmdMcpTool( - searchCommand: string, - ): "query" | "search" | "vector_search" | "deep_search" { + private resolveQmdMcpTool(searchCommand: string): BuiltinQmdMcpTool { if (this.qmdMcpToolVersion === "v2") { return "query"; } @@ -1498,16 +1566,9 @@ export class QmdMemoryManager implements MemorySearchManager { }); } - private async runQmdSearchViaMcporter(params: { - mcporter: ResolvedQmdMcporterConfig; - tool: "query" | "search" | "vector_search" | "deep_search"; - searchCommand?: string; - query: string; - limit: number; - minScore: number; - collection?: string; - timeoutMs: number; - }): Promise { + private async runQmdSearchViaMcporter( + params: QmdMcporterSearchParams, + ): Promise { await this.ensureMcporterDaemonStarted(params.mcporter); // If the version is already known as v1 but we received a stale "query" tool name @@ -1519,24 +1580,24 @@ export class QmdMemoryManager implements MemorySearchManager { : params.tool; const selector = `${params.mcporter.serverName}.${effectiveTool}`; - const callArgs: Record = - effectiveTool === "query" - ? { - // QMD 1.1+ "query" tool accepts typed sub-queries via `searches` array. - // Derive sub-query types from searchCommand to respect searchMode config. - // Note: minScore is intentionally omitted — QMD 1.1+'s query tool uses - // its own reranking pipeline and does not accept a minScore parameter. - searches: this.buildV2Searches(params.query, params.searchCommand), - limit: params.limit, - } - : { - // QMD 1.x tools accept a flat query string. - query: params.query, - limit: params.limit, - minScore: params.minScore, - }; + const useUnifiedQueryTool = effectiveTool === "query"; + const callArgs: Record = useUnifiedQueryTool + ? { + // QMD 1.1+ "query" tool accepts typed sub-queries via `searches` array. + // Derive sub-query types from searchCommand to respect searchMode config. + // Note: minScore is intentionally omitted — QMD 1.1+'s query tool uses + // its own reranking pipeline and does not accept a minScore parameter. + searches: this.buildV2Searches(params.query, params.searchCommand), + limit: params.limit, + } + : { + // QMD 1.x tools accept a flat query string. + query: params.query, + limit: params.limit, + minScore: params.minScore, + }; if (params.collection) { - if (effectiveTool === "query") { + if (useUnifiedQueryTool) { callArgs.collections = [params.collection]; } else { callArgs.collection = params.collection; @@ -1559,7 +1620,7 @@ export class QmdMemoryManager implements MemorySearchManager { { timeoutMs: Math.max(params.timeoutMs + 2_000, 5_000) }, ); // If we got here with the v2 "query" tool, confirm v2 for future calls. - if (effectiveTool === "query" && this.qmdMcpToolVersion === null) { + if (useUnifiedQueryTool && this.qmdMcpToolVersion === null) { this.markQmdV2(); } } catch (err) { @@ -1572,12 +1633,19 @@ export class QmdMemoryManager implements MemorySearchManager { // race condition where concurrent searches both probe with "query" while // the version is null — the second call would otherwise fail after the // first sets the version to "v1". - if (effectiveTool === "query" && this.isQueryToolNotFoundError(err)) { + if (useUnifiedQueryTool && this.isQueryToolNotFoundError(err)) { this.markQmdV1Fallback(); const v1Tool = this.resolveQmdMcpTool(params.searchCommand ?? "query"); return this.runQmdSearchViaMcporter({ - ...params, + mcporter: params.mcporter, tool: v1Tool, + searchCommand: params.searchCommand, + explicitToolOverride: false, + query: params.query, + limit: params.limit, + minScore: params.minScore, + collection: params.collection, + timeoutMs: params.timeoutMs, }); } throw err; @@ -2379,26 +2447,34 @@ export class QmdMemoryManager implements MemorySearchManager { return `file:${hints.preferredCollection}:${collectionRelativePath}`; } - private async runMcporterAcrossCollections(params: { - tool: "query" | "search" | "vector_search" | "deep_search"; - searchCommand?: string; - query: string; - limit: number; - minScore: number; - collectionNames: string[]; - }): Promise { + private async runMcporterAcrossCollections( + params: QmdMcporterAcrossCollectionsParams, + ): Promise { const bestByDocId = new Map(); for (const collectionName of params.collectionNames) { - const parsed = await this.runQmdSearchViaMcporter({ - mcporter: this.qmd.mcporter, - tool: params.tool, - searchCommand: params.searchCommand, - query: params.query, - limit: params.limit, - minScore: params.minScore, - collection: collectionName, - timeoutMs: this.qmd.limits.timeoutMs, - }); + const parsed = params.explicitToolOverride + ? await this.runQmdSearchViaMcporter({ + mcporter: this.qmd.mcporter, + tool: params.tool, + searchCommand: params.searchCommand, + explicitToolOverride: true, + query: params.query, + limit: params.limit, + minScore: params.minScore, + collection: collectionName, + timeoutMs: this.qmd.limits.timeoutMs, + }) + : await this.runQmdSearchViaMcporter({ + mcporter: this.qmd.mcporter, + tool: params.tool, + searchCommand: params.searchCommand, + explicitToolOverride: false, + query: params.query, + limit: params.limit, + minScore: params.minScore, + collection: collectionName, + timeoutMs: this.qmd.limits.timeoutMs, + }); for (const entry of parsed) { if (typeof entry.docid !== "string" || !entry.docid.trim()) { continue; 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 95e523072b4..5bacd2f14f2 100644 --- a/packages/memory-host-sdk/src/host/backend-config.test.ts +++ b/packages/memory-host-sdk/src/host/backend-config.test.ts @@ -143,6 +143,22 @@ describe("resolveMemoryBackendConfig", () => { const resolved = resolveMemoryBackendConfig({ cfg, agentId: "main" }); expect(resolved.qmd?.searchMode).toBe("vsearch"); }); + + it("resolves qmd mcporter search tool override", () => { + const cfg = { + agents: { defaults: { workspace: "/tmp/memory-test" } }, + memory: { + backend: "qmd", + qmd: { + searchMode: "query", + searchTool: " hybrid_search ", + }, + }, + } as OpenClawConfig; + const resolved = resolveMemoryBackendConfig({ cfg, agentId: "main" }); + expect(resolved.qmd?.searchMode).toBe("query"); + expect(resolved.qmd?.searchTool).toBe("hybrid_search"); + }); }); describe("memorySearch.extraPaths integration", () => { diff --git a/packages/memory-host-sdk/src/host/backend-config.ts b/packages/memory-host-sdk/src/host/backend-config.ts index 2907b272937..27cb8df9268 100644 --- a/packages/memory-host-sdk/src/host/backend-config.ts +++ b/packages/memory-host-sdk/src/host/backend-config.ts @@ -62,6 +62,7 @@ export type ResolvedQmdConfig = { command: string; mcporter: ResolvedQmdMcporterConfig; searchMode: MemoryQmdSearchMode; + searchTool?: string; collections: ResolvedQmdCollection[]; sessions: ResolvedQmdSessionConfig; update: ResolvedQmdUpdateConfig; @@ -202,6 +203,11 @@ function resolveSearchMode(raw?: MemoryQmdConfig["searchMode"]): MemoryQmdSearch return DEFAULT_QMD_SEARCH_MODE; } +function resolveSearchTool(raw?: MemoryQmdConfig["searchTool"]): string | undefined { + const value = raw?.trim(); + return value ? value : undefined; +} + function resolveSessionConfig( cfg: MemoryQmdConfig["sessions"], workspaceDir: string, @@ -346,6 +352,7 @@ export function resolveMemoryBackendConfig(params: { command, mcporter: resolveMcporterConfig(qmdCfg?.mcporter), searchMode: resolveSearchMode(qmdCfg?.searchMode), + searchTool: resolveSearchTool(qmdCfg?.searchTool), collections, includeDefaultMemory, sessions: resolveSessionConfig(qmdCfg?.sessions, workspaceDir), diff --git a/src/config/schema.base.generated.ts b/src/config/schema.base.generated.ts index 3b6005c768f..414752bc56f 100644 --- a/src/config/schema.base.generated.ts +++ b/src/config/schema.base.generated.ts @@ -10887,6 +10887,10 @@ export const GENERATED_BASE_CONFIG_SCHEMA = { }, ], }, + searchTool: { + type: "string", + minLength: 1, + }, includeDefaultMemory: { type: "boolean", }, @@ -13622,6 +13626,11 @@ export const GENERATED_BASE_CONFIG_SCHEMA = { help: 'Selects the QMD retrieval path: "query" uses standard query flow, "search" uses search-oriented retrieval, and "vsearch" emphasizes vector retrieval. Keep default unless tuning relevance quality.', tags: ["storage"], }, + "memory.qmd.searchTool": { + label: "QMD Search Tool Override", + help: "Overrides the exact mcporter tool name used for QMD searches while preserving `searchMode` as the semantic retrieval mode. Use this only when your QMD MCP server exposes a custom tool such as `hybrid_search` and keep it unset for the normal built-in tool mapping.", + tags: ["storage"], + }, "memory.qmd.includeDefaultMemory": { label: "QMD Include Default Memory", help: "Automatically indexes default memory files (MEMORY.md and memory/**/*.md) into QMD collections. Keep enabled unless you want indexing controlled only through explicit custom paths.", diff --git a/src/config/schema.help.quality.test.ts b/src/config/schema.help.quality.test.ts index 9208131d2ec..9d84165343b 100644 --- a/src/config/schema.help.quality.test.ts +++ b/src/config/schema.help.quality.test.ts @@ -42,6 +42,7 @@ const TARGET_KEYS = [ "memory.citations", "memory.backend", "memory.qmd.searchMode", + "memory.qmd.searchTool", "memory.qmd.scope", "memory.qmd.includeDefaultMemory", "memory.qmd.mcporter.enabled", diff --git a/src/config/schema.help.ts b/src/config/schema.help.ts index 4bfeac063b6..86798fa40e7 100644 --- a/src/config/schema.help.ts +++ b/src/config/schema.help.ts @@ -891,6 +891,8 @@ export const FIELD_HELP: Record = { "Automatically starts the mcporter daemon when mcporter-backed QMD mode is enabled (default: true). Keep enabled unless process lifecycle is managed externally by your service supervisor.", "memory.qmd.searchMode": 'Selects the QMD retrieval path: "query" uses standard query flow, "search" uses search-oriented retrieval, and "vsearch" emphasizes vector retrieval. Keep default unless tuning relevance quality.', + "memory.qmd.searchTool": + "Overrides the exact mcporter tool name used for QMD searches while preserving `searchMode` as the semantic retrieval mode. Use this only when your QMD MCP server exposes a custom tool such as `hybrid_search` and keep it unset for the normal built-in tool mapping.", "memory.qmd.includeDefaultMemory": "Automatically indexes default memory files (MEMORY.md and memory/**/*.md) into QMD collections. Keep enabled unless you want indexing controlled only through explicit custom paths.", "memory.qmd.paths": diff --git a/src/config/schema.labels.ts b/src/config/schema.labels.ts index 098723a9d81..4741c84ae5a 100644 --- a/src/config/schema.labels.ts +++ b/src/config/schema.labels.ts @@ -391,6 +391,7 @@ export const FIELD_LABELS: Record = { "memory.qmd.mcporter.serverName": "QMD MCPorter Server Name", "memory.qmd.mcporter.startDaemon": "QMD MCPorter Start Daemon", "memory.qmd.searchMode": "QMD Search Mode", + "memory.qmd.searchTool": "QMD Search Tool Override", "memory.qmd.includeDefaultMemory": "QMD Include Default Memory", "memory.qmd.paths": "QMD Extra Paths", "memory.qmd.paths.path": "QMD Path", diff --git a/src/config/types.memory.ts b/src/config/types.memory.ts index 54581f65fac..ce09d8e848e 100644 --- a/src/config/types.memory.ts +++ b/src/config/types.memory.ts @@ -14,6 +14,7 @@ export type MemoryQmdConfig = { command?: string; mcporter?: MemoryQmdMcporterConfig; searchMode?: MemoryQmdSearchMode; + searchTool?: string; includeDefaultMemory?: boolean; paths?: MemoryQmdIndexPath[]; sessions?: MemoryQmdSessionConfig; diff --git a/src/config/zod-schema.ts b/src/config/zod-schema.ts index bd2e8865245..73c4bfe8076 100644 --- a/src/config/zod-schema.ts +++ b/src/config/zod-schema.ts @@ -102,6 +102,7 @@ const MemoryQmdSchema = z command: z.string().optional(), mcporter: MemoryQmdMcporterSchema.optional(), searchMode: z.union([z.literal("query"), z.literal("search"), z.literal("vsearch")]).optional(), + searchTool: z.string().trim().min(1).optional(), includeDefaultMemory: z.boolean().optional(), paths: z.array(MemoryQmdPathSchema).optional(), sessions: MemoryQmdSessionSchema.optional(),