Skip to content

Commit b195825

Browse files
committed
fix(memory): cap embedding timeouts
1 parent 65fc5d1 commit b195825

2 files changed

Lines changed: 62 additions & 6 deletions

File tree

extensions/memory-core/src/memory/manager-embedding-ops.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
type MemoryChunk,
1616
type MemorySource,
1717
} from "openclaw/plugin-sdk/memory-core-host-engine-storage";
18+
import { MAX_TIMER_TIMEOUT_MS, resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
1819
import {
1920
MEMORY_BATCH_FAILURE_LIMIT,
2021
recordMemoryBatchFailure,
@@ -54,6 +55,17 @@ const EMBEDDING_BATCH_TIMEOUT_LOCAL_MS = 10 * 60_000;
5455

5556
const log = createSubsystemLogger("memory");
5657

58+
function resolveEmbeddingSecondsTimeoutMs(seconds: number): number {
59+
if (!Number.isFinite(seconds)) {
60+
return MAX_TIMER_TIMEOUT_MS;
61+
}
62+
const timeoutMs = Math.floor(seconds * 1000);
63+
return resolveTimerTimeoutMs(
64+
Number.isFinite(timeoutMs) ? timeoutMs : MAX_TIMER_TIMEOUT_MS,
65+
MAX_TIMER_TIMEOUT_MS,
66+
);
67+
}
68+
5769
type MemoryIndexEntry = {
5870
path: string;
5971
absPath: string;
@@ -77,7 +89,7 @@ export function resolveEmbeddingTimeoutMs(params: {
7789
if (params.kind === "query") {
7890
const runtimeTimeoutMs = params.providerRuntime?.inlineQueryTimeoutMs;
7991
if (typeof runtimeTimeoutMs === "number" && runtimeTimeoutMs > 0) {
80-
return runtimeTimeoutMs;
92+
return resolveTimerTimeoutMs(runtimeTimeoutMs, EMBEDDING_QUERY_TIMEOUT_REMOTE_MS);
8193
}
8294
return params.providerId === "local"
8395
? EMBEDDING_QUERY_TIMEOUT_LOCAL_MS
@@ -86,11 +98,11 @@ export function resolveEmbeddingTimeoutMs(params: {
8698

8799
const configuredTimeoutSeconds = params.configuredBatchTimeoutSeconds;
88100
if (typeof configuredTimeoutSeconds === "number" && configuredTimeoutSeconds > 0) {
89-
return configuredTimeoutSeconds * 1000;
101+
return resolveEmbeddingSecondsTimeoutMs(configuredTimeoutSeconds);
90102
}
91103
const runtimeTimeoutMs = params.providerRuntime?.inlineBatchTimeoutMs;
92104
if (typeof runtimeTimeoutMs === "number" && runtimeTimeoutMs > 0) {
93-
return runtimeTimeoutMs;
105+
return resolveTimerTimeoutMs(runtimeTimeoutMs, EMBEDDING_BATCH_TIMEOUT_REMOTE_MS);
94106
}
95107
return params.providerId === "local"
96108
? EMBEDDING_BATCH_TIMEOUT_LOCAL_MS
@@ -121,13 +133,14 @@ export async function runEmbeddingOperationWithTimeout<T>(params: {
121133
if (!Number.isFinite(params.timeoutMs) || params.timeoutMs <= 0) {
122134
return await params.run(controller.signal);
123135
}
136+
const timeoutMs = resolveTimerTimeoutMs(params.timeoutMs, 1);
124137
let timer: NodeJS.Timeout | null = null;
125138
const timeoutPromise = new Promise<never>((_, reject) => {
126139
timer = setTimeout(() => {
127140
const error = new Error(params.message);
128141
reject(error);
129142
controller.abort(error);
130-
}, params.timeoutMs);
143+
}, timeoutMs);
131144
timer.unref?.();
132145
});
133146
try {
@@ -453,9 +466,10 @@ export abstract class MemoryManagerEmbeddingOps extends MemoryManagerSyncOps {
453466
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
454467
return await promise;
455468
}
469+
const resolvedTimeoutMs = resolveTimerTimeoutMs(timeoutMs, 1);
456470
let timer: NodeJS.Timeout | null = null;
457471
const timeoutPromise = new Promise<never>((_, reject) => {
458-
timer = setTimeout(() => reject(new Error(message)), timeoutMs);
472+
timer = setTimeout(() => reject(new Error(message)), resolvedTimeoutMs);
459473
});
460474
try {
461475
return (await Promise.race([promise, timeoutPromise])) as T;

extensions/memory-core/src/memory/manager-embedding-timeout.test.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { describe, expect, it } from "vitest";
1+
import { MAX_TIMER_TIMEOUT_MS } from "openclaw/plugin-sdk/number-runtime";
2+
import { describe, expect, it, vi } from "vitest";
23
import {
34
resolveEmbeddingTimeoutMs,
45
resolveMemoryIndexConcurrency,
@@ -40,6 +41,30 @@ describe("memory embedding timeout resolution", () => {
4041
}),
4142
).toBe(45_000);
4243
});
44+
45+
it("caps configured and runtime embedding timeouts to timer-safe values", () => {
46+
expect(
47+
resolveEmbeddingTimeoutMs({
48+
kind: "batch",
49+
providerId: "openai",
50+
configuredBatchTimeoutSeconds: Number.MAX_SAFE_INTEGER,
51+
}),
52+
).toBe(MAX_TIMER_TIMEOUT_MS);
53+
expect(
54+
resolveEmbeddingTimeoutMs({
55+
kind: "query",
56+
providerId: "openai",
57+
providerRuntime: { inlineQueryTimeoutMs: Number.MAX_SAFE_INTEGER },
58+
}),
59+
).toBe(MAX_TIMER_TIMEOUT_MS);
60+
expect(
61+
resolveEmbeddingTimeoutMs({
62+
kind: "batch",
63+
providerId: "openai",
64+
providerRuntime: { inlineBatchTimeoutMs: Number.MAX_SAFE_INTEGER },
65+
}),
66+
).toBe(MAX_TIMER_TIMEOUT_MS);
67+
});
4368
});
4469

4570
describe("local embedding worker failure detection", () => {
@@ -104,6 +129,23 @@ describe("memory embedding timeout abort", () => {
104129
}),
105130
).rejects.toThrow("memory embeddings batch timed out after 0s");
106131
});
132+
133+
it("caps operation watchdog timers before scheduling", async () => {
134+
const timeoutSpy = vi
135+
.spyOn(globalThis, "setTimeout")
136+
.mockReturnValue(1 as unknown as ReturnType<typeof setTimeout>);
137+
138+
try {
139+
await runEmbeddingOperationWithTimeout({
140+
timeoutMs: Number.MAX_SAFE_INTEGER,
141+
message: "memory embeddings query timed out",
142+
run: async () => [1, 2, 3],
143+
});
144+
expect(timeoutSpy).toHaveBeenCalledWith(expect.any(Function), MAX_TIMER_TIMEOUT_MS);
145+
} finally {
146+
timeoutSpy.mockRestore();
147+
}
148+
});
107149
});
108150

109151
describe("memory index concurrency resolution", () => {

0 commit comments

Comments
 (0)