diff --git a/CHANGELOG.md b/CHANGELOG.md index ebbf913be4b..4962e9ebaf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai - Plugins/QA: prebuild the private QA channel runtime before plugin gauntlet source runs so wrapper CPU/RSS measurements are not polluted by private QA dist rebuild work. Thanks @vincentkoc. - Gateway/reload: bound default restart deferral and SIGUSR1 restart drain to five minutes while preserving explicit `deferralTimeoutMs: 0` indefinite waits, so stale active work accounting cannot block config reloads forever. Thanks @vincentkoc. - Active Memory: register the prompt-build hook with the configured recall timeout plus setup grace instead of the 150s maximum budget, so default memory recall cannot delay turn startup for multiple minutes. Thanks @vincentkoc. +- 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. - Security/audit: recognize dangerous node command IDs as valid `gateway.nodes.denyCommands` entries, so audit only warns on real typos or unsupported patterns. (#56923) Thanks @chziyue. - Telegram/exec approvals: stop treating general Telegram chat allowlists and `defaultTo` routes as native exec approvers; Telegram now uses explicit `execApprovals.approvers` or owner identity from `commands.ownerAllowFrom`, matching the first-pairing owner bootstrap path. Thanks @pashpashpash. - Chat commands: route sensitive group `/diagnostics` and `/export-trajectory` approvals and results to a private owner route, preferring same-surface DMs before falling back to the first configured owner route, so Discord group invocations can land in Telegram when that is the primary owner interface. Thanks @pashpashpash. diff --git a/src/commands/channels.logs.test.ts b/src/commands/channels.logs.test.ts index 59f67698ce9..9aaef2888f9 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,78 @@ 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"); + const staleFile = path.join(tempDir, "openclaw-2026-04-24.log"); + setLoggerOverride({ file: configuredFile }); + await fs.writeFile( + fallbackFile, + [ + logLine({ module: "gateway/channels/slack/send", message: "slack fallback" }), + logLine({ module: "gateway/channels/external-chat/send", message: "fallback sent" }), + ].join("\n"), + ); + await fs.writeFile( + staleFile, + logLine({ module: "gateway/channels/external-chat/send", message: "stale sent" }), + ); + await fs.utimes( + staleFile, + new Date("2026-04-24T12:00:00.000Z"), + new Date("2026-04-24T12:00:00.000Z"), + ); + await fs.utimes( + fallbackFile, + new Date("2026-04-25T12:00:00.000Z"), + new Date("2026-04-25T12:00:00.000Z"), + ); + + 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;