diff --git a/CHANGELOG.md b/CHANGELOG.md index 9eeac161fdf..7106390f08e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ Docs: https://docs.openclaw.ai - Hooks/Inbound metadata: include `guildId` and `channelName` in `message_received` metadata for both plugin and internal hook paths. (#26115) Thanks @davidrudduck. - Discord/Component auth: evaluate guild component interactions with command-gating authorizers so unauthorized users no longer get `CommandAuthorized: true` on modal/button events. (#26119) Thanks @bmendonca3. - Discord/Typing indicator: prevent stuck typing indicators by sealing channel typing keepalive callbacks after idle/cleanup and ensuring Discord dispatch always marks typing idle even if preview-stream cleanup fails. (#26295) Thanks @ngutman. +- Tests/Low-memory stability: disable Vitest `vmForks` by default on low-memory local hosts (`<64 GiB`), keep low-profile extension lane parallelism at 4 workers, and align cron isolated-agent tests with `setSessionRuntimeModel` usage to avoid deterministic suite failures. (#26324) Thanks @ngutman. - Slack/Inbound media fallback: deliver file-only messages even when Slack media downloads fail by adding a filename placeholder fallback, capping fallback names to the shared media-file limit, and normalizing empty filenames to `file` so attachment-only messages are not silently dropped. (#25181) Thanks @justinhuangcode. ## 2026.2.24 diff --git a/scripts/test-parallel.mjs b/scripts/test-parallel.mjs index 0ec8d2fdc5f..35afef83c3f 100644 --- a/scripts/test-parallel.mjs +++ b/scripts/test-parallel.mjs @@ -88,14 +88,20 @@ const isCI = process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true"; const isMacOS = process.platform === "darwin" || process.env.RUNNER_OS === "macOS"; const isWindows = process.platform === "win32" || process.env.RUNNER_OS === "Windows"; const isWindowsCi = isCI && isWindows; +const hostCpuCount = os.cpus().length; +const hostMemoryGiB = Math.floor(os.totalmem() / 1024 ** 3); +// Keep aggressive local defaults for high-memory workstations (Mac Studio class). +const highMemLocalHost = !isCI && hostMemoryGiB >= 96; +const lowMemLocalHost = !isCI && hostMemoryGiB < 64; const nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "", 10); // vmForks is a big win for transform/import heavy suites, but Node 24 had -// regressions with Vitest's vm runtime in this repo. Keep it opt-out via +// regressions with Vitest's vm runtime in this repo, and low-memory local hosts +// are more likely to hit per-worker V8 heap ceilings. Keep it opt-out via // OPENCLAW_TEST_VM_FORKS=0, and let users force-enable with =1. const supportsVmForks = Number.isFinite(nodeMajor) ? nodeMajor !== 24 : true; const useVmForks = process.env.OPENCLAW_TEST_VM_FORKS === "1" || - (process.env.OPENCLAW_TEST_VM_FORKS !== "0" && !isWindows && supportsVmForks); + (process.env.OPENCLAW_TEST_VM_FORKS !== "0" && !isWindows && supportsVmForks && !lowMemLocalHost); const disableIsolation = process.env.OPENCLAW_TEST_NO_ISOLATE === "1"; const runs = [ ...(useVmForks @@ -176,11 +182,6 @@ const testProfile = const overrideWorkers = Number.parseInt(process.env.OPENCLAW_TEST_WORKERS ?? "", 10); const resolvedOverride = Number.isFinite(overrideWorkers) && overrideWorkers > 0 ? overrideWorkers : null; -const hostCpuCount = os.cpus().length; -const hostMemoryGiB = Math.floor(os.totalmem() / 1024 ** 3); -// Keep aggressive local defaults for high-memory workstations (Mac Studio class). -const highMemLocalHost = !isCI && hostMemoryGiB >= 96; -const lowMemLocalHost = !isCI && hostMemoryGiB < 64; const parallelGatewayEnabled = process.env.OPENCLAW_TEST_PARALLEL_GATEWAY === "1" || (!isCI && highMemLocalHost); // Keep gateway serial by default except when explicitly requested or on high-memory local hosts. @@ -206,7 +207,7 @@ const defaultWorkerBudget = ? { unit: 2, unitIsolated: 1, - extensions: 1, + extensions: 4, gateway: 1, } : testProfile === "serial" @@ -236,7 +237,7 @@ const defaultWorkerBudget = // Sub-64 GiB local hosts are prone to OOM with large vmFork runs. unit: 2, unitIsolated: 1, - extensions: 1, + extensions: 4, gateway: 1, } : { @@ -335,9 +336,15 @@ const runOnce = (entry, extraArgs = []) => new Promise((resolve) => { const maxWorkers = maxWorkersForRun(entry.name); const reporterArgs = buildReporterArgs(entry, extraArgs); + // vmForks with a single worker has shown cross-file leakage in extension suites. + // Fall back to process forks when we intentionally clamp that lane to one worker. + const entryArgs = + entry.name === "extensions" && maxWorkers === 1 && entry.args.includes("--pool=vmForks") + ? entry.args.map((arg) => (arg === "--pool=vmForks" ? "--pool=forks" : arg)) + : entry.args; const args = maxWorkers ? [ - ...entry.args, + ...entryArgs, "--maxWorkers", String(maxWorkers), ...silentArgs, @@ -345,7 +352,7 @@ const runOnce = (entry, extraArgs = []) => ...windowsCiArgs, ...extraArgs, ] - : [...entry.args, ...silentArgs, ...reporterArgs, ...windowsCiArgs, ...extraArgs]; + : [...entryArgs, ...silentArgs, ...reporterArgs, ...windowsCiArgs, ...extraArgs]; const nodeOptions = process.env.NODE_OPTIONS ?? ""; const nextNodeOptions = WARNING_SUPPRESSION_FLAGS.reduce( (acc, flag) => (acc.includes(flag) ? acc : `${acc} ${flag}`.trim()), diff --git a/src/cron/isolated-agent/run.skill-filter.test.ts b/src/cron/isolated-agent/run.skill-filter.test.ts index f1f5ac9d693..02d986819d9 100644 --- a/src/cron/isolated-agent/run.skill-filter.test.ts +++ b/src/cron/isolated-agent/run.skill-filter.test.ts @@ -112,6 +112,7 @@ vi.mock("../../cli/outbound-send-deps.js", () => ({ vi.mock("../../config/sessions.js", () => ({ resolveAgentMainSessionKey: vi.fn().mockReturnValue("main:default"), resolveSessionTranscriptPath: vi.fn().mockReturnValue("/tmp/transcript.jsonl"), + setSessionRuntimeModel: vi.fn(), updateSessionStore: vi.fn().mockResolvedValue(undefined), }));