@@ -61,6 +61,8 @@ export interface GatewayStatus {
6161 connectedAt ?: number ;
6262 version ?: string ;
6363 reconnectAttempts ?: number ;
64+ /** True once the gateway's internal subsystems (skills, plugins) are ready for RPC calls. */
65+ gatewayReady ?: boolean ;
6466}
6567
6668/**
@@ -119,9 +121,11 @@ export class GatewayManager extends EventEmitter {
119121 private static readonly HEARTBEAT_TIMEOUT_MS_WIN = 25_000 ;
120122 private static readonly HEARTBEAT_MAX_MISSES_WIN = 5 ;
121123 public static readonly RESTART_COOLDOWN_MS = 5_000 ;
124+ private static readonly GATEWAY_READY_FALLBACK_MS = 30_000 ;
122125 private lastRestartAt = 0 ;
123126 /** Set by scheduleReconnect() before calling start() to signal auto-reconnect. */
124127 private isAutoReconnectStart = false ;
128+ private gatewayReadyFallbackTimer : NodeJS . Timeout | null = null ;
125129
126130 constructor ( config ?: Partial < ReconnectConfig > ) {
127131 super ( ) ;
@@ -152,6 +156,14 @@ export class GatewayManager extends EventEmitter {
152156 this . reconnectConfig = { ...DEFAULT_RECONNECT_CONFIG , ...config } ;
153157 // Device identity is loaded lazily in start() — not in the constructor —
154158 // so that async file I/O and key generation don't block module loading.
159+
160+ this . on ( 'gateway:ready' , ( ) => {
161+ this . clearGatewayReadyFallback ( ) ;
162+ if ( this . status . state === 'running' && ! this . status . gatewayReady ) {
163+ logger . info ( 'Gateway subsystems ready (event received)' ) ;
164+ this . setStatus ( { gatewayReady : true } ) ;
165+ }
166+ } ) ;
155167 }
156168
157169 private async initDeviceIdentity ( ) : Promise < void > {
@@ -231,12 +243,16 @@ export class GatewayManager extends EventEmitter {
231243 this . reconnectAttempts = 0 ;
232244 }
233245 this . isAutoReconnectStart = false ; // consume the flag
234- this . setStatus ( { state : 'starting' , reconnectAttempts : this . reconnectAttempts } ) ;
246+ this . setStatus ( { state : 'starting' , reconnectAttempts : this . reconnectAttempts , gatewayReady : false } ) ;
235247
236248 // Check if Python environment is ready (self-healing) asynchronously.
237249 // Fire-and-forget: only needs to run once, not on every retry.
238250 warmupManagedPythonReadiness ( ) ;
239251
252+ const t0 = Date . now ( ) ;
253+ let tSpawned = 0 ;
254+ let tReady = 0 ;
255+
240256 try {
241257 await runGatewayStartupSequence ( {
242258 port : this . status . port ,
@@ -262,7 +278,6 @@ export class GatewayManager extends EventEmitter {
262278 await this . connect ( port , externalToken ) ;
263279 } ,
264280 onConnectedToExistingGateway : ( ) => {
265-
266281 // If the existing gateway is actually our own spawned UtilityProcess
267282 // (e.g. after a self-restart code=1012), keep ownership so that
268283 // stop() can still terminate the process during a restart() cycle.
@@ -288,16 +303,24 @@ export class GatewayManager extends EventEmitter {
288303 } ,
289304 startProcess : async ( ) => {
290305 await this . startProcess ( ) ;
306+ tSpawned = Date . now ( ) ;
291307 } ,
292308 waitForReady : async ( port ) => {
293309 await waitForGatewayReady ( {
294310 port,
295311 getProcessExitCode : ( ) => this . processExitCode ,
296312 } ) ;
313+ tReady = Date . now ( ) ;
297314 } ,
298315 onConnectedToManagedGateway : ( ) => {
299316 this . startHealthCheck ( ) ;
300- logger . debug ( 'Gateway started successfully' ) ;
317+ const tConnected = Date . now ( ) ;
318+ logger . info ( '[metric] gateway.startup' , {
319+ configSyncMs : tSpawned ? tSpawned - t0 : undefined ,
320+ spawnToReadyMs : tReady && tSpawned ? tReady - tSpawned : undefined ,
321+ readyToConnectMs : tReady ? tConnected - tReady : undefined ,
322+ totalMs : tConnected - t0 ,
323+ } ) ;
301324 } ,
302325 runDoctorRepair : async ( ) => await runOpenClawDoctorRepair ( ) ,
303326 onDoctorRepairSuccess : ( ) => {
@@ -390,7 +413,7 @@ export class GatewayManager extends EventEmitter {
390413
391414 this . restartController . resetDeferredRestart ( ) ;
392415 this . isAutoReconnectStart = false ;
393- this . setStatus ( { state : 'stopped' , error : undefined , pid : undefined , connectedAt : undefined , uptime : undefined } ) ;
416+ this . setStatus ( { state : 'stopped' , error : undefined , pid : undefined , connectedAt : undefined , uptime : undefined , gatewayReady : undefined } ) ;
394417 }
395418
396419 /**
@@ -663,6 +686,25 @@ export class GatewayManager extends EventEmitter {
663686 clearTimeout ( this . reloadDebounceTimer ) ;
664687 this . reloadDebounceTimer = null ;
665688 }
689+ this . clearGatewayReadyFallback ( ) ;
690+ }
691+
692+ private clearGatewayReadyFallback ( ) : void {
693+ if ( this . gatewayReadyFallbackTimer ) {
694+ clearTimeout ( this . gatewayReadyFallbackTimer ) ;
695+ this . gatewayReadyFallbackTimer = null ;
696+ }
697+ }
698+
699+ private scheduleGatewayReadyFallback ( ) : void {
700+ this . clearGatewayReadyFallback ( ) ;
701+ this . gatewayReadyFallbackTimer = setTimeout ( ( ) => {
702+ this . gatewayReadyFallbackTimer = null ;
703+ if ( this . status . state === 'running' && ! this . status . gatewayReady ) {
704+ logger . info ( 'Gateway ready fallback triggered (no gateway.ready event within timeout)' ) ;
705+ this . setStatus ( { gatewayReady : true } ) ;
706+ }
707+ } , GatewayManager . GATEWAY_READY_FALLBACK_MS ) ;
666708 }
667709
668710 /**
@@ -843,6 +885,7 @@ export class GatewayManager extends EventEmitter {
843885 connectedAt : Date . now ( ) ,
844886 } ) ;
845887 this . startPing ( ) ;
888+ this . scheduleGatewayReadyFallback ( ) ;
846889 } ,
847890 onMessage : ( message ) => {
848891 this . handleMessage ( message ) ;
0 commit comments