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;