






















@@ -1,3 +1,4 @@
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;
242525-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";
26322733export type GatewayRestartSnapshot = {
2834runtime: GatewayServiceRuntime;
2935portUsage: PortUsage;
3036healthy: boolean;
3137staleGatewayPids: number[];
3238gatewayVersion?: string | null;
39+activatedPluginErrors?: PluginHealthErrorSummary[];
3340expectedVersion?: string;
3441versionMismatch?: {
3542expected: string;
@@ -47,6 +54,7 @@ export type GatewayPortHealthSnapshot = {
4754type GatewayReachability = {
4855reachable: boolean;
4956gatewayVersion: string | null;
57+activatedPluginErrors: PluginHealthErrorSummary[];
5058};
51595260function hasListenerAttributionGap(portUsage: PortUsage): boolean {
@@ -101,18 +109,73 @@ function applyExpectedVersion(
101109};
102110}
103111104-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> {
105167const token = normalizeOptionalString(process.env.OPENCLAW_GATEWAY_TOKEN);
106168const password = normalizeOptionalString(process.env.OPENCLAW_GATEWAY_PASSWORD);
107169const probe = await probeGateway({
108-url: `ws://127.0.0.1:${port}`,
170+url: `ws://127.0.0.1:${params.port}`,
109171auth: token || password ? { token, password } : undefined,
110172timeoutMs: 3_000,
111-includeDetails: false,
173+includeDetails: params.includeHealthDetails === true,
112174});
113175return {
114176reachable: probe.ok || looksLikeAuthClose(probe.close?.code, probe.close?.reason),
115177gatewayVersion: probe.server?.version ?? null,
178+activatedPluginErrors: readActivatedPluginErrors(probe.health),
116179};
117180}
118181@@ -133,7 +196,7 @@ async function inspectGatewayPortHealth(port: number): Promise<GatewayPortHealth
133196let healthy = false;
134197if (portUsage.status === "busy") {
135198try {
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: {
152215const env = params.env ?? process.env;
153216const expectedVersion = normalizeOptionalString(params.expectedVersion);
154217let reachability: GatewayReachability | null = null;
218+let activatedPluginErrors: PluginHealthErrorSummary[] = [];
155219const 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+}
157227return reachability;
158228};
159229let runtime: GatewayServiceRuntime = { status: "unknown" };
@@ -180,15 +250,20 @@ export async function inspectGatewayRestart(params: {
180250try {
181251const reachable = await loadReachability();
182252if (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: {
228303const reachable = await loadReachability();
229304healthy = reachable.reachable;
230305gatewayVersion = reachable.gatewayVersion;
306+if (reachable.activatedPluginErrors.length > 0) {
307+healthy = false;
308+}
231309} catch {
232310healthy = false;
233311}
@@ -261,15 +339,18 @@ export async function inspectGatewayRestart(params: {
261339]),
262340);
263341264-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: {
330411if (snapshot.healthy) {
331412return withWaitContext(snapshot, "healthy", attempt * delayMs);
332413}
414+if (snapshot.activatedPluginErrors?.length) {
415+return withWaitContext(snapshot, "plugin-errors", attempt * delayMs);
416+}
333417if (snapshot.staleGatewayPids.length > 0 && snapshot.runtime.status !== "running") {
334418return 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+}
402492const runtimeSummary = [
403493snapshot.runtime.status ? `status=${snapshot.runtime.status}` : null,
404494snapshot.runtime.state ? `state=${snapshot.runtime.state}` : null,
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。