diff --git a/extensions/matrix/src/matrix/actions/limits.ts b/extensions/matrix/src/matrix/actions/limits.ts index f18d9e2c059..7a6512eba39 100644 --- a/extensions/matrix/src/matrix/actions/limits.ts +++ b/extensions/matrix/src/matrix/actions/limits.ts @@ -1,6 +1,5 @@ +import { resolveIntegerOption } from "openclaw/plugin-sdk/number-runtime"; + export function resolveMatrixActionLimit(raw: unknown, fallback: number): number { - if (typeof raw !== "number" || !Number.isFinite(raw)) { - return fallback; - } - return Math.max(1, Math.floor(raw)); + return resolveIntegerOption(raw, fallback, { min: 1 }); } diff --git a/extensions/matrix/src/matrix/client/config.test.ts b/extensions/matrix/src/matrix/client/config.test.ts index b803bab0f94..37d21ee21f7 100644 --- a/extensions/matrix/src/matrix/client/config.test.ts +++ b/extensions/matrix/src/matrix/client/config.test.ts @@ -85,6 +85,24 @@ describe("Matrix auth/config live surfaces", () => { expect(resolved.encryption).toBe(false); }); + it("ignores non-finite initial sync limits", () => { + const cfg = { + channels: { + matrix: { + initialSyncLimit: Number.NaN, + accounts: { + ops: { + initialSyncLimit: Number.POSITIVE_INFINITY, + }, + }, + }, + }, + } as unknown as CoreConfig; + + const resolved = resolveMatrixConfigForAccount(cfg, "ops", {} as NodeJS.ProcessEnv); + expect(resolved.initialSyncLimit).toBeUndefined(); + }); + it("resolves accessToken SecretRef against the provided env", () => { const cfg = { channels: { diff --git a/extensions/matrix/src/matrix/client/config.ts b/extensions/matrix/src/matrix/client/config.ts index 9f6b1245868..22da548d8b4 100644 --- a/extensions/matrix/src/matrix/client/config.ts +++ b/extensions/matrix/src/matrix/client/config.ts @@ -1,4 +1,5 @@ import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; +import { resolveOptionalIntegerOption } from "openclaw/plugin-sdk/number-runtime"; import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime"; import { retryAsync } from "openclaw/plugin-sdk/retry-runtime"; import { @@ -412,7 +413,7 @@ function readMatrixAccountConfigField( } function clampMatrixInitialSyncLimit(value: unknown): number | undefined { - return typeof value === "number" ? Math.max(0, Math.floor(value)) : undefined; + return resolveOptionalIntegerOption(value, { min: 0 }); } function buildMatrixNetworkFields(params: { diff --git a/extensions/matrix/src/matrix/config-update.test.ts b/extensions/matrix/src/matrix/config-update.test.ts index aaa9eeb5ce8..735e552a0c2 100644 --- a/extensions/matrix/src/matrix/config-update.test.ts +++ b/extensions/matrix/src/matrix/config-update.test.ts @@ -54,6 +54,27 @@ describe("updateMatrixAccountConfig", () => { expect(account?.userId).toBeUndefined(); }); + it("does not store non-finite initial sync limits", () => { + const cfg = { + channels: { + matrix: { + accounts: { + default: { + initialSyncLimit: 20, + }, + }, + }, + }, + } as CoreConfig; + + const updated = updateMatrixAccountConfig(cfg, "default", { + initialSyncLimit: Number.NaN, + }); + + expect(updated.channels?.matrix?.initialSyncLimit).toBeUndefined(); + expect(updated.channels?.matrix?.accounts?.default?.initialSyncLimit).toBeUndefined(); + }); + it("preserves SecretRef auth inputs when updating config", () => { const updated = updateMatrixAccountConfig({} as CoreConfig, "default", { accessToken: { source: "env", provider: "default", id: "MATRIX_ACCESS_TOKEN" }, diff --git a/extensions/matrix/src/matrix/config-update.ts b/extensions/matrix/src/matrix/config-update.ts index 69eed293c04..f5b92af1652 100644 --- a/extensions/matrix/src/matrix/config-update.ts +++ b/extensions/matrix/src/matrix/config-update.ts @@ -1,4 +1,5 @@ import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id"; +import { resolveOptionalIntegerOption } from "openclaw/plugin-sdk/number-runtime"; import { coerceSecretRef } from "openclaw/plugin-sdk/secret-ref-runtime"; import { normalizeSecretInputString } from "openclaw/plugin-sdk/setup"; import type { CoreConfig, MatrixConfig } from "../types.js"; @@ -188,7 +189,12 @@ export function updateMatrixAccountConfig( if (patch.initialSyncLimit === null) { delete nextAccount.initialSyncLimit; } else { - nextAccount.initialSyncLimit = Math.max(0, Math.floor(patch.initialSyncLimit)); + const initialSyncLimit = resolveOptionalIntegerOption(patch.initialSyncLimit, { min: 0 }); + if (initialSyncLimit === undefined) { + delete nextAccount.initialSyncLimit; + } else { + nextAccount.initialSyncLimit = initialSyncLimit; + } } } diff --git a/extensions/matrix/src/matrix/monitor/index.ts b/extensions/matrix/src/matrix/monitor/index.ts index 7411319fffc..25a66ea8c3c 100644 --- a/extensions/matrix/src/matrix/monitor/index.ts +++ b/extensions/matrix/src/matrix/monitor/index.ts @@ -3,6 +3,7 @@ import { CHANNEL_APPROVAL_NATIVE_RUNTIME_CONTEXT_CAPABILITY } from "openclaw/plu import type { ChannelRuntimeSurface } from "openclaw/plugin-sdk/channel-contract"; import { waitUntilAbort } from "openclaw/plugin-sdk/channel-outbound"; import { registerChannelRuntimeContext } from "openclaw/plugin-sdk/channel-runtime-context"; +import { resolveOptionalIntegerOption } from "openclaw/plugin-sdk/number-runtime"; import { GROUP_POLICY_BLOCKED_LABEL, resolveThreadBindingIdleTimeoutMsForChannel, @@ -214,9 +215,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi const auth = await resolveMatrixAuth({ cfg, accountId: effectiveAccountId }); const resolvedInitialSyncLimit = - typeof opts.initialSyncLimit === "number" - ? Math.max(0, Math.floor(opts.initialSyncLimit)) - : auth.initialSyncLimit; + resolveOptionalIntegerOption(opts.initialSyncLimit, { min: 0 }) ?? auth.initialSyncLimit; const authWithLimit = resolvedInitialSyncLimit === auth.initialSyncLimit ? auth diff --git a/src/plugin-sdk/number-runtime.ts b/src/plugin-sdk/number-runtime.ts index 3fcf4a44e4c..bd5e5921f6c 100644 --- a/src/plugin-sdk/number-runtime.ts +++ b/src/plugin-sdk/number-runtime.ts @@ -4,6 +4,7 @@ export { parseFiniteNumber, resolveIntegerOption, resolveNonNegativeIntegerOption, + resolveOptionalIntegerOption, parseStrictInteger, parseStrictFiniteNumber, parseStrictNonNegativeInteger, diff --git a/src/shared/number-coercion.test.ts b/src/shared/number-coercion.test.ts index 26188c8929a..b72c81551ff 100644 --- a/src/shared/number-coercion.test.ts +++ b/src/shared/number-coercion.test.ts @@ -6,6 +6,7 @@ import { parseFiniteNumber, resolveIntegerOption, resolveNonNegativeIntegerOption, + resolveOptionalIntegerOption, parseStrictFiniteNumber, parseStrictInteger, parseStrictNonNegativeInteger, @@ -76,4 +77,12 @@ describe("number-coercion", () => { expect(resolveIntegerOption(40, 1, { max: 10 })).toBe(10); expect(resolveNonNegativeIntegerOption(Number.NaN, 3.9)).toBe(3); }); + + test("optional integer option helper rejects non-finite values", () => { + expect(resolveOptionalIntegerOption(7.9, { min: 1, max: 10 })).toBe(7); + expect(resolveOptionalIntegerOption(Number.NaN, { min: 1 })).toBeUndefined(); + expect(resolveOptionalIntegerOption(Number.POSITIVE_INFINITY, { min: 1 })).toBeUndefined(); + expect(resolveOptionalIntegerOption(-4, { min: 0 })).toBe(0); + expect(resolveOptionalIntegerOption(40, { max: 10 })).toBe(10); + }); }); diff --git a/src/shared/number-coercion.ts b/src/shared/number-coercion.ts index 0b77955b124..1840c6f0389 100644 --- a/src/shared/number-coercion.ts +++ b/src/shared/number-coercion.ts @@ -107,6 +107,19 @@ export function resolveIntegerOption( return range.max === undefined ? minBounded : Math.min(range.max, minBounded); } +export function resolveOptionalIntegerOption( + value: unknown, + range: { + min?: number; + max?: number; + } = {}, +): number | undefined { + if (typeof value !== "number" || !Number.isFinite(value)) { + return undefined; + } + return resolveIntegerOption(value, value, range); +} + export function resolveNonNegativeIntegerOption(value: unknown, fallback: number): number { return resolveIntegerOption(value, fallback, { min: 0 }); }