Skip to content

Commit dc7505a

Browse files
committed
fix(cli): keep logs follow on live gateway state
1 parent 7fc691a commit dc7505a

4 files changed

Lines changed: 86 additions & 26 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ Docs: https://docs.openclaw.ai
6363
- Codex app-server: keep native web search observations out of mirrored chat transcripts while preserving tool progress telemetry. Fixes #85109. Thanks @ugitmebaby.
6464
- OpenCode Go: strip unsupported Kimi reasoning replay fields before provider requests so repeated `kimi-k2.6` turns do not fail schema validation. Fixes #83812. Thanks @Sleeck.
6565
- Browser/CDP: add a WSL2 portproxy self-loop hint when Chrome DevTools endpoints accept connections but return an empty HTTP reply. Fixes #59209. Thanks @Owlock.
66+
- CLI/logs: read implicit local Gateway logs through the passive backend client path so `openclaw logs --follow` does not register as a paired device, and stop following stale configured-file fallbacks after live RPC failures. Fixes #83656 and #66841.
6667
- Agents/OpenAI: preserve structured provider error code, type, and redacted body metadata on boundary-aware transport failures.
6768
- Doctor/Codex: point native Codex asset warnings at the canonical `openclaw migrate plan codex` preview command. Fixes #84948. Thanks @markoa.
6869
- CLI/models: make `capability model auth logout --agent` remove auth profiles from the selected non-default agent store. Fixes #85092. Thanks @islandpreneur007.

docs/cli/logs.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ openclaw logs --url ws://127.0.0.1:18789 --token "$OPENCLAW_GATEWAY_TOKEN"
5757

5858
- Use `--local-time` to render timestamps in your local timezone.
5959
- 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.
60+
- `openclaw logs --follow` does not fall back to configured files after Gateway RPC failures, because following a stale side-by-side log can hide the active Gateway's real state.
6061
- When using `--follow`, transient gateway disconnects (WebSocket close, timeout, connection drop) trigger automatic reconnection with exponential backoff (up to 8 retries, capped at 30 s between attempts). A warning is printed to stderr on each retry, and a `[logs] gateway reconnected` notice is printed once a poll succeeds. In `--json` mode both the retry warning and the reconnect transition are emitted as `{"type":"notice"}` records on stderr. Non-recoverable errors (auth failure, bad configuration) still exit immediately.
6162

6263
## Related

src/cli/logs-cli.test.ts

Lines changed: 59 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,47 @@ describe("logs cli", () => {
132132
expect(stderrWrites.join("")).toContain("Log cursor reset");
133133
});
134134

