From d623354a0e26e0eedfcc54ac9c916929509481fa Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 25 Apr 2026 18:52:26 +0100 Subject: [PATCH] fix(infra): share diagnostic event state across loaders --- src/infra/diagnostic-events.test.ts | 19 ++++++++++- src/infra/diagnostic-events.ts | 51 ++++++++++++++++++++++++----- 2 files changed, 61 insertions(+), 9 deletions(-) diff --git a/src/infra/diagnostic-events.test.ts b/src/infra/diagnostic-events.test.ts index d0b163397d5..63a3eb4ea13 100644 --- a/src/infra/diagnostic-events.test.ts +++ b/src/infra/diagnostic-events.test.ts @@ -147,7 +147,24 @@ describe("diagnostic-events", () => { ]); }); - it("does not expose mutable diagnostic state on a global symbol", async () => { + it("shares diagnostic state across duplicate module instances", async () => { + const events: string[] = []; + onDiagnosticEvent((event) => { + events.push(event.type); + }); + + vi.resetModules(); + const specifier = "./diagnostic-events.js"; + const duplicateModule = (await import(specifier)) as typeof import("./diagnostic-events.js"); + duplicateModule.emitDiagnosticEvent({ + type: "message.queued", + source: "plugin", + }); + + expect(events).toEqual(["message.queued"]); + }); + + it("does not expose mutable diagnostic state on the obsolete global symbol", async () => { const globalStore = globalThis as Record; const events: boolean[] = []; globalStore[Symbol.for("openclaw.diagnosticEventsState")] = { diff --git a/src/infra/diagnostic-events.ts b/src/infra/diagnostic-events.ts index 8108f737eee..0f8e98407e7 100644 --- a/src/infra/diagnostic-events.ts +++ b/src/infra/diagnostic-events.ts @@ -404,14 +404,49 @@ const ASYNC_DIAGNOSTIC_EVENT_TYPES = new Set([ "log.record", ]); -const diagnosticEventsState: DiagnosticEventsGlobalState = { - enabled: true, - seq: 0, - listeners: new Set(), - dispatchDepth: 0, - asyncQueue: [], - asyncDrainScheduled: false, -}; +const DIAGNOSTIC_EVENTS_STATE_KEY = Symbol.for("openclaw.diagnosticEvents.state.v1"); + +function createDiagnosticEventsState(): DiagnosticEventsGlobalState { + return { + enabled: true, + seq: 0, + listeners: new Set(), + dispatchDepth: 0, + asyncQueue: [], + asyncDrainScheduled: false, + }; +} + +function isDiagnosticEventsState(value: unknown): value is DiagnosticEventsGlobalState { + if (!value || typeof value !== "object") { + return false; + } + const candidate = value as Partial; + return ( + typeof candidate.enabled === "boolean" && + typeof candidate.seq === "number" && + candidate.listeners instanceof Set && + typeof candidate.dispatchDepth === "number" && + Array.isArray(candidate.asyncQueue) && + typeof candidate.asyncDrainScheduled === "boolean" + ); +} + +const diagnosticEventsState: DiagnosticEventsGlobalState = (() => { + const globalStore = globalThis as Record; + const existing = globalStore[DIAGNOSTIC_EVENTS_STATE_KEY]; + if (isDiagnosticEventsState(existing)) { + return existing; + } + const created = createDiagnosticEventsState(); + Object.defineProperty(globalStore, DIAGNOSTIC_EVENTS_STATE_KEY, { + configurable: true, + enumerable: false, + value: created, + writable: false, + }); + return created; +})(); function getDiagnosticEventsState(): DiagnosticEventsGlobalState { return diagnosticEventsState;