@@ -11,6 +11,7 @@ const rootSdk = require(rootAliasPath) as Record<string, unknown>;
1111const rootAliasSource = fs . readFileSync ( rootAliasPath , "utf-8" ) ;
1212const compatPath = fileURLToPath ( new URL ( "../../plugin-sdk/compat.ts" , import . meta. url ) ) ;
1313const packageJsonPath = fileURLToPath ( new URL ( "../../../package.json" , import . meta. url ) ) ;
14+ const diagnosticEventsStateKey = Symbol . for ( "openclaw.diagnosticEvents.state.v1" ) ;
1415const legacyRootExportNames = [
1516 "registerContextEngine" ,
1617 "buildMemorySystemPromptAddition" ,
@@ -36,6 +37,10 @@ type EmptySchema = {
3637 } ;
3738} ;
3839
40+ type DiagnosticEventsStateFixture = {
41+ listeners : Set < ( event : { type : string } , metadata : { trusted : boolean } ) => void > ;
42+ } ;
43+
3944function requirePropertyDescriptor (
4045 target : Record < string , unknown > ,
4146 propertyName : string ,
@@ -183,6 +188,56 @@ function loadDiagnosticEventsAlias(distEntries: string[]) {
183188 } ) ;
184189}
185190
191+ function ensureDiagnosticEventsStateFixture (
192+ context : Record < PropertyKey , unknown > ,
193+ ) : DiagnosticEventsStateFixture {
194+ const existing = context [ diagnosticEventsStateKey ] as DiagnosticEventsStateFixture | undefined ;
195+ if ( existing ) {
196+ return existing ;
197+ }
198+ const state = vm . runInNewContext (
199+ `({
200+ marker: Symbol.for("openclaw.diagnosticEvents.state.v1"),
201+ enabled: true,
202+ seq: 0,
203+ listeners: new Set(),
204+ dispatchDepth: 0,
205+ asyncQueue: [],
206+ asyncDrainScheduled: false,
207+ asyncDroppedEvents: 0,
208+ asyncDroppedTrustedEvents: 0,
209+ asyncDroppedUntrustedEvents: 0,
210+ asyncDroppedPriorityEvents: 0,
211+ })` ,
212+ context ,
213+ ) as DiagnosticEventsStateFixture ;
214+ Object . defineProperty ( context , diagnosticEventsStateKey , {
215+ configurable : true ,
216+ enumerable : false ,
217+ value : state ,
218+ writable : false ,
219+ } ) ;
220+ return state ;
221+ }
222+
223+ function requireDiagnosticEventsStateFixture (
224+ lazyModule : ReturnType < typeof loadRootAliasWithStubs > ,
225+ ) : DiagnosticEventsStateFixture {
226+ const state = lazyModule . globalContext [ diagnosticEventsStateKey ] as
227+ | DiagnosticEventsStateFixture
228+ | undefined ;
229+ if ( ! state ) {
230+ throw new Error ( "expected diagnostic events state fixture" ) ;
231+ }
232+ return state ;
233+ }
234+
235+ function emitFixtureDiagnosticEvent ( state : DiagnosticEventsStateFixture ) : void {
236+ for ( const registered of state . listeners ) {
237+ registered ( { type : "model.usage" } , { trusted : false } ) ;
238+ }
239+ }
240+
186241function expectDiagnosticEventAccessor ( lazyModule : ReturnType < typeof loadRootAliasWithStubs > ) {
187242 expect (
188243 typeof ( lazyModule . moduleExports . onDiagnosticEvent as ( listener : ( ) => void ) => ( ) => void ) (
@@ -555,6 +610,106 @@ describe("plugin-sdk root alias", () => {
555610 expect ( unsubscribeCount ) . toBe ( 1 ) ;
556611 } ) ;
557612
613+ it ( "falls back and removes stale diagnostic listeners when the dist subscription is invalid" , ( ) => {
614+ const seen : string [ ] = [ ] ;
615+ let lazyModule ! : ReturnType < typeof loadRootAliasWithStubs > ;
616+ const preexistingListener = ( ) : void => undefined ;
617+ lazyModule = loadRootAliasWithStubs ( {
618+ aliasPath : createDistAliasPath ( ) ,
619+ distEntries : [ "diagnostic-events-W3Hz61fI.js" ] ,
620+ monolithicExports : {
621+ onDiagnosticEvent ( listener : ( event : { type : string } ) => void ) : undefined {
622+ const state = ensureDiagnosticEventsStateFixture ( lazyModule . globalContext ) ;
623+ state . listeners . add ( ( event , metadata ) => {
624+ if ( ! metadata . trusted ) {
625+ listener ( event ) ;
626+ }
627+ } ) ;
628+ return undefined ;
629+ } ,
630+ } ,
631+ } ) ;
632+ const state = ensureDiagnosticEventsStateFixture ( lazyModule . globalContext ) ;
633+ state . listeners . add ( preexistingListener ) ;
634+
635+ const unsubscribe = (
636+ lazyModule . moduleExports . onDiagnosticEvent as (
637+ listener : ( event : { type : string } ) => void ,
638+ ) => ( ) => void
639+ ) ( ( event ) => {
640+ seen . push ( event . type ) ;
641+ } ) ;
642+
643+ expect ( state . listeners . size ) . toBe ( 2 ) ;
644+ expect ( state . listeners . has ( preexistingListener ) ) . toBe ( true ) ;
645+ emitFixtureDiagnosticEvent ( state ) ;
646+ unsubscribe ( ) ;
647+
648+ expect ( seen ) . toEqual ( [ "model.usage" ] ) ;
649+ expect ( state . listeners . size ) . toBe ( 1 ) ;
650+ expect ( state . listeners . has ( preexistingListener ) ) . toBe ( true ) ;
651+ } ) ;
652+
653+ it ( "falls back to shared diagnostic state when the dist subscription throws" , ( ) => {
654+ const seen : string [ ] = [ ] ;
655+ let subscribeCount = 0 ;
656+ const lazyModule = loadRootAliasWithStubs ( {
657+ aliasPath : createDistAliasPath ( ) ,
658+ distEntries : [ "diagnostic-events-W3Hz61fI.js" ] ,
659+ monolithicExports : {
660+ onDiagnosticEvent ( ) : never {
661+ subscribeCount += 1 ;
662+ throw new Error ( "stale diagnostic subscription" ) ;
663+ } ,
664+ } ,
665+ } ) ;
666+
667+ const unsubscribe = (
668+ lazyModule . moduleExports . onDiagnosticEvent as (
669+ listener : ( event : { type : string } ) => void ,
670+ ) => ( ) => void
671+ ) ( ( event ) => {
672+ seen . push ( event . type ) ;
673+ } ) ;
674+ const state = requireDiagnosticEventsStateFixture ( lazyModule ) ;
675+
676+ expect ( subscribeCount ) . toBe ( 1 ) ;
677+ expect ( state . listeners . size ) . toBe ( 1 ) ;
678+ emitFixtureDiagnosticEvent ( state ) ;
679+ unsubscribe ( ) ;
680+
681+ expect ( seen ) . toEqual ( [ "model.usage" ] ) ;
682+ expect ( state . listeners . size ) . toBe ( 0 ) ;
683+ } ) ;
684+
685+ it ( "removes the shared-state fallback listener when diagnostic cleanup throws" , ( ) => {
686+ let diagnosticUnsubscribeCount = 0 ;
687+ const lazyModule = loadRootAliasWithStubs ( {
688+ aliasPath : createDistAliasPath ( ) ,
689+ distEntries : [ "diagnostic-events-W3Hz61fI.js" ] ,
690+ monolithicExports : {
691+ onDiagnosticEvent ( ) : ( ) => void {
692+ return ( ) => {
693+ diagnosticUnsubscribeCount += 1 ;
694+ throw new Error ( "diagnostic cleanup failed" ) ;
695+ } ;
696+ } ,
697+ } ,
698+ } ) ;
699+
700+ const unsubscribe = (
701+ lazyModule . moduleExports . onDiagnosticEvent as (
702+ listener : ( event : { type : string } ) => void ,
703+ ) => ( ) => void
704+ ) ( ( ) => undefined ) ;
705+ const state = requireDiagnosticEventsStateFixture ( lazyModule ) ;
706+
707+ expect ( state . listeners . size ) . toBe ( 1 ) ;
708+ expect ( ( ) => unsubscribe ( ) ) . toThrow ( "diagnostic cleanup failed" ) ;
709+ expect ( diagnosticUnsubscribeCount ) . toBe ( 1 ) ;
710+ expect ( state . listeners . size ) . toBe ( 0 ) ;
711+ } ) ;
712+
558713 it ( "bridges diagnostic listeners through shared process state when the lazy module is isolated" , ( ) => {
559714 const seen : string [ ] = [ ] ;
560715 const lazyModule = loadDiagnosticEventsAlias ( [ "diagnostic-events-W3Hz61fI.js" ] ) ;
0 commit comments