135+
it("uses the passive local Gateway client for implicit loopback log reads", async () => {
136+
callGatewayFromCli.mockResolvedValueOnce({
137+
file: "/tmp/openclaw.log",
138+
lines: ["raw line"],
139+
});
140+
141+
captureStdoutWrites();
142+
143+
await runLogsCli(["logs"]);
144+
145+
expect(callGatewayFromCli).toHaveBeenCalledWith(
146+
"logs.tail",
147+
expect.any(Object),
148+
{ cursor: undefined, limit: 200, maxBytes: 250_000 },
149+
{
150+
progress: true,
151+
clientName: "gateway-client",
152+
mode: "backend",
153+
deviceIdentity: null,
154+
},
155+
);
156+
});
157+
158+
it("keeps explicit Gateway URLs on the normal CLI client identity", async () => {
159+
callGatewayFromCli.mockResolvedValueOnce({
160+
file: "/tmp/openclaw.log",
161+
lines: ["raw line"],
162+
});
163+
164+
captureStdoutWrites();
165+
166+
await runLogsCli(["logs", "--url", "ws://127.0.0.1:18789"]);
167+
168+
expect(callGatewayFromCli).toHaveBeenCalledWith(
169+
"logs.tail",
170+
expect.any(Object),
171+
{ cursor: undefined, limit: 200, maxBytes: 250_000 },
172+
{ progress: true },
173+
);
174+
});
175+
135176
it("wires --local-time through CLI parsing and emits local timestamps", async () => {
136177
callGatewayFromCli.mockResolvedValueOnce({
137178
file: "/tmp/openclaw.log",
@@ -278,39 +319,32 @@ describe("logs cli", () => {
278319
});
279320

280321
describe("--follow retry behavior", () => {
281-
it("uses local fallback (not retry warning) for loopback close errors in --follow mode", async () => {
282-
// Loopback close errors are absorbed by shouldUseLocalLogsFallback inside fetchLogs —
283-
// they never reach the retry path, so no "gateway disconnected" warning is emitted.
284-
callGatewayFromCli.mockRejectedValueOnce(
285-
new GatewayTransportError({
286-
kind: "closed",
287-
code: 1006,
288-
reason: "abnormal closure",
289-
connectionDetails: {
290-
url: "ws://127.0.0.1:18789",
291-
urlSource: "local loopback",
292-
message: "",
293-
},
294-
message: "gateway closed (1006 abnormal closure): abnormal closure",
295-
}),
296-
);
297-
readConfiguredLogTail.mockResolvedValueOnce({
298-
file: "/tmp/openclaw.log",
299-
cursor: 5,
300-
lines: ["local fallback line"],
301-
truncated: false,
302-
reset: false,
322+
it("retries loopback close errors in --follow mode instead of tailing fallback files", async () => {
323+
const closeError = new GatewayTransportError({
324+
kind: "closed",
325+
code: 1006,
326+
reason: "abnormal closure",
327+
connectionDetails: {
328+
url: "ws://127.0.0.1:18789",
329+
urlSource: "local loopback",
330+
message: "",
331+
},
332+
message: "gateway closed (1006 abnormal closure): abnormal closure",
303333
});
334+
for (let i = 0; i <= 8; i += 1) {
335+
callGatewayFromCli.mockRejectedValueOnce(closeError);
336+
}
304337

305338
const stderrWrites = captureStderrWrites();
306339
const stdoutWrites = captureStdoutWrites();
307340
const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => undefined as never);
308341

309342
await runLogsCli(["logs", "--follow", "--interval", "1"]);
310343

311-
expect(stderrWrites.join("")).toContain("Local Gateway RPC unavailable");
312-
expect(stderrWrites.join("")).not.toContain("gateway disconnected");
313-
expect(stdoutWrites.join("")).toContain("local fallback line");
344+
expect(readConfiguredLogTail).not.toHaveBeenCalled();
345+
expect((stderrWrites.join("").match(/gateway disconnected/g) ?? []).length).toBe(8);
346+
expect(stderrWrites.join("")).toContain("Gateway not reachable");
347+
expect(stdoutWrites.join("")).not.toContain("local fallback line");
314348
expect(exitSpy).toHaveBeenCalledWith(1);
315349
});
316350

src/cli/logs-cli.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
type GatewayConnectionDetails,
77
} from "../gateway/call.js";
88
import { isLoopbackHost } from "../gateway/net.js";
9+
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../gateway/protocol/client-info.js";
910
import { readConnectPairingRequiredMessage } from "../gateway/protocol/connect-error-details.js";
1011
import { computeBackoff } from "../infra/backoff.js";
1112
import { formatErrorMessage } from "../infra/errors.js";
@@ -78,7 +79,7 @@ async function fetchLogs(
7879
"logs.tail",
7980
opts,
8081
{ cursor, limit, maxBytes },
81-
{ progress: showProgress },
82+
buildLogsTailGatewayExtra(opts, showProgress),
8283
);
8384
if (!payload || typeof payload !== "object") {
8485
throw new Error("Unexpected logs.tail response");
@@ -104,6 +105,9 @@ function normalizeErrorMessage(error: unknown): string {
104105
}
105106

106107
function shouldUseLocalLogsFallback(opts: LogsCliOptions, error: unknown): boolean {
108+
if (opts.follow) {
109+
return false;
110+
}
107111
if (!isLocalGatewayRpcUnavailableError(error)) {
108112
return false;
109113
}
@@ -116,6 +120,26 @@ function shouldUseLocalLogsFallback(opts: LogsCliOptions, error: unknown): boole
116120
return isImplicitLoopbackGatewayConnection(connection);
117121
}
118122

123+
function buildLogsTailGatewayExtra(opts: LogsCliOptions, showProgress: boolean) {
124+
const base = { progress: showProgress };
125+
if (!shouldUsePassiveLocalLogsClient(opts)) {
126+
return base;
127+
}
128+
return {
129+
...base,
130+
clientName: GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT,
131+
mode: GATEWAY_CLIENT_MODES.BACKEND,
132+
deviceIdentity: null,
133+
};
134+
}
135+
136+
function shouldUsePassiveLocalLogsClient(opts: LogsCliOptions): boolean {
137+
if (typeof opts.url === "string" && opts.url.trim().length > 0) {
138+
return false;
139+
}
140+
return isImplicitLoopbackGatewayConnection(buildGatewayConnectionDetails());
141+
}
142+
119143
function isImplicitLoopbackGatewayConnection(connection: GatewayConnectionDetails): boolean {
120144
if (connection.urlSource !== "local loopback") {
121145
return false;

0 commit comments

Comments
 (0)