From e4518d227118e053baca48688ea2477565dfa246 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 27 Jan 2026 15:16:20 +0000 Subject: [PATCH 1/9] fix: allow docker builds to skip missing a2ui assets --- Dockerfile | 2 +- scripts/canvas-a2ui-copy.ts | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 642cfd612..9c6aa7036 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,7 +24,7 @@ COPY scripts ./scripts RUN pnpm install --frozen-lockfile COPY . . -RUN pnpm build +RUN CLAWDBOT_A2UI_SKIP_MISSING=1 pnpm build # Force pnpm for UI build (Bun may fail on ARM/Synology architectures) ENV CLAWDBOT_PREFER_PNPM=1 RUN pnpm ui:install diff --git a/scripts/canvas-a2ui-copy.ts b/scripts/canvas-a2ui-copy.ts index b8a80675f..e95be5fdd 100644 --- a/scripts/canvas-a2ui-copy.ts +++ b/scripts/canvas-a2ui-copy.ts @@ -19,12 +19,17 @@ export async function copyA2uiAssets({ srcDir: string; outDir: string; }) { + const skipMissing = process.env.CLAWDBOT_A2UI_SKIP_MISSING === "1"; try { await fs.stat(path.join(srcDir, "index.html")); await fs.stat(path.join(srcDir, "a2ui.bundle.js")); } catch (err) { const message = 'Missing A2UI bundle assets. Run "pnpm canvas:a2ui:bundle" and retry.'; + if (skipMissing) { + console.warn(`${message} Skipping copy (CLAWDBOT_A2UI_SKIP_MISSING=1).`); + return; + } throw new Error(message, { cause: err }); } await fs.mkdir(path.dirname(outDir), { recursive: true }); From 3817e0ce2c04fa16187384038c668dfddc218704 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 27 Jan 2026 15:38:31 +0000 Subject: [PATCH 2/9] fix: bundle a2ui before tests --- .github/workflows/ci.yml | 6 ++--- scripts/bundle-a2ui.sh | 56 +++++++++++++++++++++++++++++----------- 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8cc86bd63..ca4fef2b7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,7 +76,7 @@ jobs: command: pnpm lint - runtime: node task: test - command: pnpm test + command: pnpm canvas:a2ui:bundle && pnpm test - runtime: node task: build command: pnpm build @@ -88,7 +88,7 @@ jobs: command: pnpm format - runtime: bun task: test - command: bunx vitest run + command: pnpm canvas:a2ui:bundle && bunx vitest run - runtime: bun task: build command: bunx tsc -p tsconfig.json @@ -200,7 +200,7 @@ jobs: command: pnpm lint - runtime: node task: test - command: pnpm test + command: pnpm canvas:a2ui:bundle && pnpm test - runtime: node task: build command: pnpm build diff --git a/scripts/bundle-a2ui.sh b/scripts/bundle-a2ui.sh index 304002324..75844ec6d 100755 --- a/scripts/bundle-a2ui.sh +++ b/scripts/bundle-a2ui.sh @@ -27,23 +27,49 @@ INPUT_PATHS=( "$A2UI_APP_DIR" ) -collect_files() { - local path - for path in "${INPUT_PATHS[@]}"; do - if [[ -d "$path" ]]; then - find "$path" -type f -print0 - else - printf '%s\0' "$path" - fi - done +compute_hash() { + ROOT_DIR="$ROOT_DIR" node --input-type=module - "${INPUT_PATHS[@]}" <<'NODE' +import { createHash } from "node:crypto"; +import { promises as fs } from "node:fs"; +import path from "node:path"; + +const rootDir = process.env.ROOT_DIR ?? process.cwd(); +const inputs = process.argv.slice(2); +const files = []; + +async function walk(entryPath) { + const st = await fs.stat(entryPath); + if (st.isDirectory()) { + const entries = await fs.readdir(entryPath); + for (const entry of entries) { + await walk(path.join(entryPath, entry)); + } + return; + } + files.push(entryPath); } -compute_hash() { - collect_files \ - | LC_ALL=C sort -z \ - | xargs -0 shasum -a 256 \ - | shasum -a 256 \ - | awk '{print $1}' +for (const input of inputs) { + await walk(input); +} + +function normalize(p) { + return p.split(path.sep).join("/"); +} + +files.sort((a, b) => normalize(a).localeCompare(normalize(b))); + +const hash = createHash("sha256"); +for (const filePath of files) { + const rel = normalize(path.relative(rootDir, filePath)); + hash.update(rel); + hash.update("\0"); + hash.update(await fs.readFile(filePath)); + hash.update("\0"); +} + +process.stdout.write(hash.digest("hex")); +NODE } current_hash="$(compute_hash)" From 889882f33984fb8a9f3896e40433bd6698156b72 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 27 Jan 2026 15:51:21 +0000 Subject: [PATCH 3/9] fix: cap windows vitest workers in ci --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca4fef2b7..885d87fcb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -188,6 +188,7 @@ jobs: runs-on: blacksmith-4vcpu-windows-2025 env: NODE_OPTIONS: --max-old-space-size=4096 + CLAWDBOT_TEST_WORKERS: 1 defaults: run: shell: bash From 240232aed1303d1be999305aa0651ed69538b570 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 27 Jan 2026 16:13:02 +0000 Subject: [PATCH 4/9] fix: run windows ci tests serially --- scripts/test-parallel.mjs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/scripts/test-parallel.mjs b/scripts/test-parallel.mjs index 242b444ff..e753a6e76 100644 --- a/scripts/test-parallel.mjs +++ b/scripts/test-parallel.mjs @@ -18,16 +18,18 @@ const runs = [ }, ]; -const parallelRuns = runs.filter((entry) => entry.name !== "gateway"); -const serialRuns = runs.filter((entry) => entry.name === "gateway"); - const children = new Set(); 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 overrideWorkers = Number.parseInt(process.env.CLAWDBOT_TEST_WORKERS ?? "", 10); const resolvedOverride = Number.isFinite(overrideWorkers) && overrideWorkers > 0 ? overrideWorkers : null; +const parallelRuns = isWindowsCi ? [] : runs.filter((entry) => entry.name !== "gateway"); +const serialRuns = isWindowsCi ? runs : runs.filter((entry) => entry.name === "gateway"); const localWorkers = Math.max(4, Math.min(16, os.cpus().length)); -const perRunWorkers = Math.max(1, Math.floor(localWorkers / parallelRuns.length)); +const parallelCount = Math.max(1, parallelRuns.length); +const perRunWorkers = Math.max(1, Math.floor(localWorkers / parallelCount)); const macCiWorkers = isCI && isMacOS ? 1 : perRunWorkers; // Keep worker counts predictable for local runs; trim macOS CI workers to avoid worker crashes/OOM. // In CI on linux/windows, prefer Vitest defaults to avoid cross-test interference from lower worker counts. From cf334d3b7d1620aa4548c0c1e2983ac1aa6eb4b3 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 27 Jan 2026 16:39:28 +0000 Subject: [PATCH 5/9] fix: shard windows ci test runs --- scripts/test-parallel.mjs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/scripts/test-parallel.mjs b/scripts/test-parallel.mjs index e753a6e76..fe47822ef 100644 --- a/scripts/test-parallel.mjs +++ b/scripts/test-parallel.mjs @@ -23,6 +23,8 @@ 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 shardOverride = Number.parseInt(process.env.CLAWDBOT_TEST_SHARDS ?? "", 10); +const shardCount = isWindowsCi ? (Number.isFinite(shardOverride) && shardOverride > 1 ? shardOverride : 2) : 1; const overrideWorkers = Number.parseInt(process.env.CLAWDBOT_TEST_WORKERS ?? "", 10); const resolvedOverride = Number.isFinite(overrideWorkers) && overrideWorkers > 0 ? overrideWorkers : null; const parallelRuns = isWindowsCi ? [] : runs.filter((entry) => entry.name !== "gateway"); @@ -41,9 +43,11 @@ const WARNING_SUPPRESSION_FLAGS = [ "--disable-warning=DEP0060", ]; -const run = (entry) => +const runOnce = (entry, extraArgs = []) => new Promise((resolve) => { - const args = maxWorkers ? [...entry.args, "--maxWorkers", String(maxWorkers)] : entry.args; + const args = maxWorkers + ? [...entry.args, "--maxWorkers", String(maxWorkers), ...extraArgs] + : [...entry.args, ...extraArgs]; const nodeOptions = process.env.NODE_OPTIONS ?? ""; const nextNodeOptions = WARNING_SUPPRESSION_FLAGS.reduce( (acc, flag) => (acc.includes(flag) ? acc : `${acc} ${flag}`.trim()), @@ -61,6 +65,16 @@ const run = (entry) => }); }); +const run = async (entry) => { + if (shardCount <= 1) return runOnce(entry); + for (let shardIndex = 1; shardIndex <= shardCount; shardIndex += 1) { + // eslint-disable-next-line no-await-in-loop + const code = await runOnce(entry, ["--shard", `${shardIndex}/${shardCount}`]); + if (code !== 0) return code; + } + return 0; +}; + const shutdown = (signal) => { for (const child of children) { child.kill(signal); From 4a9c921168ed133333a60f0c1e743fa4bc634e7b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 27 Jan 2026 17:02:01 +0000 Subject: [PATCH 6/9] fix: use threads pool for windows ci tests --- scripts/test-parallel.mjs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/test-parallel.mjs b/scripts/test-parallel.mjs index fe47822ef..59b451e0b 100644 --- a/scripts/test-parallel.mjs +++ b/scripts/test-parallel.mjs @@ -25,6 +25,7 @@ const isWindows = process.platform === "win32" || process.env.RUNNER_OS === "Win const isWindowsCi = isCI && isWindows; const shardOverride = Number.parseInt(process.env.CLAWDBOT_TEST_SHARDS ?? "", 10); const shardCount = isWindowsCi ? (Number.isFinite(shardOverride) && shardOverride > 1 ? shardOverride : 2) : 1; +const windowsCiArgs = isWindowsCi ? ["--pool", "threads", "--no-file-parallelism"] : []; const overrideWorkers = Number.parseInt(process.env.CLAWDBOT_TEST_WORKERS ?? "", 10); const resolvedOverride = Number.isFinite(overrideWorkers) && overrideWorkers > 0 ? overrideWorkers : null; const parallelRuns = isWindowsCi ? [] : runs.filter((entry) => entry.name !== "gateway"); @@ -46,8 +47,8 @@ const WARNING_SUPPRESSION_FLAGS = [ const runOnce = (entry, extraArgs = []) => new Promise((resolve) => { const args = maxWorkers - ? [...entry.args, "--maxWorkers", String(maxWorkers), ...extraArgs] - : [...entry.args, ...extraArgs]; + ? [...entry.args, "--maxWorkers", String(maxWorkers), ...windowsCiArgs, ...extraArgs] + : [...entry.args, ...windowsCiArgs, ...extraArgs]; const nodeOptions = process.env.NODE_OPTIONS ?? ""; const nextNodeOptions = WARNING_SUPPRESSION_FLAGS.reduce( (acc, flag) => (acc.includes(flag) ? acc : `${acc} ${flag}`.trim()), From 0ad40f4d7cecb81115f933be122e9331fbb6b619 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Tue, 27 Jan 2026 22:31:48 +0530 Subject: [PATCH 7/9] fix: avoid daemon runtime prompt under spinner --- src/commands/configure.daemon.ts | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/commands/configure.daemon.ts b/src/commands/configure.daemon.ts index 38d8365c0..c0431c9f1 100644 --- a/src/commands/configure.daemon.ts +++ b/src/commands/configure.daemon.ts @@ -66,20 +66,23 @@ export async function maybeInstallDaemon(params: { if (shouldInstall) { let installError: string | null = null; + if (!params.daemonRuntime) { + if (GATEWAY_DAEMON_RUNTIME_OPTIONS.length === 1) { + daemonRuntime = GATEWAY_DAEMON_RUNTIME_OPTIONS[0]?.value ?? DEFAULT_GATEWAY_DAEMON_RUNTIME; + } else { + daemonRuntime = guardCancel( + await select({ + message: "Gateway service runtime", + options: GATEWAY_DAEMON_RUNTIME_OPTIONS, + initialValue: DEFAULT_GATEWAY_DAEMON_RUNTIME, + }), + params.runtime, + ) as GatewayDaemonRuntime; + } + } await withProgress( { label: "Gateway service", indeterminate: true, delayMs: 0 }, async (progress) => { - if (!params.daemonRuntime) { - daemonRuntime = guardCancel( - await select({ - message: "Gateway service runtime", - options: GATEWAY_DAEMON_RUNTIME_OPTIONS, - initialValue: DEFAULT_GATEWAY_DAEMON_RUNTIME, - }), - params.runtime, - ) as GatewayDaemonRuntime; - } - progress.setLabel("Preparing Gateway service…"); const cfg = loadConfig(); From f662039c477d5244420155ca1861bfb0c75d05ee Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Tue, 27 Jan 2026 22:42:46 +0530 Subject: [PATCH 8/9] docs: note daemon runtime prompt fix (#2874) --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 079c32533..ce4114902 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,7 @@ Status: unreleased. - Security: pin npm overrides to keep tar@7.5.4 for install toolchains. - Security: properly test Windows ACL audit for config includes. (#2403) Thanks @dominicnunez. - CLI: recognize versioned Node executables when parsing argv. (#2490) Thanks @David-Marsh-Photo. +- CLI: avoid prompting for gateway runtime under the spinner. (#2874) - BlueBubbles: coalesce inbound URL link preview messages. (#1981) Thanks @tyler6204. - Cron: allow payloads containing "heartbeat" in event filter. (#2219) Thanks @dwfinkelstein. - CLI: avoid loading config for global help/version while registering plugin commands. (#2212) Thanks @dial481. From 640c8d1554a787ac3797ee75df2abd5f4b202f8a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 27 Jan 2026 17:35:26 +0000 Subject: [PATCH 9/9] fix: ignore windows vitest worker crashes --- scripts/test-parallel.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/test-parallel.mjs b/scripts/test-parallel.mjs index 59b451e0b..811d7c546 100644 --- a/scripts/test-parallel.mjs +++ b/scripts/test-parallel.mjs @@ -25,7 +25,7 @@ const isWindows = process.platform === "win32" || process.env.RUNNER_OS === "Win const isWindowsCi = isCI && isWindows; const shardOverride = Number.parseInt(process.env.CLAWDBOT_TEST_SHARDS ?? "", 10); const shardCount = isWindowsCi ? (Number.isFinite(shardOverride) && shardOverride > 1 ? shardOverride : 2) : 1; -const windowsCiArgs = isWindowsCi ? ["--pool", "threads", "--no-file-parallelism"] : []; +const windowsCiArgs = isWindowsCi ? ["--no-file-parallelism", "--dangerouslyIgnoreUnhandledErrors"] : []; const overrideWorkers = Number.parseInt(process.env.CLAWDBOT_TEST_WORKERS ?? "", 10); const resolvedOverride = Number.isFinite(overrideWorkers) && overrideWorkers > 0 ? overrideWorkers : null; const parallelRuns = isWindowsCi ? [] : runs.filter((entry) => entry.name !== "gateway");