Skip to content

Commit 7b3ba58

Browse files
Jasmine ZhangJasmine Zhang
authored andcommitted
fix(memory): honor configured qmd search timeouts
1 parent 46a3442 commit 7b3ba58

2 files changed

Lines changed: 99 additions & 4 deletions

File tree

extensions/memory-core/src/tools.test.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,76 @@ describe("memory_search unavailable payloads", () => {
184184
}
185185
});
186186

187+
it("honors configured qmd timeout budgets for slow successful searches", async () => {
188+
vi.useFakeTimers();
189+
try {
190+
setMemoryBackend("qmd");
191+
setMemorySearchImpl(
192+
async () =>
193+
await new Promise((resolve) => {
194+
setTimeout(
195+
() =>
196+
resolve([
197+
{
198+
path: "MEMORY.md",
199+
startLine: 1,
200+
endLine: 1,
201+
score: 0.9,
202+
snippet: "Slow QMD hit.",
203+
source: "memory" as const,
204+
},
205+
]),
206+
20_000,
207+
);
208+
}),
209+
);
210+
const tool = createMemorySearchToolOrThrow({
211+
config: asOpenClawConfig({
212+
agents: { list: [{ id: "main", default: true }] },
213+
memory: { backend: "qmd", qmd: { limits: { timeoutMs: 30_000 } } },
214+
}),
215+
});
216+
217+
const resultPromise = tool.execute("slow-qmd-success", { query: "hello" });
218+
await vi.advanceTimersByTimeAsync(20_000);
219+
const result = await resultPromise;
220+
221+
expect((result.details as { results?: Array<{ path: string }> }).results?.[0]?.path).toBe(
222+
"MEMORY.md",
223+
);
224+
} finally {
225+
vi.useRealTimers();
226+
}
227+
});
228+
229+
it("times out qmd searches after the configured timeout plus bounded overhead", async () => {
230+
vi.useFakeTimers();
231+
try {
232+
setMemoryBackend("qmd");
233+
setMemorySearchImpl(async () => await new Promise(() => {}));
234+
const tool = createMemorySearchToolOrThrow({
235+
config: asOpenClawConfig({
236+
agents: { list: [{ id: "main", default: true }] },
237+
memory: { backend: "qmd", qmd: { limits: { timeoutMs: 30_000 } } },
238+
}),
239+
});
240+
241+
const resultPromise = tool.execute("slow-qmd-timeout", { query: "hello" });
242+
await vi.advanceTimersByTimeAsync(34_999);
243+
await Promise.resolve();
244+
await vi.advanceTimersByTimeAsync(1);
245+
const result = await resultPromise;
246+
247+
expectUnavailableMemorySearchDetails(result.details, {
248+
error: "memory_search timed out after 35s",
249+
warning: "Memory search is unavailable due to an embedding/provider error.",
250+
action: "Check embedding provider configuration and retry memory_search.",
251+
});
252+
} finally {
253+
vi.useRealTimers();
254+
}
255+
});
256+
187257
it("re-resolves the manager once when a cached sqlite handle was closed", async () => {
188258
let searchCalls = 0;
189259
setMemorySearchImpl(async () => {

extensions/memory-core/src/tools.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ type MemorySearchToolResult =
4545
| MemoryCorpusSearchResult;
4646

4747
const MEMORY_SEARCH_TOOL_TIMEOUT_MS = 15_000;
48+
const MEMORY_SEARCH_TOOL_QMD_TIMEOUT_OVERHEAD_MS = 5_000;
4849
const MEMORY_SEARCH_TOOL_COOLDOWN_MS = 60_000;
4950

5051
const memorySearchToolCooldowns = new Map<string, { until: number; error: string }>();
@@ -81,6 +82,24 @@ export const testing = {
8182
},
8283
} as const;
8384

85+
function resolveMemorySearchToolTimeoutMs(params: {
86+
backend: string | undefined;
87+
qmdTimeoutMs: number | undefined;
88+
}): number {
89+
if (
90+
params.backend !== "qmd" ||
91+
typeof params.qmdTimeoutMs !== "number" ||
92+
!Number.isFinite(params.qmdTimeoutMs) ||
93+
params.qmdTimeoutMs <= 0
94+
) {
95+
return MEMORY_SEARCH_TOOL_TIMEOUT_MS;
96+
}
97+
return Math.max(
98+
MEMORY_SEARCH_TOOL_TIMEOUT_MS,
99+
Math.floor(params.qmdTimeoutMs) + MEMORY_SEARCH_TOOL_QMD_TIMEOUT_OVERHEAD_MS,
100+
);
101+
}
102+
84103
async function runMemorySearchToolWithDeadline<T>(params: {
85104
timeoutMs: number;
86105
run: () => Promise<T>;
@@ -345,6 +364,8 @@ export function createMemorySearchTool(options: {
345364
execute:
346365
({ cfg, agentId }) =>
347366
async (_toolCallId, params) => {
367+
const { resolveMemoryBackendConfig } = await loadMemoryToolRuntime();
368+
const resolvedMemoryBackend = resolveMemoryBackendConfig({ cfg, agentId });
348369
const rawParams = asToolParamsRecord(params);
349370
const query = readStringParam(rawParams, "query", { required: true });
350371
const maxResults = readPositiveIntegerParam(rawParams, "maxResults");
@@ -381,9 +402,11 @@ export function createMemorySearchTool(options: {
381402
};
382403

383404
const outcome = await runMemorySearchToolWithDeadline({
384-
timeoutMs: MEMORY_SEARCH_TOOL_TIMEOUT_MS,
405+
timeoutMs: resolveMemorySearchToolTimeoutMs({
406+
backend: resolvedMemoryBackend.backend,
407+
qmdTimeoutMs: resolvedMemoryBackend.qmd?.limits?.timeoutMs,
408+
}),
385409
run: async () => {
386-
const { resolveMemoryBackendConfig } = await loadMemoryToolRuntime();
387410
const shouldQuerySupplements = requestedCorpus === "wiki" || requestedCorpus === "all";
388411
const shouldQueryMemory = requestedCorpus !== "wiki" && !cooldown;
389412
if (cooldown && !shouldQuerySupplements) {
@@ -502,10 +525,12 @@ export function createMemorySearchTool(options: {
502525
}
503526
const status = activeMemory.manager.status();
504527
const decorated = decorateCitations(rawResults, includeCitations);
505-
const resolved = resolveMemoryBackendConfig({ cfg, agentId });
506528
const memoryResults =
507529
status.backend === "qmd"
508-
? clampResultsByInjectedChars(decorated, resolved.qmd?.limits.maxInjectedChars)
530+
? clampResultsByInjectedChars(
531+
decorated,
532+
resolvedMemoryBackend.qmd?.limits?.maxInjectedChars,
533+
)
509534
: decorated;
510535
surfacedMemoryResults = memoryResults.map((result) => ({
511536
...result,

0 commit comments

Comments
 (0)