fix(cli): fall back to file logs when local logs rpc closes

This commit is contained in:
Peter Steinberger
2026-04-29 07:45:09 +01:00
parent 6306e2fdcb
commit e25b542100
5 changed files with 61 additions and 6 deletions

View File

@@ -15,6 +15,8 @@ Docs: https://docs.openclaw.ai
### Fixes
- MCP/plugins: stringify non-array plugin tool results with chat-content coercion instead of default object stringification, so MCP callers receive useful JSON/text content from plugin tools. Thanks @vincentkoc.
- CLI/logs: fall back to the configured Gateway file log when implicit loopback Gateway connections close or time out before or during `logs.tail`, so `openclaw logs` still works while diagnosing local-model Gateway disconnects. Refs #74078. Thanks @sakalaboator.
- MCP/plugins: stringify non-array plugin tool results with chat-content coercion instead of default object stringification, so MCP callers receive useful JSON/text content from plugin tools. Thanks @vincentkoc.
- Channels/Discord: remove Discord-owned queued-run timeout replies through the shared channel lifecycle queue while preserving message ordering and compatibility timeout constants, so long Discord turns stay governed by session/tool/runtime lifecycle instead of channel fallback errors. Thanks @codexGW.
- Agents/tools: clamp `process.poll` waits to 30 seconds, advertise that cap in the tool schema, and honor abort signals while waiting, so long command polls cannot pin agent responsiveness after cancellation. Thanks @vincentkoc.

View File

@@ -56,7 +56,7 @@ openclaw logs --url ws://127.0.0.1:18789 --token "$OPENCLAW_GATEWAY_TOKEN"
## Notes
- Use `--local-time` to render timestamps in your local timezone.
- If the local loopback Gateway asks for pairing, `openclaw logs` falls back to the configured local log file automatically. Explicit `--url` targets do not use this fallback.
- If the implicit local loopback Gateway asks for pairing, closes during connect, or times out before `logs.tail` answers, `openclaw logs` falls back to the configured Gateway file log automatically. Explicit `--url` targets do not use this fallback.
## Related

View File

@@ -73,9 +73,10 @@ In JSON mode, the CLI emits `type`-tagged objects:
- `notice`: truncation / rotation hints
- `raw`: unparsed log line
If the local loopback Gateway asks for pairing, `openclaw logs` falls back to
the configured local log file automatically. Explicit `--url` targets do not
use this fallback.
If the implicit local loopback Gateway asks for pairing, closes during connect,
or times out before `logs.tail` answers, `openclaw logs` falls back to the
configured Gateway file log automatically. Explicit `--url` targets do not use
this fallback.
If the Gateway is unreachable, the CLI prints a short hint to run:

View File

@@ -181,6 +181,46 @@ describe("logs cli", () => {
expect(stderrWrites.join("")).toContain("reading local log file instead");
});
it("falls back to the configured Gateway file log on loopback gateway close errors", async () => {
callGatewayFromCli.mockRejectedValueOnce(
new Error("gateway closed (1000 normal closure): no close reason"),
);
readConfiguredLogTail.mockResolvedValueOnce({
file: "/tmp/openclaw.log",
cursor: 5,
size: 5,
lines: ["local fallback line"],
truncated: false,
reset: false,
});
const stdoutWrites = captureStdoutWrites();
const stderrWrites = captureStderrWrites();
await runLogsCli(["logs"]);
expect(readConfiguredLogTail).toHaveBeenCalledTimes(1);
expect(stdoutWrites.join("")).toContain("local fallback line");
expect(stderrWrites.join("")).toContain("Local Gateway RPC unavailable");
});
it("does not use local fallback for explicit Gateway URLs", async () => {
callGatewayFromCli.mockRejectedValueOnce(
new Error("gateway closed (1000 normal closure): no close reason"),
);
const stdoutWrites = captureStdoutWrites();
const stderrWrites = captureStderrWrites();
const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => undefined as never);
await runLogsCli(["logs", "--url", "ws://127.0.0.1:18789"]);
expect(readConfiguredLogTail).not.toHaveBeenCalled();
expect(stdoutWrites.join("")).not.toContain("local fallback line");
expect(stderrWrites.join("")).toContain("Gateway not reachable");
expect(exitSpy).toHaveBeenCalledWith(1);
});
describe("formatLogTimestamp", () => {
it("formats UTC timestamp in plain mode by default", () => {
const result = formatLogTimestamp("2025-01-01T12:00:00.000Z");

View File

@@ -49,7 +49,7 @@ type LogsCliOptions = {
expectFinal?: boolean;
};
const LOCAL_FALLBACK_NOTICE = "Gateway pairing required; reading local log file instead.";
const LOCAL_FALLBACK_NOTICE = "Local Gateway RPC unavailable; reading configured file log instead.";
function parsePositiveInt(value: string | undefined, fallback: number): number {
if (!value) {
@@ -81,6 +81,7 @@ async function fetchLogs(
if (!shouldUseLocalLogsFallback(opts, error)) {
throw error;
}
// Match the Gateway logs.tail source when implicit local RPC is unavailable.
return {
...(await readConfiguredLogTail({ cursor, limit, maxBytes })),
localFallback: true,
@@ -97,7 +98,7 @@ function normalizeErrorMessage(error: unknown): string {
function shouldUseLocalLogsFallback(opts: LogsCliOptions, error: unknown): boolean {
const message = normalizeLowercaseStringOrEmpty(normalizeErrorMessage(error));
if (!readConnectPairingRequiredMessage(message)) {
if (!isLocalGatewayRpcUnavailableError(message)) {
return false;
}
if (typeof opts.url === "string" && opts.url.trim().length > 0) {
@@ -114,6 +115,17 @@ function shouldUseLocalLogsFallback(opts: LogsCliOptions, error: unknown): boole
}
}
function isLocalGatewayRpcUnavailableError(message: string): boolean {
if (readConnectPairingRequiredMessage(message)) {
return true;
}
return (
message.includes("gateway closed (") ||
message.includes("gateway timeout after") ||
message.includes("gateway connect failed:")
);
}
export function formatLogTimestamp(
value?: string,
mode: "pretty" | "plain" = "plain",