mirror of https://github.com/openclaw/openclaw.git
UI: surface gateway restart reasons in dashboard disconnect state (#46580)
* UI: surface gateway shutdown reason * UI: add gateway restart disconnect tests * Changelog: add dashboard restart reason fix * UI: cover reconnect shutdown state
This commit is contained in:
parent
cbec476b6b
commit
39377b7a20
|
|
@ -21,6 +21,7 @@ Docs: https://docs.openclaw.ai
|
|||
- CI/channel test routing: move the built-in channel suites into `test:channels` and keep them out of `test:extensions`, so extension CI no longer fails after the channel migration while targeted test routing still sends Slack, Signal, and iMessage suites to the right lane. (#46066) Thanks @scoootscooob.
|
||||
- Agents/usage tracking: stop forcing `supportsUsageInStreaming: false` on non-native openai-completions endpoints so providers like DashScope, DeepSeek, and other OpenAI-compatible backends report token usage and cost instead of showing all zeros. (#46142)
|
||||
- Node/startup: remove leftover debug `console.log("node host PATH: ...")` that printed the resolved PATH on every `openclaw node run` invocation. (#46411)
|
||||
- Control UI/dashboard: preserve structured gateway shutdown reasons across restart disconnects so config-triggered restarts no longer fall back to `disconnected (1006): no reason`. (#46532) Thanks @vincentkoc.
|
||||
|
||||
## 2026.3.13
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
|||
import { GATEWAY_EVENT_UPDATE_AVAILABLE } from "../../../src/gateway/events.js";
|
||||
import { ConnectErrorDetailCodes } from "../../../src/gateway/protocol/connect-error-details.js";
|
||||
import { connectGateway, resolveControlUiClientVersion } from "./app-gateway.ts";
|
||||
import type { GatewayHelloOk } from "./gateway.ts";
|
||||
|
||||
const loadChatHistoryMock = vi.hoisted(() => vi.fn(async () => undefined));
|
||||
|
||||
|
|
@ -9,6 +10,7 @@ type GatewayClientMock = {
|
|||
start: ReturnType<typeof vi.fn>;
|
||||
stop: ReturnType<typeof vi.fn>;
|
||||
options: { clientVersion?: string };
|
||||
emitHello: (hello?: GatewayHelloOk) => void;
|
||||
emitClose: (info: {
|
||||
code: number;
|
||||
reason?: string;
|
||||
|
|
@ -39,6 +41,7 @@ vi.mock("./gateway.ts", () => {
|
|||
constructor(
|
||||
private opts: {
|
||||
clientVersion?: string;
|
||||
onHello?: (hello: GatewayHelloOk) => void;
|
||||
onClose?: (info: {
|
||||
code: number;
|
||||
reason: string;
|
||||
|
|
@ -52,6 +55,15 @@ vi.mock("./gateway.ts", () => {
|
|||
start: this.start,
|
||||
stop: this.stop,
|
||||
options: { clientVersion: this.opts.clientVersion },
|
||||
emitHello: (hello) => {
|
||||
this.opts.onHello?.(
|
||||
hello ?? {
|
||||
type: "hello-ok",
|
||||
protocol: 3,
|
||||
snapshot: {},
|
||||
},
|
||||
);
|
||||
},
|
||||
emitClose: (info) => {
|
||||
this.opts.onClose?.({
|
||||
code: info.code,
|
||||
|
|
@ -356,6 +368,93 @@ describe("connectGateway", () => {
|
|||
expect(host.lastErrorCode).toBe("AUTH_TOKEN_MISMATCH");
|
||||
});
|
||||
|
||||
it("surfaces shutdown restart reasons before the socket closes", () => {
|
||||
const host = createHost();
|
||||
|
||||
connectGateway(host);
|
||||
const client = gatewayClientInstances[0];
|
||||
expect(client).toBeDefined();
|
||||
|
||||
client.emitEvent({
|
||||
event: "shutdown",
|
||||
payload: {
|
||||
reason: "config change requires gateway restart (plugins.installs)",
|
||||
restartExpectedMs: 1500,
|
||||
},
|
||||
});
|
||||
client.emitClose({ code: 1006 });
|
||||
|
||||
expect(host.lastError).toBe(
|
||||
"Restarting: config change requires gateway restart (plugins.installs)",
|
||||
);
|
||||
expect(host.lastErrorCode).toBeNull();
|
||||
});
|
||||
|
||||
it("clears pending shutdown messages on successful hello after reconnect", () => {
|
||||
const host = createHost();
|
||||
|
||||
connectGateway(host);
|
||||
const client = gatewayClientInstances[0];
|
||||
expect(client).toBeDefined();
|
||||
|
||||
client.emitEvent({
|
||||
event: "shutdown",
|
||||
payload: {
|
||||
reason: "config change",
|
||||
restartExpectedMs: 1500,
|
||||
},
|
||||
});
|
||||
client.emitClose({ code: 1006 });
|
||||
|
||||
expect(host.lastError).toBe("Restarting: config change");
|
||||
|
||||
client.emitHello();
|
||||
expect(host.lastError).toBeNull();
|
||||
|
||||
client.emitClose({ code: 1006 });
|
||||
expect(host.lastError).toBe("disconnected (1006): no reason");
|
||||
});
|
||||
|
||||
it("keeps shutdown restart reasons on service restart closes", () => {
|
||||
const host = createHost();
|
||||
|
||||
connectGateway(host);
|
||||
const client = gatewayClientInstances[0];
|
||||
expect(client).toBeDefined();
|
||||
|
||||
client.emitEvent({
|
||||
event: "shutdown",
|
||||
payload: {
|
||||
reason: "gateway restarting",
|
||||
restartExpectedMs: 1500,
|
||||
},
|
||||
});
|
||||
client.emitClose({ code: 1012, reason: "service restart" });
|
||||
|
||||
expect(host.lastError).toBe("Restarting: gateway restarting");
|
||||
expect(host.lastErrorCode).toBeNull();
|
||||
});
|
||||
|
||||
it("prefers shutdown restart reasons over non-1012 close reasons", () => {
|
||||
const host = createHost();
|
||||
|
||||
connectGateway(host);
|
||||
const client = gatewayClientInstances[0];
|
||||
expect(client).toBeDefined();
|
||||
|
||||
client.emitEvent({
|
||||
event: "shutdown",
|
||||
payload: {
|
||||
reason: "gateway restarting",
|
||||
restartExpectedMs: 1500,
|
||||
},
|
||||
});
|
||||
client.emitClose({ code: 1001, reason: "going away" });
|
||||
|
||||
expect(host.lastError).toBe("Restarting: gateway restarting");
|
||||
expect(host.lastErrorCode).toBeNull();
|
||||
});
|
||||
|
||||
it("does not reload chat history for each live tool result event", () => {
|
||||
const host = createHost();
|
||||
|
||||
|
|
|
|||
|
|
@ -91,6 +91,10 @@ type SessionDefaultsSnapshot = {
|
|||
scope?: string;
|
||||
};
|
||||
|
||||
type GatewayHostWithShutdownMessage = GatewayHost & {
|
||||
pendingShutdownMessage?: string | null;
|
||||
};
|
||||
|
||||
export function resolveControlUiClientVersion(params: {
|
||||
gatewayUrl: string;
|
||||
serverVersion: string | null;
|
||||
|
|
@ -171,6 +175,8 @@ function applySessionDefaults(host: GatewayHost, defaults?: SessionDefaultsSnaps
|
|||
}
|
||||
|
||||
export function connectGateway(host: GatewayHost) {
|
||||
const shutdownHost = host as GatewayHostWithShutdownMessage;
|
||||
shutdownHost.pendingShutdownMessage = null;
|
||||
host.lastError = null;
|
||||
host.lastErrorCode = null;
|
||||
host.hello = null;
|
||||
|
|
@ -195,6 +201,7 @@ export function connectGateway(host: GatewayHost) {
|
|||
if (host.client !== client) {
|
||||
return;
|
||||
}
|
||||
shutdownHost.pendingShutdownMessage = null;
|
||||
host.connected = true;
|
||||
host.lastError = null;
|
||||
host.lastErrorCode = null;
|
||||
|
|
@ -234,9 +241,10 @@ export function connectGateway(host: GatewayHost) {
|
|||
: error.message;
|
||||
return;
|
||||
}
|
||||
host.lastError = `disconnected (${code}): ${reason || "no reason"}`;
|
||||
host.lastError =
|
||||
shutdownHost.pendingShutdownMessage ?? `disconnected (${code}): ${reason || "no reason"}`;
|
||||
} else {
|
||||
host.lastError = null;
|
||||
host.lastError = shutdownHost.pendingShutdownMessage ?? null;
|
||||
host.lastErrorCode = null;
|
||||
}
|
||||
},
|
||||
|
|
@ -347,6 +355,22 @@ function handleGatewayEventUnsafe(host: GatewayHost, evt: GatewayEventFrame) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (evt.event === "shutdown") {
|
||||
const payload = evt.payload as { reason?: unknown; restartExpectedMs?: unknown } | undefined;
|
||||
const reason =
|
||||
payload && typeof payload.reason === "string" && payload.reason.trim()
|
||||
? payload.reason.trim()
|
||||
: "gateway stopping";
|
||||
const shutdownMessage =
|
||||
typeof payload?.restartExpectedMs === "number"
|
||||
? `Restarting: ${reason}`
|
||||
: `Disconnected: ${reason}`;
|
||||
(host as GatewayHostWithShutdownMessage).pendingShutdownMessage = shutdownMessage;
|
||||
host.lastError = shutdownMessage;
|
||||
host.lastErrorCode = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (evt.event === "cron" && host.tab === "cron") {
|
||||
void loadCron(host as unknown as Parameters<typeof loadCron>[0]);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue