diff --git a/extensions/bonjour/src/ciao.test.ts b/extensions/bonjour/src/ciao.test.ts index 6d40787331c..dacd7d7a1f0 100644 --- a/extensions/bonjour/src/ciao.test.ts +++ b/extensions/bonjour/src/ciao.test.ts @@ -48,6 +48,34 @@ describe("bonjour-ciao", () => { expect(ignoreCiaoUnhandledRejection(new Error("CIAO PROBING CANCELLED"))).toBe(true); }); + it("suppresses wrapped ciao cancellation rejections", () => { + expect( + classifyCiaoUnhandledRejection({ + reason: new Error("CIAO ANNOUNCEMENT CANCELLED"), + }), + ).toEqual({ + kind: "cancellation", + formatted: "CIAO ANNOUNCEMENT CANCELLED", + }); + }); + + it("suppresses aggregate ciao assertion rejections", () => { + expect( + classifyCiaoUnhandledRejection( + new AggregateError([ + Object.assign( + new Error("Reached illegal state! IPV4 address change from defined to undefined!"), + { name: "AssertionError" }, + ), + ]), + ), + ).toEqual({ + kind: "interface-assertion", + formatted: + "AssertionError: Reached illegal state! IPV4 address change from defined to undefined!", + }); + }); + it("suppresses lower-case string cancellation reasons too", () => { expect(ignoreCiaoUnhandledRejection("ciao announcement cancelled during cleanup")).toBe(true); }); diff --git a/extensions/bonjour/src/ciao.ts b/extensions/bonjour/src/ciao.ts index d8a9a4a5c0c..7f129c968b5 100644 --- a/extensions/bonjour/src/ciao.ts +++ b/extensions/bonjour/src/ciao.ts @@ -11,17 +11,59 @@ export type CiaoProcessErrorClassification = | { kind: "interface-assertion"; formatted: string } | { kind: "netmask-assertion"; formatted: string }; +function collectCiaoProcessErrorCandidates(reason: unknown): unknown[] { + const queue: unknown[] = [reason]; + const seen = new Set(); + const candidates: unknown[] = []; + + while (queue.length > 0) { + const current = queue.shift(); + if (current == null || seen.has(current)) { + continue; + } + seen.add(current); + candidates.push(current); + + if (!current || typeof current !== "object") { + continue; + } + const record = current as Record; + for (const nested of [ + record.cause, + record.reason, + record.original, + record.error, + record.data, + ]) { + if (nested != null && !seen.has(nested)) { + queue.push(nested); + } + } + if (Array.isArray(record.errors)) { + for (const nested of record.errors) { + if (nested != null && !seen.has(nested)) { + queue.push(nested); + } + } + } + } + + return candidates; +} + export function classifyCiaoProcessError(reason: unknown): CiaoProcessErrorClassification | null { - const formatted = formatBonjourError(reason); - const message = formatted.toUpperCase(); - if (CIAO_CANCELLATION_MESSAGE_RE.test(message)) { - return { kind: "cancellation", formatted }; - } - if (CIAO_INTERFACE_ASSERTION_MESSAGE_RE.test(message)) { - return { kind: "interface-assertion", formatted }; - } - if (CIAO_NETMASK_ASSERTION_MESSAGE_RE.test(message)) { - return { kind: "netmask-assertion", formatted }; + for (const candidate of collectCiaoProcessErrorCandidates(reason)) { + const formatted = formatBonjourError(candidate); + const message = formatted.toUpperCase(); + if (CIAO_CANCELLATION_MESSAGE_RE.test(message)) { + return { kind: "cancellation", formatted }; + } + if (CIAO_INTERFACE_ASSERTION_MESSAGE_RE.test(message)) { + return { kind: "interface-assertion", formatted }; + } + if (CIAO_NETMASK_ASSERTION_MESSAGE_RE.test(message)) { + return { kind: "netmask-assertion", formatted }; + } } return null; } diff --git a/src/infra/unhandled-rejections.fatal-detection.test.ts b/src/infra/unhandled-rejections.fatal-detection.test.ts index a868487bf5d..f010dfbbd52 100644 --- a/src/infra/unhandled-rejections.fatal-detection.test.ts +++ b/src/infra/unhandled-rejections.fatal-detection.test.ts @@ -196,32 +196,6 @@ describe("installUnhandledRejectionHandler - fatal detection", () => { ); }); - it("does not exit on known Bonjour advertiser failures", () => { - const bonjourCases: unknown[] = [ - new Error("CIAO ANNOUNCEMENT CANCELLED"), - new Error("CIAO PROBING CANCELLED"), - Object.assign( - new Error("Reached illegal state! IPV4 address change from defined to undefined!"), - { name: "AssertionError" }, - ), - Object.assign( - new Error( - "IP address version must match. Netmask cannot have a version different from the address!", - ), - { name: "AssertionError" }, - ), - ]; - - for (const bonjourErr of bonjourCases) { - expectExitCodeFromUnhandled(bonjourErr, []); - } - - expect(consoleWarnSpy).toHaveBeenCalledWith( - "[openclaw] Non-fatal unhandled rejection (continuing):", - expect.stringContaining("CIAO ANNOUNCEMENT CANCELLED"), - ); - }); - it("exits on generic errors without code", () => { const genericErr = new Error("Something went wrong"); diff --git a/src/infra/unhandled-rejections.ts b/src/infra/unhandled-rejections.ts index 6d9e569d190..219fda7a10f 100644 --- a/src/infra/unhandled-rejections.ts +++ b/src/infra/unhandled-rejections.ts @@ -116,12 +116,6 @@ const TRANSIENT_SQLITE_MESSAGE_SNIPPETS = [ "disk i/o error", ]; -const CIAO_CANCELLATION_MESSAGE_RE = /^CIAO (?:ANNOUNCEMENT|PROBING) CANCELLED\b/u; -const CIAO_INTERFACE_ASSERTION_MESSAGE_RE = - /REACHED ILLEGAL STATE!?\s+IPV4 ADDRESS CHANGE FROM (?:DEFINED TO UNDEFINED|UNDEFINED TO DEFINED)!?/u; -const CIAO_NETMASK_ASSERTION_MESSAGE_RE = - /IP ADDRESS VERSION MUST MATCH\.\s+NETMASK CANNOT HAVE A VERSION DIFFERENT FROM THE ADDRESS!?/u; - function hasSqliteSignal(err: unknown): boolean { if (!err || typeof err !== "object") { return false; @@ -341,46 +335,8 @@ export function isTransientSqliteError(err: unknown): boolean { return false; } -export function isNonFatalBonjourAdvertiserError(err: unknown): boolean { - if (!err) { - return false; - } - - for (const candidate of collectNestedUnhandledErrorCandidates(err)) { - const rawMessage = - candidate && typeof candidate === "object" - ? (candidate as { message?: unknown }).message - : undefined; - const message = - typeof candidate === "string" - ? candidate - : candidate && typeof candidate === "object" - ? typeof rawMessage === "string" - ? rawMessage - : "" - : ""; - const normalized = message.trim().toUpperCase(); - if (!normalized) { - continue; - } - if ( - CIAO_CANCELLATION_MESSAGE_RE.test(normalized) || - CIAO_INTERFACE_ASSERTION_MESSAGE_RE.test(normalized) || - CIAO_NETMASK_ASSERTION_MESSAGE_RE.test(normalized) - ) { - return true; - } - } - - return false; -} - export function isTransientUnhandledRejectionError(err: unknown): boolean { - return ( - isTransientNetworkError(err) || - isTransientSqliteError(err) || - isNonFatalBonjourAdvertiserError(err) - ); + return isTransientNetworkError(err) || isTransientSqliteError(err); } export function registerUnhandledRejectionHandler(handler: UnhandledRejectionHandler): () => void {