@@ -464,6 +464,80 @@ export function createDispatcherPlugin(config: DispatcherPluginConfig = {}): Plu
464464 }
465465 ctx . logger . info ( `[Dispatcher] Registered ${ routes . length } AI routes` ) ;
466466 } ) ;
467+
468+ // ── Fallback: recover routes cached before hook was registered ──
469+ // If AIServicePlugin.start() ran before DispatcherPlugin.start()
470+ // (possible when plugin start order differs from registration order),
471+ // the 'ai:routes' trigger fires with no listener. The AIServicePlugin
472+ // caches the routes on the kernel as __aiRoutes so we can recover here.
473+ const cachedRoutes = ( kernel as any ) . __aiRoutes as RouteDefinition [ ] | undefined ;
474+ if ( cachedRoutes && Array . isArray ( cachedRoutes ) && cachedRoutes . length > 0 ) {
475+ let registered = 0 ;
476+ for ( const route of cachedRoutes ) {
477+ const routePath = route . path . startsWith ( '/api/v1' )
478+ ? route . path
479+ : `${ prefix } ${ route . path } ` ;
480+
481+ const handler = async ( req : any , res : any ) => {
482+ try {
483+ const result = await route . handler ( {
484+ body : req . body ,
485+ params : req . params ,
486+ query : req . query ,
487+ } ) ;
488+
489+ if ( result . stream && result . events ) {
490+ res . status ( result . status ) ;
491+ if ( result . headers ) {
492+ for ( const [ k , v ] of Object . entries ( result . headers ) ) {
493+ res . header ( k , v as string ) ;
494+ }
495+ } else {
496+ res . header ( 'Content-Type' , 'text/event-stream' ) ;
497+ res . header ( 'Cache-Control' , 'no-cache' ) ;
498+ res . header ( 'Connection' , 'keep-alive' ) ;
499+ }
500+ if ( typeof res . write === 'function' && typeof res . end === 'function' ) {
501+ for await ( const event of result . events ) {
502+ res . write ( typeof event === 'string' ? event : `data: ${ JSON . stringify ( event ) } \n\n` ) ;
503+ }
504+ res . end ( ) ;
505+ } else {
506+ const events = [ ] ;
507+ for await ( const event of result . events ) {
508+ events . push ( event ) ;
509+ }
510+ res . json ( { events } ) ;
511+ }
512+ } else {
513+ res . status ( result . status ) ;
514+ if ( result . body !== undefined ) {
515+ res . json ( result . body ) ;
516+ } else {
517+ res . end ( ) ;
518+ }
519+ }
520+ } catch ( err : any ) {
521+ errorResponse ( err , res ) ;
522+ }
523+ } ;
524+
525+ const m = route . method . toLowerCase ( ) ;
526+ if ( m === 'get' && typeof server . get === 'function' ) {
527+ server . get ( routePath , handler ) ;
528+ registered ++ ;
529+ } else if ( m === 'post' && typeof server . post === 'function' ) {
530+ server . post ( routePath , handler ) ;
531+ registered ++ ;
532+ } else if ( m === 'delete' && typeof server . delete === 'function' ) {
533+ server . delete ( routePath , handler ) ;
534+ registered ++ ;
535+ }
536+ }
537+ if ( registered > 0 ) {
538+ ctx . logger . info ( `[Dispatcher] Recovered ${ registered } cached AI routes (hook timing fallback)` ) ;
539+ }
540+ }
467541 } ,
468542 } ;
469543}
0 commit comments