mirror of https://github.com/openclaw/openclaw.git
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
This commit is contained in:
parent
e7984272a7
commit
da35718cb2
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<QmdQueryResult[]> => {
|
||||
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<QmdQueryResult[]> {
|
||||
private async runQmdSearchViaMcporter(
|
||||
params: QmdMcporterSearchParams,
|
||||
): Promise<QmdQueryResult[]> {
|
||||
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<string, unknown> =
|
||||
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<string, unknown> = 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<QmdQueryResult[]> {
|
||||
private async runMcporterAcrossCollections(
|
||||
params: QmdMcporterAcrossCollectionsParams,
|
||||
): Promise<QmdQueryResult[]> {
|
||||
const bestByDocId = new Map<string, QmdQueryResult>();
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -891,6 +891,8 @@ export const FIELD_HELP: Record<string, string> = {
|
|||
"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":
|
||||
|
|
|
|||
|
|
@ -391,6 +391,7 @@ export const FIELD_LABELS: Record<string, string> = {
|
|||
"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",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export type MemoryQmdConfig = {
|
|||
command?: string;
|
||||
mcporter?: MemoryQmdMcporterConfig;
|
||||
searchMode?: MemoryQmdSearchMode;
|
||||
searchTool?: string;
|
||||
includeDefaultMemory?: boolean;
|
||||
paths?: MemoryQmdIndexPath[];
|
||||
sessions?: MemoryQmdSessionConfig;
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
Loading…
Reference in New Issue