1+ import type { PluginHealthErrorSummary } from "../../commands/health.types.js" ;
12import type { GatewayServiceRuntime } from "../../daemon/service-runtime.js" ;
23import type { GatewayService } from "../../daemon/service.js" ;
34import { probeGateway } from "../../gateway/probe.js" ;
@@ -22,14 +23,20 @@ export const DEFAULT_RESTART_HEALTH_ATTEMPTS = Math.ceil(
2223const STOPPED_FREE_EARLY_EXIT_GRACE_MS = 10_000 ;
2324const WINDOWS_STOPPED_FREE_EARLY_EXIT_GRACE_MS = 90_000 ;
2425
25- export type GatewayRestartWaitOutcome = "healthy" | "stale-pids" | "stopped-free" | "timeout" ;
26+ export type GatewayRestartWaitOutcome =
27+ | "healthy"
28+ | "plugin-errors"
29+ | "stale-pids"
30+ | "stopped-free"
31+ | "timeout" ;
2632
2733export type GatewayRestartSnapshot = {
2834 runtime : GatewayServiceRuntime ;
2935 portUsage : PortUsage ;
3036 healthy : boolean ;
3137 staleGatewayPids : number [ ] ;
3238 gatewayVersion ?: string | null ;
39+ activatedPluginErrors ?: PluginHealthErrorSummary [ ] ;
3340 expectedVersion ?: string ;
3441 versionMismatch ?: {
3542 expected : string ;
@@ -47,6 +54,7 @@ export type GatewayPortHealthSnapshot = {
4754type GatewayReachability = {
4855 reachable : boolean ;
4956 gatewayVersion : string | null ;
57+ activatedPluginErrors : PluginHealthErrorSummary [ ] ;
5058} ;
5159
5260function hasListenerAttributionGap ( portUsage : PortUsage ) : boolean {
@@ -101,18 +109,73 @@ function applyExpectedVersion(
101109 } ;
102110}
103111
104- async function confirmGatewayReachable ( port : number ) : Promise < GatewayReachability > {
112+ function readActivatedPluginErrors ( health : unknown ) : PluginHealthErrorSummary [ ] {
113+ if ( ! health || typeof health !== "object" ) {
114+ return [ ] ;
115+ }
116+ const plugins = ( health as { plugins ?: unknown } ) . plugins ;
117+ if ( ! plugins || typeof plugins !== "object" ) {
118+ return [ ] ;
119+ }
120+ const errors = ( plugins as { errors ?: unknown } ) . errors ;
121+ if ( ! Array . isArray ( errors ) ) {
122+ return [ ] ;
123+ }
124+ return errors
125+ . filter ( ( entry ) : entry is PluginHealthErrorSummary => {
126+ if ( ! entry || typeof entry !== "object" ) {
127+ return false ;
128+ }
129+ const candidate = entry as Partial < PluginHealthErrorSummary > ;
130+ return (
131+ candidate . activated === true &&
132+ typeof candidate . id === "string" &&
133+ typeof candidate . error === "string"
134+ ) ;
135+ } )
136+ . map ( ( entry ) => {
137+ const error : PluginHealthErrorSummary = {
138+ id : entry . id ,
139+ origin : typeof entry . origin === "string" ? entry . origin : "unknown" ,
140+ activated : true ,
141+ error : entry . error ,
142+ } ;
143+ if ( typeof entry . activationSource === "string" ) {
144+ error . activationSource = entry . activationSource ;
145+ }
146+ if ( typeof entry . activationReason === "string" ) {
147+ error . activationReason = entry . activationReason ;
148+ }
149+ if ( typeof entry . failurePhase === "string" ) {
150+ error . failurePhase = entry . failurePhase ;
151+ }
152+ return error ;
153+ } ) ;
154+ }
155+
156+ function applyActivatedPluginErrors ( snapshot : GatewayRestartSnapshot ) : GatewayRestartSnapshot {
157+ if ( ! snapshot . activatedPluginErrors ?. length ) {
158+ return snapshot ;
159+ }
160+ return { ...snapshot , healthy : false } ;
161+ }
162+
163+ async function confirmGatewayReachable ( params : {
164+ port : number ;
165+ includeHealthDetails ?: boolean ;
166+ } ) : Promise < GatewayReachability > {
105167 const token = normalizeOptionalString ( process . env . OPENCLAW_GATEWAY_TOKEN ) ;
106168 const password = normalizeOptionalString ( process . env . OPENCLAW_GATEWAY_PASSWORD ) ;
107169 const probe = await probeGateway ( {
108- url : `ws://127.0.0.1:${ port } ` ,
170+ url : `ws://127.0.0.1:${ params . port } ` ,
109171 auth : token || password ? { token, password } : undefined ,
110172 timeoutMs : 3_000 ,
111- includeDetails : false ,
173+ includeDetails : params . includeHealthDetails === true ,
112174 } ) ;
113175 return {
114176 reachable : probe . ok || looksLikeAuthClose ( probe . close ?. code , probe . close ?. reason ) ,
115177 gatewayVersion : probe . server ?. version ?? null ,
178+ activatedPluginErrors : readActivatedPluginErrors ( probe . health ) ,
116179 } ;
117180}
118181
@@ -133,7 +196,7 @@ async function inspectGatewayPortHealth(port: number): Promise<GatewayPortHealth
133196 let healthy = false ;
134197 if ( portUsage . status === "busy" ) {
135198 try {
136- healthy = ( await confirmGatewayReachable ( port ) ) . reachable ;
199+ healthy = ( await confirmGatewayReachable ( { port } ) ) . reachable ;
137200 } catch {
138201 // best-effort probe
139202 }
@@ -152,8 +215,15 @@ export async function inspectGatewayRestart(params: {
152215 const env = params . env ?? process . env ;
153216 const expectedVersion = normalizeOptionalString ( params . expectedVersion ) ;
154217 let reachability : GatewayReachability | null = null ;
218+ let activatedPluginErrors : PluginHealthErrorSummary [ ] = [ ] ;
155219 const loadReachability = async ( ) => {
156- reachability ??= await confirmGatewayReachable ( params . port ) ;
220+ if ( ! reachability ) {
221+ reachability = await confirmGatewayReachable ( {
222+ port : params . port ,
223+ includeHealthDetails : Boolean ( expectedVersion ) ,
224+ } ) ;
225+ activatedPluginErrors = reachability . activatedPluginErrors ;
226+ }
157227 return reachability ;
158228 } ;
159229 let runtime : GatewayServiceRuntime = { status : "unknown" } ;
@@ -180,15 +250,20 @@ export async function inspectGatewayRestart(params: {
180250 try {
181251 const reachable = await loadReachability ( ) ;
182252 if ( reachable . reachable ) {
183- return applyExpectedVersion (
184- {
185- runtime,
186- portUsage,
187- healthy : true ,
188- staleGatewayPids : [ ] ,
189- gatewayVersion : reachable . gatewayVersion ,
190- } ,
191- expectedVersion ,
253+ return applyActivatedPluginErrors (
254+ applyExpectedVersion (
255+ {
256+ runtime,
257+ portUsage,
258+ healthy : true ,
259+ staleGatewayPids : [ ] ,
260+ gatewayVersion : reachable . gatewayVersion ,
261+ ...( reachable . activatedPluginErrors . length > 0
262+ ? { activatedPluginErrors : reachable . activatedPluginErrors }
263+ : { } ) ,
264+ } ,
265+ expectedVersion ,
266+ ) ,
192267 ) ;
193268 }
194269 } catch {
@@ -228,6 +303,9 @@ export async function inspectGatewayRestart(params: {
228303 const reachable = await loadReachability ( ) ;
229304 healthy = reachable . reachable ;
230305 gatewayVersion = reachable . gatewayVersion ;
306+ if ( reachable . activatedPluginErrors . length > 0 ) {
307+ healthy = false ;
308+ }
231309 } catch {
232310 healthy = false ;
233311 }
@@ -261,15 +339,18 @@ export async function inspectGatewayRestart(params: {
261339 ] ) ,
262340 ) ;
263341
264- return applyExpectedVersion (
265- {
266- runtime,
267- portUsage,
268- healthy,
269- staleGatewayPids,
270- ...( gatewayVersion !== undefined ? { gatewayVersion } : { } ) ,
271- } ,
272- expectedVersion ,
342+ return applyActivatedPluginErrors (
343+ applyExpectedVersion (
344+ {
345+ runtime,
346+ portUsage,
347+ healthy,
348+ staleGatewayPids,
349+ ...( gatewayVersion !== undefined ? { gatewayVersion } : { } ) ,
350+ ...( activatedPluginErrors . length ? { activatedPluginErrors } : { } ) ,
351+ } ,
352+ expectedVersion ,
353+ ) ,
273354 ) ;
274355}
275356
@@ -330,6 +411,9 @@ export async function waitForGatewayHealthyRestart(params: {
330411 if ( snapshot . healthy ) {
331412 return withWaitContext ( snapshot , "healthy" , attempt * delayMs ) ;
332413 }
414+ if ( snapshot . activatedPluginErrors ?. length ) {
415+ return withWaitContext ( snapshot , "plugin-errors" , attempt * delayMs ) ;
416+ }
333417 if ( snapshot . staleGatewayPids . length > 0 && snapshot . runtime . status !== "running" ) {
334418 return withWaitContext ( snapshot , "stale-pids" , attempt * delayMs ) ;
335419 }
@@ -399,6 +483,12 @@ export function renderRestartDiagnostics(snapshot: GatewayRestartSnapshot): stri
399483 `Gateway version mismatch: expected ${ snapshot . versionMismatch . expected } , running gateway reported ${ actual } .` ,
400484 ) ;
401485 }
486+ if ( snapshot . activatedPluginErrors ?. length ) {
487+ lines . push ( "Activated plugin load errors:" ) ;
488+ for ( const plugin of snapshot . activatedPluginErrors ) {
489+ lines . push ( `- ${ plugin . id } : ${ plugin . error } ` ) ;
490+ }
491+ }
402492 const runtimeSummary = [
403493 snapshot . runtime . status ? `status=${ snapshot . runtime . status } ` : null ,
404494 snapshot . runtime . state ? `state=${ snapshot . runtime . state } ` : null ,
0 commit comments