mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 16:20:43 +00:00
feat: show restart handoffs in doctor
This commit is contained in:
@@ -69,6 +69,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
|
- Doctor/gateway: report recent supervisor restart handoffs in `openclaw doctor --deep`, using the installed service environment when available so service-managed clean exits are visible in guided diagnostics.
|
||||||
- Gateway/status: show recent supervisor restart handoffs in `openclaw gateway status --deep`, including JSON details, so clean service-managed restarts are reported as restart handoffs instead of opaque stopped-service diagnostics.
|
- Gateway/status: show recent supervisor restart handoffs in `openclaw gateway status --deep`, including JSON details, so clean service-managed restarts are reported as restart handoffs instead of opaque stopped-service diagnostics.
|
||||||
- Video generation: wait up to 20 minutes for slow fal/MiniMax queue-backed jobs, stop forwarding unsupported Google Veo generated-audio options, and normalize MiniMax `720P` requests to its supported `768P` resolution with the usual override warning/details instead of failing fallback.
|
- Video generation: wait up to 20 minutes for slow fal/MiniMax queue-backed jobs, stop forwarding unsupported Google Veo generated-audio options, and normalize MiniMax `720P` requests to its supported `768P` resolution with the usual override warning/details instead of failing fallback.
|
||||||
- Video generation: accept provider-specific aspect-ratio and resolution hints at the tool boundary, normalize `720P` to MiniMax's supported `768P`, and stop sending Google `generateAudio` on Gemini video requests so provider fallback can recover from model-specific parameter differences. Thanks @vincentkoc.
|
- Video generation: accept provider-specific aspect-ratio and resolution hints at the tool boundary, normalize `720P` to MiniMax's supported `768P`, and stop sending Google `generateAudio` on Gemini video requests so provider fallback can recover from model-specific parameter differences. Thanks @vincentkoc.
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ openclaw doctor --generate-gateway-token
|
|||||||
- `--force`: apply aggressive repairs, including overwriting custom service config when needed
|
- `--force`: apply aggressive repairs, including overwriting custom service config when needed
|
||||||
- `--non-interactive`: run without prompts; safe migrations and non-service repairs only
|
- `--non-interactive`: run without prompts; safe migrations and non-service repairs only
|
||||||
- `--generate-gateway-token`: generate and configure a gateway token
|
- `--generate-gateway-token`: generate and configure a gateway token
|
||||||
- `--deep`: scan system services for extra gateway installs
|
- `--deep`: scan system services for extra gateway installs and report recent Gateway supervisor restart handoffs
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import type { ExtraGatewayService } from "../daemon/inspect.js";
|
import type { ExtraGatewayService } from "../daemon/inspect.js";
|
||||||
import * as launchd from "../daemon/launchd.js";
|
import * as launchd from "../daemon/launchd.js";
|
||||||
|
import type { GatewayRestartHandoff } from "../infra/restart-handoff.js";
|
||||||
import { withEnvAsync } from "../test-utils/env.js";
|
import { withEnvAsync } from "../test-utils/env.js";
|
||||||
import { createDoctorPrompter } from "./doctor-prompter.js";
|
import { createDoctorPrompter } from "./doctor-prompter.js";
|
||||||
import { EXTERNAL_SERVICE_REPAIR_NOTE } from "./doctor-service-repair-policy.js";
|
import { EXTERNAL_SERVICE_REPAIR_NOTE } from "./doctor-service-repair-policy.js";
|
||||||
@@ -18,6 +19,9 @@ const sleep = vi.hoisted(() => vi.fn(async () => {}));
|
|||||||
const healthCommand = vi.hoisted(() => vi.fn(async () => {}));
|
const healthCommand = vi.hoisted(() => vi.fn(async () => {}));
|
||||||
const inspectPortUsage = vi.hoisted(() => vi.fn());
|
const inspectPortUsage = vi.hoisted(() => vi.fn());
|
||||||
const readLastGatewayErrorLine = vi.hoisted(() => vi.fn(async () => null));
|
const readLastGatewayErrorLine = vi.hoisted(() => vi.fn(async () => null));
|
||||||
|
const readGatewayRestartHandoffSync = vi.hoisted(() =>
|
||||||
|
vi.fn<() => GatewayRestartHandoff | null>(() => null),
|
||||||
|
);
|
||||||
const findSystemGatewayServices = vi.hoisted(() =>
|
const findSystemGatewayServices = vi.hoisted(() =>
|
||||||
vi.fn<() => Promise<ExtraGatewayService[]>>(async () => []),
|
vi.fn<() => Promise<ExtraGatewayService[]>>(async () => []),
|
||||||
);
|
);
|
||||||
@@ -82,6 +86,16 @@ vi.mock("../infra/ports.js", () => ({
|
|||||||
formatPortDiagnostics: vi.fn(() => []),
|
formatPortDiagnostics: vi.fn(() => []),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock("../infra/restart-handoff.js", async () => {
|
||||||
|
const actual = await vi.importActual<typeof import("../infra/restart-handoff.js")>(
|
||||||
|
"../infra/restart-handoff.js",
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
readGatewayRestartHandoffSync,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock("../infra/wsl.js", () => ({
|
vi.mock("../infra/wsl.js", () => ({
|
||||||
isWSL: vi.fn(async () => false),
|
isWSL: vi.fn(async () => false),
|
||||||
}));
|
}));
|
||||||
@@ -133,7 +147,9 @@ describe("maybeRepairGatewayDaemon", () => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
service.isLoaded.mockResolvedValue(true);
|
service.isLoaded.mockResolvedValue(true);
|
||||||
service.readRuntime.mockResolvedValue({ status: "running" });
|
service.readRuntime.mockResolvedValue({ status: "running" });
|
||||||
|
service.readCommand.mockResolvedValue(null);
|
||||||
service.restart.mockResolvedValue({ outcome: "completed" });
|
service.restart.mockResolvedValue({ outcome: "completed" });
|
||||||
|
readGatewayRestartHandoffSync.mockReturnValue(null);
|
||||||
findSystemGatewayServices.mockResolvedValue([]);
|
findSystemGatewayServices.mockResolvedValue([]);
|
||||||
inspectPortUsage.mockResolvedValue({
|
inspectPortUsage.mockResolvedValue({
|
||||||
port: 18789,
|
port: 18789,
|
||||||
@@ -245,6 +261,64 @@ describe("maybeRepairGatewayDaemon", () => {
|
|||||||
await runScheduledGatewayRepair("Restart gateway service now?");
|
await runScheduledGatewayRepair("Restart gateway service now?");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("reports recent restart handoffs during deep doctor", async () => {
|
||||||
|
setPlatform("linux");
|
||||||
|
service.readCommand.mockResolvedValueOnce({
|
||||||
|
programArguments: ["/bin/node", "cli", "gateway"],
|
||||||
|
environment: {
|
||||||
|
OPENCLAW_STATE_DIR: "/tmp/openclaw-service",
|
||||||
|
OPENCLAW_CONFIG_PATH: "/tmp/openclaw-service/openclaw.json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
readGatewayRestartHandoffSync.mockReturnValueOnce({
|
||||||
|
kind: "gateway-supervisor-restart-handoff",
|
||||||
|
version: 1,
|
||||||
|
intentId: "intent-1",
|
||||||
|
pid: 12_345,
|
||||||
|
createdAt: 10_000,
|
||||||
|
expiresAt: 70_000,
|
||||||
|
reason: "plugin source changed",
|
||||||
|
source: "plugin-change",
|
||||||
|
restartKind: "full-process",
|
||||||
|
supervisorMode: "systemd",
|
||||||
|
});
|
||||||
|
|
||||||
|
await maybeRepairGatewayDaemon({
|
||||||
|
cfg: { gateway: {} },
|
||||||
|
runtime: { log: vi.fn(), error: vi.fn(), exit: vi.fn() },
|
||||||
|
prompter: createDoctorPrompter({
|
||||||
|
runtime: { log: vi.fn(), error: vi.fn(), exit: vi.fn() },
|
||||||
|
options: { deep: true, nonInteractive: true },
|
||||||
|
}),
|
||||||
|
options: { deep: true, nonInteractive: true },
|
||||||
|
gatewayDetailsMessage: "details",
|
||||||
|
healthOk: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(readGatewayRestartHandoffSync).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
OPENCLAW_STATE_DIR: "/tmp/openclaw-service",
|
||||||
|
OPENCLAW_CONFIG_PATH: "/tmp/openclaw-service/openclaw.json",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(note).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining("Recent restart handoff: full-process via systemd"),
|
||||||
|
"Gateway",
|
||||||
|
);
|
||||||
|
expect(note).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining("reason=plugin source changed"),
|
||||||
|
"Gateway",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not read restart handoffs during normal doctor", async () => {
|
||||||
|
setPlatform("linux");
|
||||||
|
|
||||||
|
await runNonInteractiveRepair();
|
||||||
|
|
||||||
|
expect(readGatewayRestartHandoffSync).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("skips start verification when a stopped service start is only scheduled", async () => {
|
it("skips start verification when a stopped service start is only scheduled", async () => {
|
||||||
service.readRuntime.mockResolvedValue({ status: "stopped" });
|
service.readRuntime.mockResolvedValue({ status: "stopped" });
|
||||||
await runScheduledGatewayRepair("Start gateway service now?");
|
await runScheduledGatewayRepair("Start gateway service now?");
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ import { describeGatewayServiceRestart, resolveGatewayService } from "../daemon/
|
|||||||
import { renderSystemdUnavailableHints } from "../daemon/systemd-hints.js";
|
import { renderSystemdUnavailableHints } from "../daemon/systemd-hints.js";
|
||||||
import { isSystemdUserServiceAvailable } from "../daemon/systemd.js";
|
import { isSystemdUserServiceAvailable } from "../daemon/systemd.js";
|
||||||
import { formatPortDiagnostics, inspectPortUsage } from "../infra/ports.js";
|
import { formatPortDiagnostics, inspectPortUsage } from "../infra/ports.js";
|
||||||
|
import {
|
||||||
|
formatGatewayRestartHandoffDiagnostic,
|
||||||
|
readGatewayRestartHandoffSync,
|
||||||
|
} from "../infra/restart-handoff.js";
|
||||||
import { isWSL } from "../infra/wsl.js";
|
import { isWSL } from "../infra/wsl.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
import { note } from "../terminal/note.js";
|
import { note } from "../terminal/note.js";
|
||||||
@@ -126,8 +130,23 @@ export async function maybeRepairGatewayDaemon(params: {
|
|||||||
loaded = false;
|
loaded = false;
|
||||||
}
|
}
|
||||||
let serviceRuntime: Awaited<ReturnType<typeof service.readRuntime>> | undefined;
|
let serviceRuntime: Awaited<ReturnType<typeof service.readRuntime>> | undefined;
|
||||||
|
const command = params.options.deep
|
||||||
|
? await Promise.resolve(service.readCommand(process.env)).catch(() => null)
|
||||||
|
: null;
|
||||||
|
const serviceEnv = command?.environment
|
||||||
|
? ({
|
||||||
|
...process.env,
|
||||||
|
...command.environment,
|
||||||
|
} satisfies NodeJS.ProcessEnv)
|
||||||
|
: process.env;
|
||||||
if (loaded) {
|
if (loaded) {
|
||||||
serviceRuntime = await service.readRuntime(process.env).catch(() => undefined);
|
serviceRuntime = await service.readRuntime(serviceEnv).catch(() => undefined);
|
||||||
|
}
|
||||||
|
if (params.options.deep) {
|
||||||
|
const handoff = readGatewayRestartHandoffSync(serviceEnv);
|
||||||
|
if (handoff) {
|
||||||
|
note(formatGatewayRestartHandoffDiagnostic(handoff), "Gateway");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.platform === "darwin" && params.cfg.gateway?.mode !== "remote") {
|
if (process.platform === "darwin" && params.cfg.gateway?.mode !== "remote") {
|
||||||
|
|||||||
Reference in New Issue
Block a user