diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c60adbbfcd..de08cba1708 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai ### 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. 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..2d4866048ad 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 @@ -725,12 +731,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-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/src/plugins/installed-plugin-index.ts b/src/plugins/installed-plugin-index.ts index e8b4f4cc40f..5c98dd9d553 100644 --- a/src/plugins/installed-plugin-index.ts +++ b/src/plugins/installed-plugin-index.ts @@ -610,6 +610,9 @@ function buildInstalledPluginIndex( if (record.setupSource) { indexRecord.setupSource = record.setupSource; } + if (record.syntheticAuthRefs && record.syntheticAuthRefs.length > 0) { + indexRecord.syntheticAuthRefs = record.syntheticAuthRefs; + } if (candidate?.packageName) { indexRecord.packageName = candidate.packageName; }