diff --git a/src/agents/memory-search.test.ts b/src/agents/memory-search.test.ts index beab0b18e08..066688bae49 100644 --- a/src/agents/memory-search.test.ts +++ b/src/agents/memory-search.test.ts @@ -4,6 +4,7 @@ import { clearMemoryEmbeddingProviders, registerMemoryEmbeddingProvider, } from "../plugins/memory-embedding-providers.js"; +import { MAX_TIMER_TIMEOUT_MS } from "../shared/number-coercion.js"; import { resolveMemorySearchConfig, resolveMemorySearchSyncConfig } from "./memory-search.js"; const asConfig = (cfg: OpenClawConfig): OpenClawConfig => cfg; @@ -529,6 +530,50 @@ describe("memory search config", () => { expectDefaultRemoteBatch(resolved); }); + it("normalizes remote batch timer config once before provider adapters receive it", () => { + const cfg = asConfig({ + agents: { + defaults: { + memorySearch: { + provider: "openai", + remote: { + batch: { + pollIntervalMs: Number.MAX_SAFE_INTEGER, + timeoutMinutes: Number.MAX_SAFE_INTEGER, + }, + }, + }, + }, + }, + }); + + const resolved = resolveMemorySearchConfig(cfg, "main"); + + expect(resolved?.remote?.batch?.pollIntervalMs).toBe(MAX_TIMER_TIMEOUT_MS); + expect(resolved?.remote?.batch?.timeoutMinutes).toBe(Math.floor(MAX_TIMER_TIMEOUT_MS / 60_000)); + }); + + it("keeps the default remote batch poll delay for zero intervals", () => { + const cfg = asConfig({ + agents: { + defaults: { + memorySearch: { + provider: "openai", + remote: { + batch: { + pollIntervalMs: 0, + }, + }, + }, + }, + }, + }); + + const resolved = resolveMemorySearchConfig(cfg, "main"); + + expect(resolved?.remote?.batch?.pollIntervalMs).toBe(2000); + }); + it("keeps remote unset for local provider without overrides", () => { const cfg = configWithDefaultProvider("local"); const resolved = resolveMemorySearchConfig(cfg, "main"); diff --git a/src/agents/memory-search.ts b/src/agents/memory-search.ts index 38ebbe2bdee..3530d19073c 100644 --- a/src/agents/memory-search.ts +++ b/src/agents/memory-search.ts @@ -4,6 +4,10 @@ import { findNormalizedProviderValue, normalizeProviderId, } from "@openclaw/model-catalog-core/provider-id"; +import { + MAX_TIMER_TIMEOUT_MS, + resolvePositiveTimerTimeoutMs, +} from "@openclaw/normalization-core/number-coercion"; import { normalizeStringEntries, uniqueStrings, @@ -126,6 +130,29 @@ const DEFAULT_TEMPORAL_DECAY_HALF_LIFE_DAYS = 30; const DEFAULT_CACHE_ENABLED = true; const DEFAULT_SOURCES: Array<"memory" | "sessions"> = ["memory"]; const DEFAULT_MEMORY_EMBEDDING_PROVIDER = "openai"; +const DEFAULT_REMOTE_BATCH_POLL_INTERVAL_MS = 2_000; +const DEFAULT_REMOTE_BATCH_TIMEOUT_MINUTES = 60; +const MAX_REMOTE_BATCH_TIMEOUT_MINUTES = Math.floor(MAX_TIMER_TIMEOUT_MS / 60_000); + +function resolveRemoteBatchPollIntervalMs( + overrideValue: number | undefined, + defaultValue: number | undefined, +): number { + return resolvePositiveTimerTimeoutMs( + overrideValue ?? defaultValue, + DEFAULT_REMOTE_BATCH_POLL_INTERVAL_MS, + ); +} + +function resolveRemoteBatchTimeoutMinutes( + overrideValue: number | undefined, + defaultValue: number | undefined, +): number { + const value = overrideValue ?? defaultValue; + return typeof value === "number" && Number.isFinite(value) && value > 0 + ? clampInt(value, 1, MAX_REMOTE_BATCH_TIMEOUT_MINUTES) + : DEFAULT_REMOTE_BATCH_TIMEOUT_MINUTES; +} function normalizeSources( sources: Array<"memory" | "sessions"> | undefined, @@ -221,10 +248,14 @@ function mergeConfig( 1, overrideRemote?.batch?.concurrency ?? defaultRemote?.batch?.concurrency ?? 2, ), - pollIntervalMs: - overrideRemote?.batch?.pollIntervalMs ?? defaultRemote?.batch?.pollIntervalMs ?? 2000, - timeoutMinutes: - overrideRemote?.batch?.timeoutMinutes ?? defaultRemote?.batch?.timeoutMinutes ?? 60, + pollIntervalMs: resolveRemoteBatchPollIntervalMs( + overrideRemote?.batch?.pollIntervalMs, + defaultRemote?.batch?.pollIntervalMs, + ), + timeoutMinutes: resolveRemoteBatchTimeoutMinutes( + overrideRemote?.batch?.timeoutMinutes, + defaultRemote?.batch?.timeoutMinutes, + ), }; const remote = includeRemote ? {