@@ -244,12 +244,32 @@ export const layer = Layer.effect(
244244 } ) . pipe ( Effect . ignore )
245245 }
246246
247- // Subscribe to bus events, fiber interrupted when scope closes
247+ // Subscribe to bus events, fiber interrupted when scope closes.
248+ // session.idle and server.instance.disposed are plugins' only chance to
249+ // drain async work (e.g. OTel span exporters) before src/index.ts's
250+ // top-level finally runs forceFlush and calls process.exit() — await
251+ // those handlers; keep the rest fire-and-forget for throughput.
248252 yield * bus . subscribeAll ( ) . pipe (
249253 Stream . runForEach ( ( input ) =>
250- Effect . sync ( ( ) => {
254+ Effect . promise ( async ( ) => {
255+ const awaitHook = input . type === "server.instance.disposed" || input . type === "session.idle"
251256 for ( const hook of hooks ) {
252- void hook [ "event" ] ?.( { event : input as any } )
257+ try {
258+ const ret = hook [ "event" ] ?.( { event : input as any } )
259+ if ( awaitHook && ret ) {
260+ await ret
261+ } else if ( ret ) {
262+ // Fire-and-forget path: surface async failures to logs instead of letting them
263+ // become unhandledRejections that hide which plugin/event broke.
264+ void Promise . resolve ( ret ) . catch ( ( err ) =>
265+ log . error ( "plugin event hook failed" , { error : err } ) ,
266+ )
267+ }
268+ } catch ( err ) {
269+ // Catches sync throws + awaited async rejections so one bad plugin can't kill
270+ // the subscription fiber and silently disable every other plugin.
271+ log . error ( "plugin event hook failed" , { error : err } )
272+ }
253273 }
254274 } ) ,
255275 ) ,
0 commit comments