diff --git a/CHANGELOG.md b/CHANGELOG.md index 15ffa4dc2ba..780b48a9be5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,31 @@ Docs: https://docs.openclaw.ai ## Unreleased +### Fixes + +- Gateway/Bonjour: keep @homebridge/ciao cancellation handlers registered across advertiser restarts so late probing cancellations cannot crash Linux and other mDNS-churned gateways. Thanks @codex. +- Plugins/startup: load the default `memory-core` slot during Gateway startup when permitted so active-memory recall can call `memory_search` and `memory_get` without requiring an explicit `plugins.slots.memory` entry, while preserving `plugins.slots.memory: "none"`. Thanks @codex. +- Plugins/CLI: prefer native require for compiled bundled plugin JavaScript before jiti so read-only config, status, device, and node commands avoid unnecessary transform overhead on slow hosts. Fixes #62842. Thanks @Effet. +- Plugins/compat: inventory doctor-side deprecation migrations separately from runtime plugin compatibility so release sweeps preserve needed repairs while enforcing dated removal windows. Thanks @vincentkoc. +- Plugins/compat: add missing dated compatibility records for legacy extension-api, memory registration, provider hook/type aliases, runtime aliases, channel SDK helpers, and approval/test utility shims. Thanks @vincentkoc. +- Plugins/CLI: refresh the persisted registry after managed plugin files are removed so ClawHub uninstall cannot leave stale `plugins list` entries. Thanks @codex. +- Plugins/CLI: make plugin install and uninstall config writes conflict-aware, clear stale denylist entries on explicit reinstall/removal, and delete managed plugin files only after config/index commit succeeds. Thanks @codex. +- Plugins: fail `plugins update` when tracked plugin or hook updates error, keep bundled runtime-dependency repair behind restrictive allowlists, and reject package installs with unloadable extension entries. Thanks @codex. +- Gateway/chat: keep duplicate attachment-backed `chat.send` retries with the same idempotency key on the documented in-flight path so aborts still target the real active run. Fixes #70139. Thanks @Feelw00. +- Plugins: share package entrypoint resolution between install and discovery, reject mismatched `runtimeExtensions`, and cache bundled runtime-dependency manifest reads during scans. Thanks @codex. + +## 2026.4.26 + +### Fixes + +- Plugins/CLI: let flag-driven `openclaw channels add` install the selected channel plugin from its default source without opening an interactive prompt, fixing published npm Telegram setup in stdin-closed automation. Thanks @codex. +- Onboarding/setup: keep first-run config reads, plugin compatibility notices, and post-model sanity checks on cold metadata paths unless the user chooses to browse all models, avoiding full plugin/runtime catalog work between prompts. Thanks @shakkernerd. +- Onboarding/auth: run manifest-owned provider auth choices through scoped setup providers so selecting OpenAI Codex browser/device auth no longer loads every provider runtime before OAuth starts. Thanks @shakkernerd. +- Onboarding/auth: keep the post-auth default-model policy lookup on manifest/setup metadata so the next prompt appears without loading broad provider runtime. Thanks @shakkernerd. +- Onboarding/models: keep skip-auth and provider-scoped model picker prompts off the full global model catalog path, and cache provider catalog hook resolution so setup no longer stalls after auth on large plugin registries. Thanks @shakkernerd. +- Gateway/Bonjour: suppress known @homebridge/ciao cancellation and network assertion failures through scoped process handlers so malformed mDNS packets or restricted VPS networking disable/restart Bonjour instead of crashing the gateway. Fixes #67578. Thanks @zenassist26-create. +- Discord: keep late clicks on already-resolved exec approval buttons quiet when elevated mode auto-resolved the request, while still surfacing real approval submission failures. Fixes #66906. Thanks @rlerikse. + ## 2026.4.25 ### Highlights diff --git a/extensions/bonjour/src/advertiser.test.ts b/extensions/bonjour/src/advertiser.test.ts index 9f13037c7ad..7b1de8be9ed 100644 --- a/extensions/bonjour/src/advertiser.test.ts +++ b/extensions/bonjour/src/advertiser.test.ts @@ -490,6 +490,8 @@ describe("gateway bonjour advertiser", () => { const stateRef = { value: "announcing" }; const events: string[] = []; + const cleanupException = vi.fn(); + const cleanupRejection = vi.fn(); let advertiseCount = 0; const destroy = vi.fn().mockImplementation(async () => { events.push("destroy"); @@ -505,6 +507,8 @@ describe("gateway bonjour advertiser", () => { return Promise.resolve(); }); mockCiaoService({ advertise, destroy, stateRef }); + registerUncaughtExceptionHandler.mockImplementation(() => cleanupException); + registerUnhandledRejectionHandler.mockImplementation(() => cleanupRejection); const started = await startAdvertiser({ gatewayPort: 18789, @@ -513,6 +517,8 @@ describe("gateway bonjour advertiser", () => { expect(createService).toHaveBeenCalledTimes(1); expect(advertise).toHaveBeenCalledTimes(1); + expect(registerUncaughtExceptionHandler).toHaveBeenCalledTimes(1); + expect(registerUnhandledRejectionHandler).toHaveBeenCalledTimes(1); await vi.advanceTimersByTimeAsync(15_000); @@ -521,11 +527,15 @@ describe("gateway bonjour advertiser", () => { expect(advertise).toHaveBeenCalledTimes(2); expect(destroy).toHaveBeenCalledTimes(1); expect(shutdown).not.toHaveBeenCalled(); + expect(cleanupException).not.toHaveBeenCalled(); + expect(cleanupRejection).not.toHaveBeenCalled(); expect(events).toEqual(["advertise:1", "destroy", "advertise:2"]); await started.stop(); expect(destroy).toHaveBeenCalledTimes(2); expect(shutdown).toHaveBeenCalledTimes(1); + expect(cleanupException).toHaveBeenCalledTimes(1); + expect(cleanupRejection).toHaveBeenCalledTimes(1); }); it("treats probing-to-announcing churn as one unhealthy window", async () => { diff --git a/extensions/bonjour/src/advertiser.ts b/extensions/bonjour/src/advertiser.ts index 752e3a21028..0db1fcbed7f 100644 --- a/extensions/bonjour/src/advertiser.ts +++ b/extensions/bonjour/src/advertiser.ts @@ -50,8 +50,6 @@ type CiaoModule = { type BonjourCycle = { responder: BonjourResponder; services: Array<{ label: string; svc: BonjourService }>; - cleanupUncaughtException?: () => void; - cleanupUnhandledRejection?: () => void; }; type ServiceStateTracker = { @@ -179,6 +177,18 @@ export async function startGatewayBonjourAdvertiser( const { getResponder, Protocol } = await loadCiaoModule(); const restoreConsoleLog = installCiaoConsoleNoiseFilter(); let requestCiaoRecovery: ((classification: CiaoProcessErrorClassification) => void) | undefined; + let cleanupUnhandledRejection: (() => void) | undefined; + let cleanupUncaughtException: (() => void) | undefined; + let processHandlersCleaned = false; + + function cleanupProcessHandlers() { + if (processHandlersCleaned) { + return; + } + processHandlersCleaned = true; + cleanupUncaughtException?.(); + cleanupUnhandledRejection?.(); + } const handleCiaoProcessError = (reason: unknown): boolean => { const classification = classifyCiaoProcessError(reason); @@ -196,6 +206,8 @@ export async function startGatewayBonjourAdvertiser( } return true; }; + cleanupUnhandledRejection = deps.registerUnhandledRejectionHandler?.(handleCiaoProcessError); + cleanupUncaughtException = deps.registerUncaughtExceptionHandler?.(handleCiaoProcessError); try { const hostnameRaw = process.env.OPENCLAW_MDNS_HOSTNAME?.trim() || "openclaw"; @@ -259,16 +271,7 @@ export async function startGatewayBonjourAdvertiser( svc: gateway as unknown as BonjourService, }); - const cleanupUnhandledRejection = - services.length > 0 && deps.registerUnhandledRejectionHandler - ? deps.registerUnhandledRejectionHandler(handleCiaoProcessError) - : undefined; - const cleanupUncaughtException = - services.length > 0 && deps.registerUncaughtExceptionHandler - ? deps.registerUncaughtExceptionHandler(handleCiaoProcessError) - : undefined; - - return { responder, services, cleanupUncaughtException, cleanupUnhandledRejection }; + return { responder, services }; } async function stopCycle(cycle: BonjourCycle | null, opts?: { shutdownResponder?: boolean }) { @@ -288,9 +291,6 @@ export async function startGatewayBonjourAdvertiser( } } catch { /* ignore */ - } finally { - cycle.cleanupUncaughtException?.(); - cycle.cleanupUnhandledRejection?.(); } } @@ -483,10 +483,12 @@ export async function startGatewayBonjourAdvertiser( } await stopCycle(cycle, { shutdownResponder: true }); restoreConsoleLog(); + cleanupProcessHandlers(); }, }; } catch (err) { restoreConsoleLog(); + cleanupProcessHandlers(); throw err; } } diff --git a/scripts/e2e/parallels-linux-smoke.sh b/scripts/e2e/parallels-linux-smoke.sh index 4b0712a24be..d56a3a96d27 100644 --- a/scripts/e2e/parallels-linux-smoke.sh +++ b/scripts/e2e/parallels-linux-smoke.sh @@ -42,6 +42,7 @@ TIMEOUT_ONBOARD_S=180 TIMEOUT_AGENT_S="${OPENCLAW_PARALLELS_LINUX_AGENT_TIMEOUT_S:-300}" TIMEOUT_GATEWAY_S=240 PHASE_STALE_WARN_S=60 +DISABLE_BONJOUR_FOR_GATEWAY=0 FRESH_MAIN_STATUS="skip" FRESH_MAIN_VERSION="skip" @@ -230,6 +231,11 @@ esac API_KEY_VALUE="${!API_KEY_ENV:-}" [[ -n "$API_KEY_VALUE" ]] || die "$API_KEY_ENV is required" +case "${OPENCLAW_PARALLELS_LINUX_DISABLE_BONJOUR:-}" in + 1|true|TRUE|yes|YES|on|ON) + DISABLE_BONJOUR_FOR_GATEWAY=1 + ;; +esac resolve_vm_name() { local json requested explicit @@ -460,7 +466,18 @@ restore_snapshot() { wait_for_guest_ready || die "guest did not become ready in $VM_NAME" } +sync_guest_clock() { + local host_now + host_now="@$(date -u '+%s')" + guest_exec date -u -s "$host_now" >/dev/null + guest_exec hwclock --systohc >/dev/null 2>&1 || true + guest_exec timedatectl set-ntp true >/dev/null 2>&1 || true + guest_exec systemctl restart systemd-timesyncd >/dev/null 2>&1 || true + guest_exec date -u +} + bootstrap_guest() { + sync_guest_clock guest_exec apt-get -o Acquire::Check-Date=false update guest_exec apt-get install -y curl ca-certificates } @@ -725,12 +742,16 @@ EOF } start_gateway_background() { - local cmd api_key_value_q + local cmd api_key_value_q bonjour_env api_key_value_q="$(shell_quote "$API_KEY_VALUE")" + bonjour_env="" + if [[ "$DISABLE_BONJOUR_FOR_GATEWAY" -eq 1 ]]; then + bonjour_env=" OPENCLAW_DISABLE_BONJOUR=1" + fi cmd="$(cat </dev/null 2>&1 || true rm -f /tmp/openclaw-parallels-linux-gateway.log -setsid sh -lc 'exec env OPENCLAW_HOME=/root OPENCLAW_STATE_DIR=/root/.openclaw OPENCLAW_CONFIG_PATH=/root/.openclaw/openclaw.json ${API_KEY_ENV}=${api_key_value_q} openclaw gateway run --bind loopback --port 18789 --force >/tmp/openclaw-parallels-linux-gateway.log 2>&1' >/dev/null 2>&1 < /dev/null & +setsid sh -lc 'exec env OPENCLAW_HOME=/root OPENCLAW_STATE_DIR=/root/.openclaw OPENCLAW_CONFIG_PATH=/root/.openclaw/openclaw.json${bonjour_env} ${API_KEY_ENV}=${api_key_value_q} openclaw gateway run --bind loopback --port 18789 --force >/tmp/openclaw-parallels-linux-gateway.log 2>&1' >/dev/null 2>&1 < /dev/null & EOF )" guest_exec bash -lc "$cmd" diff --git a/scripts/e2e/parallels-macos-smoke.sh b/scripts/e2e/parallels-macos-smoke.sh index 9659ed3eac9..9068463873a 100644 --- a/scripts/e2e/parallels-macos-smoke.sh +++ b/scripts/e2e/parallels-macos-smoke.sh @@ -1149,6 +1149,7 @@ run_dev_channel_update() { rm -rf $(shell_quote "$update_root") export PATH=$(shell_quote "$bootstrap_bin:$GUEST_EXEC_PATH") /usr/bin/env NODE_OPTIONS=--max-old-space-size=4096 \ + OPENCLAW_DISABLE_BUNDLED_PLUGINS=1 \ $GUEST_NODE_BIN $GUEST_OPENCLAW_ENTRY update --channel dev --yes --json EOF )" "$update_log" "$update_done" "$TIMEOUT_UPDATE_DEV_S" "$update_runner" diff --git a/scripts/e2e/parallels-npm-update-smoke.sh b/scripts/e2e/parallels-npm-update-smoke.sh index 7dfb84e3335..d37e8376f65 100755 --- a/scripts/e2e/parallels-npm-update-smoke.sh +++ b/scripts/e2e/parallels-npm-update-smoke.sh @@ -1944,7 +1944,7 @@ if platform_enabled windows; then fi if platform_enabled linux; then - bash "$ROOT_DIR/scripts/e2e/parallels-linux-smoke.sh" \ + OPENCLAW_PARALLELS_LINUX_DISABLE_BONJOUR=1 bash "$ROOT_DIR/scripts/e2e/parallels-linux-smoke.sh" \ --mode fresh \ --provider "$PROVIDER" \ --model "$MODEL_ID" \ diff --git a/scripts/e2e/parallels-windows-smoke.sh b/scripts/e2e/parallels-windows-smoke.sh index 63088e20709..ddd56da0377 100644 --- a/scripts/e2e/parallels-windows-smoke.sh +++ b/scripts/e2e/parallels-windows-smoke.sh @@ -614,6 +614,150 @@ EOF )" } +guest_run_agent_turn_process() { + local env_name_q env_value_q runner_basename runner_script_path runner_url runner_url_q + local runner_name stdout_name stderr_name done_name + local start_seconds poll_deadline startup_checked state_rc log_rc done_rc + local agent_combined done_status launcher_state + env_name_q="$(ps_single_quote "$API_KEY_ENV")" + env_value_q="$(ps_single_quote "$API_KEY_VALUE")" + runner_basename="openclaw-parallels-agent-runner-$RANDOM-$RANDOM.ps1" + runner_script_path="$MAIN_TGZ_DIR/$runner_basename" + runner_url="http://$HOST_IP:$HOST_PORT/$runner_basename" + runner_url_q="$(ps_single_quote "$runner_url")" + runner_name="openclaw-parallels-agent-$RANDOM-$RANDOM.ps1" + stdout_name="openclaw-parallels-agent-$RANDOM-$RANDOM.out.log" + stderr_name="openclaw-parallels-agent-$RANDOM-$RANDOM.err.log" + done_name="openclaw-parallels-agent-$RANDOM-$RANDOM.done" + start_seconds="$SECONDS" + poll_deadline=$((SECONDS + TIMEOUT_AGENT_S + 60)) + startup_checked=0 + + cat >"$runner_script_path" <<'EOF' +param( + [string]$StdoutPath, + [string]$StderrPath, + [string]$DonePath, + [string]$EnvName, + [string]$EnvValue +) +$ErrorActionPreference = 'Continue' +try { + if ($EnvName -ne '') { + Set-Item -Path ('Env:' + $EnvName) -Value $EnvValue + } + $node = Join-Path $env:ProgramFiles 'nodejs\node.exe' + if (-not (Test-Path $node)) { + $node = 'node' + } + $entry = Join-Path $env:APPDATA 'npm\node_modules\openclaw\openclaw.mjs' + & $node $entry agent --local --agent main --session-id 'parallels-windows-smoke' --message 'Reply with exact ASCII text OK only.' --json > $StdoutPath 2> $StderrPath + Set-Content -Path $DonePath -Value ([string]$LASTEXITCODE) + exit $LASTEXITCODE +} catch { + $_ | Out-String | Set-Content -Path $StderrPath + Set-Content -Path $DonePath -Value '1' + exit 1 +} +EOF + + guest_powershell_poll 20 "$(cat <