From 63241bf1e00cd8b50e21df0911c1d9acb31adb96 Mon Sep 17 00:00:00 2001 From: Troy Hitch <77130+troyhitch@users.noreply.github.com> Date: Sat, 25 Apr 2026 14:38:30 -0400 Subject: [PATCH] fix(bonjour): suppress ciao cancellation across plugin runtime copies Fix the bundled Bonjour gateway discovery crash-loop caused by ciao probe cancellation rejections after the Bonjour plugin migration. The plugin entry now wires the existing rejection handler into the advertiser, and the unhandled-rejection handler registry is anchored on globalThis so staged plugin SDK module copies register into the same process-level handler set used by the host. Verification: - pnpm test:serial extensions/bonjour/src/advertiser.test.ts src/infra/unhandled-rejections.fatal-detection.test.ts - OPENCLAW_LOCAL_CHECK_MODE=throttled pnpm check:changed partially completed: conflict markers plus core/core-test/extensions/extension-test typecheck passed; local lint lane hit a self-lock and was stopped. --- CHANGELOG.md | 1 + extensions/bonjour/index.ts | 3 ++- src/infra/unhandled-rejections.ts | 15 ++++++++++++++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2edf880b6c9..bba04f97956 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Plugins/Bonjour: stop the gateway from crash-looping on `CIAO PROBING CANCELLED` when the mDNS watchdog cancels a stuck probe. Restores the rejection-handler wiring dropped during the bonjour plugin migration and shares unhandled-rejection state across module instances so plugin-staged copies of `openclaw/plugin-sdk/runtime` register into the same handler set the host consults. Especially affects Docker on macOS, where mDNS probing reliably hits the watchdog. Thanks @troyhitch. - Plugins/startup: remove ownerless bundled runtime-dependency install locks after a short grace window and include lock owner details when startup times out waiting for a plugin runtime-deps lock. Thanks @steipete. diff --git a/extensions/bonjour/index.ts b/extensions/bonjour/index.ts index a02c7e2e612..ba39e0f874e 100644 --- a/extensions/bonjour/index.ts +++ b/extensions/bonjour/index.ts @@ -1,4 +1,5 @@ import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; +import { registerUnhandledRejectionHandler } from "openclaw/plugin-sdk/runtime"; import { startGatewayBonjourAdvertiser } from "./src/advertiser.js"; function formatBonjourInstanceName(displayName: string) { @@ -32,7 +33,7 @@ export default definePluginEntry({ cliPath: ctx.cliPath, minimal: ctx.minimal, }, - { logger: api.logger }, + { logger: api.logger, registerUnhandledRejectionHandler }, ); return { stop: advertiser.stop }; }, diff --git a/src/infra/unhandled-rejections.ts b/src/infra/unhandled-rejections.ts index 48f6f3d10de..ac26f490e7e 100644 --- a/src/infra/unhandled-rejections.ts +++ b/src/infra/unhandled-rejections.ts @@ -11,7 +11,20 @@ import { runFatalErrorHooks } from "./fatal-error-hooks.js"; type UnhandledRejectionHandler = (reason: unknown) => boolean; -const handlers = new Set(); +// Plugins resolve `openclaw/plugin-sdk/runtime` through their own staged +// `node_modules`, which loads a separate copy of this module. To keep registry +// state shared across instances, anchor the handlers Set on globalThis. +const HANDLERS_GLOBAL_KEY = Symbol.for("openclaw.unhandledRejection.handlers"); +const handlers: Set = (() => { + const g = globalThis as unknown as Record>; + const existing = g[HANDLERS_GLOBAL_KEY]; + if (existing instanceof Set) { + return existing; + } + const created = new Set(); + g[HANDLERS_GLOBAL_KEY] = created; + return created; +})(); const FATAL_ERROR_CODES = new Set([ "ERR_OUT_OF_MEMORY",