From fba6b88e8644365360f82802cbe25039a091409d Mon Sep 17 00:00:00 2001 From: ethanclaw Date: Wed, 11 Mar 2026 14:42:23 +0800 Subject: [PATCH] fix(logs): find active log file across date boundaries Fixes #42875 When gateway runs across midnight, openclaw channels logs was looking for today's log file instead of the active one. This change makes the CLI find the most recently modified log file as a fallback. --- CHANGELOG.md | 1 + src/commands/channels.logs.test.ts | 65 ++++++++++++++++++++++++++++-- src/commands/channels/logs.ts | 3 +- src/logging/log-tail.ts | 2 +- 4 files changed, 65 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b77aa7137f..97d62cf0919 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- CLI/channels logs: reuse the rolling log-file resolver so `openclaw channels logs` falls back to the active dated log across date boundaries without reading unrelated custom log files. Fixes #42875; carries forward #42904 and #43043. Thanks @ethanclaw and @wdskuki. - NVIDIA/NIM: persist the `NVIDIA_API_KEY` provider marker and mark bundled NVIDIA Chat Completions models as string-content compatible, so NIM models load from `models.json` and OpenAI-compatible subagent calls send plain text content. Fixes #73013 and #50107; refs #73014. Thanks @bautrey, @iot2edge, @ifearghal, and @futhgar. - Channels/Discord: let text-only configs drop the `GuildVoiceStates` gateway intent and expose a bounded `/gateway/bot` metadata timeout with rate-limited fallback logs, reducing idle CPU and warning floods. Fixes #73709 and #73585. Thanks @sanchezm86 and @trac3r00. - CLI/plugins: use plugin metadata snapshots for install slot selection and add opt-in plugin lifecycle timing traces, so plugin install avoids runtime-loading the plugin registry for metadata-only decisions. Thanks @shakkernerd. diff --git a/src/commands/channels.logs.test.ts b/src/commands/channels.logs.test.ts index 59f67698ce9..06445ed60ca 100644 --- a/src/commands/channels.logs.test.ts +++ b/src/commands/channels.logs.test.ts @@ -37,6 +37,14 @@ function logLine(params: { module: string; message: string }) { }); } +function readJsonPayload() { + return JSON.parse(String(runtime.log.mock.calls[0]?.[0])) as { + file: string; + channel: string; + lines: Array<{ message: string }>; + }; +} + describe("channelsLogsCommand", () => { let tempDir: string; let logPath: string; @@ -75,11 +83,60 @@ describe("channelsLogsCommand", () => { includeDisabled: true, }), ); - const payload = JSON.parse(String(runtime.log.mock.calls[0]?.[0])) as { - channel: string; - lines: Array<{ message: string }>; - }; + const payload = readJsonPayload(); expect(payload.channel).toBe("external-chat"); expect(payload.lines.map((line) => line.message)).toEqual(["external sent"]); }); + + it("falls back to the latest rolling log when the configured rolling file is missing", async () => { + const configuredFile = path.join(tempDir, "openclaw-2026-04-26.log"); + const fallbackFile = path.join(tempDir, "openclaw-2026-04-25.log"); + setLoggerOverride({ file: configuredFile }); + await fs.writeFile( + fallbackFile, + logLine({ module: "gateway/channels/external-chat/send", message: "fallback sent" }), + ); + + await channelsLogsCommand({ channel: "external-chat", json: true }, runtime); + + const payload = readJsonPayload(); + expect(payload.file).toBe(fallbackFile); + expect(payload.lines.map((line) => line.message)).toEqual(["fallback sent"]); + }); + + it("prefers the configured rolling log when it exists", async () => { + const configuredFile = path.join(tempDir, "openclaw-2026-04-26.log"); + const fallbackFile = path.join(tempDir, "openclaw-2026-04-25.log"); + setLoggerOverride({ file: configuredFile }); + await fs.writeFile( + fallbackFile, + logLine({ module: "gateway/channels/external-chat/send", message: "fallback sent" }), + ); + await fs.writeFile( + configuredFile, + logLine({ module: "gateway/channels/external-chat/send", message: "current sent" }), + ); + + await channelsLogsCommand({ channel: "external-chat", json: true }, runtime); + + const payload = readJsonPayload(); + expect(payload.file).toBe(configuredFile); + expect(payload.lines.map((line) => line.message)).toEqual(["current sent"]); + }); + + it("does not fall back to rolling logs for a missing custom log file", async () => { + const configuredFile = path.join(tempDir, "custom-channel.log"); + const fallbackFile = path.join(tempDir, "openclaw-2026-04-25.log"); + setLoggerOverride({ file: configuredFile }); + await fs.writeFile( + fallbackFile, + logLine({ module: "gateway/channels/external-chat/send", message: "fallback sent" }), + ); + + await channelsLogsCommand({ channel: "external-chat", json: true }, runtime); + + const payload = readJsonPayload(); + expect(payload.file).toBe(configuredFile); + expect(payload.lines).toEqual([]); + }); }); diff --git a/src/commands/channels/logs.ts b/src/commands/channels/logs.ts index 6c60c703309..38fdcfa038e 100644 --- a/src/commands/channels/logs.ts +++ b/src/commands/channels/logs.ts @@ -1,6 +1,7 @@ import fs from "node:fs/promises"; import { normalizeChannelId as normalizeBundledChannelId } from "../../channels/registry.js"; import { getResolvedLoggerSettings } from "../../logging.js"; +import { resolveLogFile } from "../../logging/log-tail.js"; import { parseLogLine } from "../../logging/parse-log-line.js"; import { listPluginContributionIds, @@ -103,7 +104,7 @@ export async function channelsLogsCommand( ? Math.floor(limitRaw) : DEFAULT_LIMIT; - const file = getResolvedLoggerSettings().file; + const file = await resolveLogFile(getResolvedLoggerSettings().file); const rawLines = await readTailLines(file, limit * 4); const parsed = rawLines .map(parseLogLine) diff --git a/src/logging/log-tail.ts b/src/logging/log-tail.ts index 52415212e2c..9f4287ae108 100644 --- a/src/logging/log-tail.ts +++ b/src/logging/log-tail.ts @@ -23,7 +23,7 @@ function isRollingLogFile(file: string): boolean { return ROLLING_LOG_RE.test(path.basename(file)); } -async function resolveLogFile(file: string): Promise { +export async function resolveLogFile(file: string): Promise { const stat = await fs.stat(file).catch(() => null); if (stat) { return file;