mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-05 16:40:26 +00:00
fix: QMD 1.1+ mcporter compatibility with legacy fallback [AI-assisted] (#54728)
* fix: QMD 2.0 mcporter compatibility with v1 fallback [AI-assisted]
QMD 2.0 unified all search modes under a single 'query' MCP tool
with typed sub-queries, replacing search/vector_search/deep_search.
- Default to QMD 2.0 'query' tool with {searches: [...]} format
- Auto-detect version on first call and cache for the session
- Fall back to v1 tool names if 'query' is not found
- Backwards compatible: v1 users get one retry then cached
AI-assisted: Built with Claude (Opus 4.6) via OpenClaw. Fully tested
against QMD 2.0 with mcporter 0.7.3 daemon. v1 fallback path not
live-tested (no v1 instance available). Code reviewed and understood.
* test: add QMD v2 tool format and v1 fallback tests
- Verify mcporter bridge uses 'query' tool with {searches: [...]} format (v2)
- Verify fallback to 'deep_search' with {query, limit} format when v2 not found
- Verify v1 fallback logs a warning for visibility
* fix: address review feedback — multi-collection v1 fallback + test cleanup
- Fix multi-collection v1 fallback: resolve effectiveTool at the top
of runQmdSearchViaMcporter so stale 'query' tool names from the
loop are corrected once qmdMcpToolVersion is set to 'v1'
- Assert callCount in v1 fallback test (one v2 attempt + one v1 retry)
- Remove spurious global state reset (qmdMcpToolVersion is per-instance)
* docs: correct version references — breaking change was QMD 1.5, not 2.0
The MCP tool removal (search/vector_search/deep_search → query) happened
in QMD 1.5, not 2.0. QMD 2.0 was the SDK/library refactor.
Updated all comments, test names, and documentation to reflect this.
* fix: respect searchMode when building v2 mcporter queries
When searchMode is 'search' (BM25), only send lex sub-query.
When 'vsearch', only send vec. Default 'query' sends all three
(lex + vec + hyde) for full hybrid search with reranking.
Previously all three sub-queries were always sent regardless of
the configured searchMode, which could trigger unnecessary vector
embedding and HyDE LLM work on setups explicitly requesting
lexical-only search.
Addresses Codex P2 review feedback.
* docs: correct to QMD 1.1.0 — that's the actual version that removed the tools
Per CHANGELOG.md, MCP tools search/vector_search/deep_search were removed
in QMD 1.1.0 (2026-02-20), not 1.5 (which doesn't exist). Versions go
1.0.7 → 1.1.0 → 1.1.1 → 1.1.2 → 1.1.5 → 1.1.6 → 2.0.0.
* fix: remove redundant v1 guard (race condition) + tighten error matching
1. Remove qmdMcpToolVersion !== 'v1' guard from catch block. It's
redundant (effectiveTool === 'query' already prevents infinite retry)
and introduces a race condition: concurrent searches that both probe
with 'query' while version is null would fail after the first sets
version to 'v1'.
2. Tighten isToolNotFoundError regex to require 'Tool' near 'not found'
(within 40 chars, no sentence boundary). Prevents false-positives
when user query text in the mcporter args contains both words.
Addresses Greptile P1 (concurrent-search race) and Codex P2
(overly broad error matching).
* fix: address claude code review — type safety, minScore docs, explicit switch
- Restore type union for tool param instead of bare string
- Add comment explaining minScore omission for v2 (QMD 1.1+ uses
its own reranking pipeline, no minScore parameter)
- Make buildV2Searches switch exhaustive with explicit case 'query'
* fix: resolve CI failures — oxfmt formatting + TypeScript type errors
- Run oxfmt to fix formatting issues
- Add union return type to resolveQmdMcpTool() and
runMcporterAcrossCollections() tool param to satisfy tsc
(string was not assignable to the union type)
* fix(memory): align qmd query collection filters
* fix(memory): narrow qmd missing-tool fallback detection
* fix(memory): ignore qmd timeout text for v1 fallback
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
committed by
GitHub
parent
0fdf724125
commit
b888741462
@@ -1636,6 +1636,242 @@ describe("QmdMemoryManager", () => {
|
||||
await manager.close();
|
||||
});
|
||||
|
||||
it("uses QMD 1.1+ query tool with searches array via mcporter", async () => {
|
||||
cfg = {
|
||||
...cfg,
|
||||
memory: {
|
||||
backend: "qmd",
|
||||
qmd: {
|
||||
includeDefaultMemory: false,
|
||||
searchMode: "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") {
|
||||
// Verify it calls qmd.query (v2) not qmd.deep_search (v1)
|
||||
expect(args[1]).toBe("qmd.query");
|
||||
const callArgs = JSON.parse(args[args.indexOf("--args") + 1]);
|
||||
// Verify QMD 1.1+ searches array format
|
||||
expect(callArgs).toHaveProperty("searches");
|
||||
expect(Array.isArray(callArgs.searches)).toBe(true);
|
||||
expect(callArgs.searches).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ type: "lex" }),
|
||||
expect.objectContaining({ type: "vec" }),
|
||||
expect.objectContaining({ type: "hyde" }),
|
||||
]),
|
||||
);
|
||||
expect(callArgs).toHaveProperty("collections", ["workspace-main"]);
|
||||
// Should NOT have flat query/minScore (v1 format)
|
||||
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("falls back to QMD <1.1 tool names when query tool is not found", async () => {
|
||||
// qmdMcpToolVersion is an instance field — each createManager() starts fresh.
|
||||
|
||||
cfg = {
|
||||
...cfg,
|
||||
memory: {
|
||||
backend: "qmd",
|
||||
qmd: {
|
||||
includeDefaultMemory: false,
|
||||
searchMode: "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;
|
||||
|
||||
let callCount = 0;
|
||||
spawnMock.mockImplementation((cmd: string, args: string[]) => {
|
||||
const child = createMockChild({ autoClose: false });
|
||||
if (isMcporterCommand(cmd) && args[0] === "call") {
|
||||
callCount++;
|
||||
const toolSelector = args[1];
|
||||
if (toolSelector === "qmd.query") {
|
||||
// Simulate QMD <1.1 — "query" tool does not exist
|
||||
// The error message appears in stdout (mcporter wraps MCP errors in JSON output)
|
||||
queueMicrotask(() => {
|
||||
child.stderr.emit("data", "MCP error -32602: Tool query not found");
|
||||
child.closeWith(1);
|
||||
});
|
||||
return child;
|
||||
}
|
||||
if (toolSelector === "qmd.deep_search") {
|
||||
// v1 tool exists — verify v1 args format
|
||||
const callArgs = JSON.parse(args[args.indexOf("--args") + 1]);
|
||||
expect(callArgs).toHaveProperty("query");
|
||||
expect(callArgs).not.toHaveProperty("searches");
|
||||
// Return empty results (avoids needing a SQLite fixture)
|
||||
emitAndClose(child, "stdout", JSON.stringify({ results: [] }));
|
||||
return child;
|
||||
}
|
||||
emitAndClose(child, "stdout", JSON.stringify({ results: [] }));
|
||||
return child;
|
||||
}
|
||||
emitAndClose(child, "stdout", "[]");
|
||||
return child;
|
||||
});
|
||||
|
||||
const { manager } = await createManager();
|
||||
// The first search should try v2, fail, then retry with v1
|
||||
await manager.search("hello", { sessionKey: "agent:main:slack:dm:u123" });
|
||||
|
||||
// Should have logged the v1 fallback warning
|
||||
expect(logWarnMock).toHaveBeenCalledWith(
|
||||
expect.stringContaining("falling back to v1 tool names"),
|
||||
);
|
||||
|
||||
// One v2 attempt (fails) + one v1 retry (succeeds) per collection
|
||||
expect(callCount).toBe(2);
|
||||
|
||||
await manager.close();
|
||||
});
|
||||
|
||||
it("does not pin v1 fallback when only the serialized query text contains tool-not-found words", async () => {
|
||||
cfg = {
|
||||
...cfg,
|
||||
memory: {
|
||||
backend: "qmd",
|
||||
qmd: {
|
||||
includeDefaultMemory: false,
|
||||
searchMode: "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;
|
||||
|
||||
const selectors: string[] = [];
|
||||
let firstQueryCall = true;
|
||||
spawnMock.mockImplementation((cmd: string, args: string[]) => {
|
||||
const child = createMockChild({ autoClose: false });
|
||||
if (isMcporterCommand(cmd) && args[0] === "call") {
|
||||
selectors.push(args[1] ?? "");
|
||||
if (args[1] === "qmd.query" && firstQueryCall) {
|
||||
firstQueryCall = false;
|
||||
queueMicrotask(() => {
|
||||
child.stderr.emit("data", "backend unavailable");
|
||||
child.closeWith(1);
|
||||
});
|
||||
return child;
|
||||
}
|
||||
emitAndClose(child, "stdout", JSON.stringify({ results: [] }));
|
||||
return child;
|
||||
}
|
||||
emitAndClose(child, "stdout", "[]");
|
||||
return child;
|
||||
});
|
||||
|
||||
const { manager } = await createManager();
|
||||
|
||||
await expect(
|
||||
manager.search("abc: Tool query not found", {
|
||||
sessionKey: "agent:main:slack:dm:u123",
|
||||
}),
|
||||
).resolves.toEqual([]);
|
||||
|
||||
await manager.search("hello again", { sessionKey: "agent:main:slack:dm:u123" });
|
||||
|
||||
expect(selectors.length).toBeGreaterThanOrEqual(2);
|
||||
expect(selectors.every((selector) => selector === "qmd.query")).toBe(true);
|
||||
expect(logWarnMock).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining("falling back to v1 tool names"),
|
||||
);
|
||||
|
||||
await manager.close();
|
||||
});
|
||||
|
||||
it("does not pin v1 fallback when a timed out query contains tool-not-found words", async () => {
|
||||
cfg = {
|
||||
...cfg,
|
||||
memory: {
|
||||
backend: "qmd",
|
||||
qmd: {
|
||||
includeDefaultMemory: false,
|
||||
searchMode: "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;
|
||||
|
||||
const selectors: string[] = [];
|
||||
let firstQueryCall = true;
|
||||
spawnMock.mockImplementation((cmd: string, args: string[]) => {
|
||||
const child = createMockChild({ autoClose: false });
|
||||
if (isMcporterCommand(cmd) && args[0] === "call") {
|
||||
selectors.push(args[1] ?? "");
|
||||
if (args[1] === "qmd.query" && firstQueryCall) {
|
||||
firstQueryCall = false;
|
||||
return child;
|
||||
}
|
||||
emitAndClose(child, "stdout", JSON.stringify({ results: [] }));
|
||||
return child;
|
||||
}
|
||||
emitAndClose(child, "stdout", "[]");
|
||||
return child;
|
||||
});
|
||||
|
||||
const { manager } = await createManager();
|
||||
const managerWithPrivate = manager as object as {
|
||||
runMcporter: typeof manager["runMcporter"];
|
||||
};
|
||||
const originalRunMcporter = managerWithPrivate.runMcporter.bind(managerWithPrivate);
|
||||
let injectTimeoutOnce = true;
|
||||
const runMcporterSpy = vi
|
||||
.spyOn(managerWithPrivate, "runMcporter")
|
||||
.mockImplementation(async (...args) => {
|
||||
if (injectTimeoutOnce) {
|
||||
injectTimeoutOnce = false;
|
||||
firstQueryCall = false;
|
||||
throw new Error(
|
||||
'mcporter call qmd.query --args {"query":"abc: Tool query not found"} timed out after 5000ms',
|
||||
);
|
||||
}
|
||||
return await originalRunMcporter(...args);
|
||||
});
|
||||
|
||||
await expect(
|
||||
manager.search('abc: Tool query not found', {
|
||||
sessionKey: "agent:main:slack:dm:u123",
|
||||
}),
|
||||
).rejects.toThrow("timed out after 5000ms");
|
||||
|
||||
await manager.search("hello again", { sessionKey: "agent:main:slack:dm:u123" });
|
||||
|
||||
expect(runMcporterSpy).toHaveBeenCalled();
|
||||
expect(selectors.length).toBeGreaterThanOrEqual(1);
|
||||
expect(selectors.every((selector) => selector === "qmd.query")).toBe(true);
|
||||
expect(logWarnMock).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining("falling back to v1 tool names"),
|
||||
);
|
||||
|
||||
runMcporterSpy.mockRestore();
|
||||
await manager.close();
|
||||
});
|
||||
|
||||
it("resolves mcporter to a direct Windows entrypoint without enabling shell mode", async () => {
|
||||
const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
|
||||
const previousPath = process.env.PATH;
|
||||
|
||||
@@ -776,16 +776,12 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
): Promise<QmdQueryResult[]> => {
|
||||
try {
|
||||
if (mcporterEnabled) {
|
||||
const tool: "search" | "vector_search" | "deep_search" =
|
||||
qmdSearchCommand === "search"
|
||||
? "search"
|
||||
: qmdSearchCommand === "vsearch"
|
||||
? "vector_search"
|
||||
: "deep_search";
|
||||
const tool = this.resolveQmdMcpTool(qmdSearchCommand);
|
||||
const minScore = opts?.minScore ?? 0;
|
||||
if (collectionNames.length > 1) {
|
||||
return await this.runMcporterAcrossCollections({
|
||||
tool,
|
||||
searchCommand: qmdSearchCommand,
|
||||
query: trimmed,
|
||||
limit,
|
||||
minScore,
|
||||
@@ -795,6 +791,7 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
return await this.runQmdSearchViaMcporter({
|
||||
mcporter: this.qmd.mcporter,
|
||||
tool,
|
||||
searchCommand: qmdSearchCommand,
|
||||
query: trimmed,
|
||||
limit,
|
||||
minScore,
|
||||
@@ -1248,6 +1245,87 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* QMD 1.1+ unified all search modes under a single "query" MCP tool
|
||||
* that accepts a `searches` array with typed sub-queries (lex, vec, hyde).
|
||||
* QMD <1.1 exposed separate tools: search, vector_search, deep_search.
|
||||
*
|
||||
* This method probes the MCP server once to detect which interface is
|
||||
* available and caches the result for subsequent calls.
|
||||
*/
|
||||
private qmdMcpToolVersion: "v2" | "v1" | null = null;
|
||||
|
||||
private resolveQmdMcpTool(
|
||||
searchCommand: string,
|
||||
): "query" | "search" | "vector_search" | "deep_search" {
|
||||
if (this.qmdMcpToolVersion === "v2") {
|
||||
return "query";
|
||||
}
|
||||
if (this.qmdMcpToolVersion === "v1") {
|
||||
return searchCommand === "search"
|
||||
? "search"
|
||||
: searchCommand === "vsearch"
|
||||
? "vector_search"
|
||||
: "deep_search";
|
||||
}
|
||||
// Not yet probed — default to v2 (current QMD).
|
||||
// If the call fails with "not found", markQmdV1Fallback() will retry with v1 names.
|
||||
return "query";
|
||||
}
|
||||
|
||||
private markQmdV1Fallback(): void {
|
||||
if (this.qmdMcpToolVersion !== "v1") {
|
||||
this.qmdMcpToolVersion = "v1";
|
||||
log.warn(
|
||||
"QMD MCP server does not expose the v2 'query' tool; falling back to v1 tool names (search/vector_search/deep_search).",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private markQmdV2(): void {
|
||||
this.qmdMcpToolVersion = "v2";
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the `searches` array for QMD 1.1+ `query` tool, respecting
|
||||
* the configured searchMode so lexical-only or vector-only modes
|
||||
* don't trigger unnecessary LLM/embedding work.
|
||||
*/
|
||||
private buildV2Searches(
|
||||
query: string,
|
||||
searchCommand?: string,
|
||||
): Array<{ type: string; query: string }> {
|
||||
switch (searchCommand) {
|
||||
case "search":
|
||||
// BM25 keyword search only
|
||||
return [{ type: "lex", query }];
|
||||
case "vsearch":
|
||||
// Vector search only
|
||||
return [{ type: "vec", query }];
|
||||
case "query":
|
||||
case undefined:
|
||||
default:
|
||||
// Full hybrid: lex + vec + hyde (query expansion)
|
||||
return [
|
||||
{ type: "lex", query },
|
||||
{ type: "vec", query },
|
||||
{ type: "hyde", query },
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
private isQueryToolNotFoundError(err: unknown): boolean {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const detail = message.match(/ failed \(code \d+\): ([\s\S]*)$/)?.[1];
|
||||
if (!detail) {
|
||||
return false;
|
||||
}
|
||||
// Match only the specific v2-query missing-tool signatures emitted by MCP.
|
||||
// The full mcporter command summary includes the serialized user query, so
|
||||
// parse only the trailing stderr/stdout detail before deciding to pin v1.
|
||||
return /(?:^|\n|:\s)(?:MCP error [^:\n]+:\s*)?Tool ['"]?query['"]? not found\b/i.test(detail);
|
||||
}
|
||||
|
||||
private async ensureMcporterDaemonStarted(mcporter: ResolvedQmdMcporterConfig): Promise<void> {
|
||||
if (!mcporter.enabled) {
|
||||
return;
|
||||
@@ -1299,7 +1377,8 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
|
||||
private async runQmdSearchViaMcporter(params: {
|
||||
mcporter: ResolvedQmdMcporterConfig;
|
||||
tool: "search" | "vector_search" | "deep_search";
|
||||
tool: "query" | "search" | "vector_search" | "deep_search";
|
||||
searchCommand?: string;
|
||||
query: string;
|
||||
limit: number;
|
||||
minScore: number;
|
||||
@@ -1308,29 +1387,78 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
}): Promise<QmdQueryResult[]> {
|
||||
await this.ensureMcporterDaemonStarted(params.mcporter);
|
||||
|
||||
const selector = `${params.mcporter.serverName}.${params.tool}`;
|
||||
const callArgs: Record<string, unknown> = {
|
||||
query: params.query,
|
||||
limit: params.limit,
|
||||
minScore: params.minScore,
|
||||
};
|
||||
// If the version is already known as v1 but we received a stale "query" tool name
|
||||
// (e.g. from runMcporterAcrossCollections iterating after the first collection
|
||||
// triggered the fallback), resolve the correct v1 tool name immediately.
|
||||
const effectiveTool =
|
||||
params.tool === "query" && this.qmdMcpToolVersion === "v1"
|
||||
? this.resolveQmdMcpTool(params.searchCommand ?? "query")
|
||||
: 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,
|
||||
};
|
||||
if (params.collection) {
|
||||
callArgs.collection = params.collection;
|
||||
if (effectiveTool === "query") {
|
||||
callArgs.collections = [params.collection];
|
||||
} else {
|
||||
callArgs.collection = params.collection;
|
||||
}
|
||||
}
|
||||
|
||||
const result = await this.runMcporter(
|
||||
[
|
||||
"call",
|
||||
selector,
|
||||
"--args",
|
||||
JSON.stringify(callArgs),
|
||||
"--output",
|
||||
"json",
|
||||
"--timeout",
|
||||
String(Math.max(0, params.timeoutMs)),
|
||||
],
|
||||
{ timeoutMs: Math.max(params.timeoutMs + 2_000, 5_000) },
|
||||
);
|
||||
let result: { stdout: string };
|
||||
try {
|
||||
result = await this.runMcporter(
|
||||
[
|
||||
"call",
|
||||
selector,
|
||||
"--args",
|
||||
JSON.stringify(callArgs),
|
||||
"--output",
|
||||
"json",
|
||||
"--timeout",
|
||||
String(Math.max(0, params.timeoutMs)),
|
||||
],
|
||||
{ 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) {
|
||||
this.markQmdV2();
|
||||
}
|
||||
} catch (err) {
|
||||
// If the v2 "query" tool is not found, fall back to v1 tool names.
|
||||
// No need to guard on qmdMcpToolVersion !== "v1" here — if the version
|
||||
// were already "v1", effectiveTool would have been resolved to a v1 tool
|
||||
// name at the top of this function (not "query"). The effectiveTool ===
|
||||
// "query" check alone prevents infinite retry loops since the recursive
|
||||
// call passes a v1 tool name. Removing the version guard also fixes a
|
||||
// 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)) {
|
||||
this.markQmdV1Fallback();
|
||||
const v1Tool = this.resolveQmdMcpTool(params.searchCommand ?? "query");
|
||||
return this.runQmdSearchViaMcporter({
|
||||
...params,
|
||||
tool: v1Tool,
|
||||
});
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
const parsedUnknown: unknown = JSON.parse(result.stdout);
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
@@ -2129,7 +2257,8 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
}
|
||||
|
||||
private async runMcporterAcrossCollections(params: {
|
||||
tool: "search" | "vector_search" | "deep_search";
|
||||
tool: "query" | "search" | "vector_search" | "deep_search";
|
||||
searchCommand?: string;
|
||||
query: string;
|
||||
limit: number;
|
||||
minScore: number;
|
||||
@@ -2140,6 +2269,7 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
const parsed = await this.runQmdSearchViaMcporter({
|
||||
mcporter: this.qmd.mcporter,
|
||||
tool: params.tool,
|
||||
searchCommand: params.searchCommand,
|
||||
query: params.query,
|
||||
limit: params.limit,
|
||||
minScore: params.minScore,
|
||||
|
||||
Reference in New Issue
Block a user