mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:50:43 +00:00
feat: show restart handoffs in gateway status
This commit is contained in:
@@ -69,6 +69,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- 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: 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.
|
||||
- OpenAI/Google Meet: fail realtime voice connection attempts when the socket closes before `session.updated`, avoiding stuck Meet joins waiting on a bridge that never became ready. Thanks @vincentkoc.
|
||||
|
||||
@@ -295,6 +295,7 @@ openclaw gateway status --require-rpc
|
||||
- If the probe succeeds, unresolved auth-ref warnings are suppressed to avoid false positives.
|
||||
- Use `--require-rpc` in scripts and automation when a listening service is not enough and you need read-scope RPC calls to be healthy too.
|
||||
- `--deep` adds a best-effort scan for extra launchd/systemd/schtasks installs. When multiple gateway-like services are detected, human output prints cleanup hints and warns that most setups should run one gateway per machine.
|
||||
- `--deep` also reports a recent Gateway supervisor restart handoff when the service process exited cleanly for an external supervisor restart.
|
||||
- Human output includes the resolved file log path plus the CLI-vs-service config paths/validity snapshot to help diagnose profile or state-dir drift.
|
||||
|
||||
</Accordion>
|
||||
|
||||
@@ -3,6 +3,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createMockGatewayService } from "../../daemon/service.test-helpers.js";
|
||||
import type { GatewayRestartHandoff } from "../../infra/restart-handoff.js";
|
||||
import { captureEnv } from "../../test-utils/env.js";
|
||||
import type { GatewayRestartSnapshot } from "./restart-health.js";
|
||||
import { gatherDaemonStatus } from "./status.gather.js";
|
||||
@@ -27,6 +28,9 @@ const inspectPortUsage = vi.fn(async (port: number) => ({
|
||||
hints: [],
|
||||
}));
|
||||
const readLastGatewayErrorLine = vi.fn(async (_env?: NodeJS.ProcessEnv) => null);
|
||||
const readGatewayRestartHandoffSync = vi.fn<
|
||||
(_env?: NodeJS.ProcessEnv) => GatewayRestartHandoff | null
|
||||
>(() => null);
|
||||
const auditGatewayServiceConfig = vi.fn(async (_opts?: unknown) => undefined);
|
||||
const serviceIsLoaded = vi.fn(async (_opts?: unknown) => true);
|
||||
const serviceReadRuntime = vi.fn(async (_env?: NodeJS.ProcessEnv) => ({ status: "running" }));
|
||||
@@ -136,6 +140,10 @@ vi.mock("../../infra/ports.js", () => ({
|
||||
formatPortDiagnostics: () => [],
|
||||
}));
|
||||
|
||||
vi.mock("../../infra/restart-handoff.js", () => ({
|
||||
readGatewayRestartHandoffSync: (env?: NodeJS.ProcessEnv) => readGatewayRestartHandoffSync(env),
|
||||
}));
|
||||
|
||||
vi.mock("../../infra/tailnet.js", () => ({
|
||||
pickPrimaryTailnetIPv4: () => pickPrimaryTailnetIPv4(),
|
||||
}));
|
||||
@@ -173,6 +181,7 @@ describe("gatherDaemonStatus", () => {
|
||||
callGatewayStatusProbe.mockClear();
|
||||
loadGatewayTlsRuntime.mockClear();
|
||||
inspectGatewayRestart.mockClear();
|
||||
readGatewayRestartHandoffSync.mockClear();
|
||||
readConfigFileSnapshotCalls.mockClear();
|
||||
loadConfigCalls.mockClear();
|
||||
daemonLoadedConfig = {
|
||||
@@ -369,6 +378,49 @@ describe("gatherDaemonStatus", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("surfaces recent service restart handoffs only during deep status", async () => {
|
||||
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: "launchd",
|
||||
});
|
||||
|
||||
const status = await gatherDaemonStatus({
|
||||
rpc: {},
|
||||
probe: false,
|
||||
deep: true,
|
||||
});
|
||||
|
||||
expect(readGatewayRestartHandoffSync).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
OPENCLAW_STATE_DIR: "/tmp/openclaw-daemon",
|
||||
OPENCLAW_CONFIG_PATH: "/tmp/openclaw-daemon/openclaw.json",
|
||||
}),
|
||||
);
|
||||
expect(status.service.restartHandoff).toMatchObject({
|
||||
reason: "plugin source changed",
|
||||
restartKind: "full-process",
|
||||
supervisorMode: "launchd",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not read restart handoffs during normal status", async () => {
|
||||
await gatherDaemonStatus({
|
||||
rpc: {},
|
||||
probe: false,
|
||||
deep: false,
|
||||
});
|
||||
|
||||
expect(readGatewayRestartHandoffSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses the fast config path for plain same-file status reads", async () => {
|
||||
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-status-config-"));
|
||||
const configPath = path.join(tmp, "openclaw.json");
|
||||
|
||||
@@ -29,6 +29,10 @@ import {
|
||||
type PortListener,
|
||||
type PortUsageStatus,
|
||||
} from "../../infra/ports.js";
|
||||
import {
|
||||
readGatewayRestartHandoffSync,
|
||||
type GatewayRestartHandoff,
|
||||
} from "../../infra/restart-handoff.js";
|
||||
import { resolveConfiguredLogFilePath } from "../../logging/log-file-path.js";
|
||||
import { createLazyImportLoader } from "../../shared/lazy-promise.js";
|
||||
import { normalizeListenerAddress, parsePortFromArgs, pickProbeHostForBind } from "./shared.js";
|
||||
@@ -247,6 +251,7 @@ export type DaemonStatus = {
|
||||
} | null;
|
||||
runtime?: GatewayServiceRuntime;
|
||||
configAudit?: ServiceConfigAudit;
|
||||
restartHandoff?: GatewayRestartHandoff;
|
||||
};
|
||||
config?: {
|
||||
cli: ConfigSummary;
|
||||
@@ -437,6 +442,7 @@ export async function gatherDaemonStatus(
|
||||
service.isLoaded({ env: serviceEnv }).catch(() => false),
|
||||
service.readRuntime(serviceEnv).catch((err) => ({ status: "unknown", detail: String(err) })),
|
||||
]);
|
||||
const restartHandoff = opts.deep ? readGatewayRestartHandoffSync(serviceEnv) : null;
|
||||
const configAudit = command
|
||||
? await loadServiceAuditModule().then(({ auditGatewayServiceConfig }) =>
|
||||
auditGatewayServiceConfig({
|
||||
@@ -556,6 +562,7 @@ export async function gatherDaemonStatus(
|
||||
command,
|
||||
runtime,
|
||||
configAudit,
|
||||
...(restartHandoff ? { restartHandoff } : {}),
|
||||
},
|
||||
config: {
|
||||
cli: cliConfigSummary,
|
||||
|
||||
@@ -157,6 +157,41 @@ describe("printDaemonStatus", () => {
|
||||
expect(runtime.log).toHaveBeenCalledWith(expect.stringContaining("Capability: write-capable"));
|
||||
});
|
||||
|
||||
it("prints restart handoff diagnostics when deep status gathered one", () => {
|
||||
printDaemonStatus(
|
||||
{
|
||||
service: {
|
||||
label: "LaunchAgent",
|
||||
loaded: true,
|
||||
loadedText: "loaded",
|
||||
notLoadedText: "not loaded",
|
||||
runtime: { status: "stopped" },
|
||||
restartHandoff: {
|
||||
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: "launchd",
|
||||
},
|
||||
},
|
||||
extraServices: [],
|
||||
},
|
||||
{ json: false },
|
||||
);
|
||||
|
||||
expect(runtime.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Recent restart handoff: full-process via launchd"),
|
||||
);
|
||||
expect(runtime.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("reason=plugin source changed"),
|
||||
);
|
||||
});
|
||||
|
||||
it("passes daemon TLS state to dashboard link rendering", () => {
|
||||
printDaemonStatus(
|
||||
{
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "../../daemon/systemd-hints.js";
|
||||
import { classifySystemdUnavailableDetail } from "../../daemon/systemd-unavailable.js";
|
||||
import { resolveControlUiLinks } from "../../gateway/control-ui-links.js";
|
||||
import { formatGatewayRestartHandoffDiagnostic } from "../../infra/restart-handoff.js";
|
||||
import { isWSLEnv } from "../../infra/wsl.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { colorize } from "../../terminal/theme.js";
|
||||
@@ -180,6 +181,9 @@ export function printDaemonStatus(status: DaemonStatus, opts: { json: boolean })
|
||||
const runtimeColor = resolveRuntimeStatusColor(service.runtime?.status);
|
||||
defaultRuntime.log(`${label("Runtime:")} ${colorize(rich, runtimeColor, runtimeLine)}`);
|
||||
}
|
||||
if (service.restartHandoff) {
|
||||
defaultRuntime.log(infoText(formatGatewayRestartHandoffDiagnostic(service.restartHandoff)));
|
||||
}
|
||||
|
||||
if (rpc && !rpc.ok && service.loaded && service.runtime?.status === "running") {
|
||||
defaultRuntime.log(
|
||||
|
||||
Reference in New Issue
Block a user