diff --git a/src/cli/logs-cli.test.ts b/src/cli/logs-cli.test.ts index 9405bcda459c..b7c68035d42d 100644 --- a/src/cli/logs-cli.test.ts +++ b/src/cli/logs-cli.test.ts @@ -208,7 +208,9 @@ describe("logs cli", () => { expect(stdoutWrites.join("")).toContain("Log file:"); expect(stdoutWrites.join("")).toContain("raw line"); - expect(stderrWrites.join("")).toContain("Log tail truncated"); + expect(stderrWrites.join("")).toContain( + "Log tail truncated (increase --limit or --max-bytes).", + ); expect(stderrWrites.join("")).toContain("Log cursor reset"); }); diff --git a/src/cli/logs-cli.ts b/src/cli/logs-cli.ts index 493e64a84fa4..e9b09ef3fe97 100644 --- a/src/cli/logs-cli.ts +++ b/src/cli/logs-cli.ts @@ -725,7 +725,7 @@ export function registerLogsCli(program: Command) { if ( !emitJsonLine({ type: "notice", - message: "Log tail truncated (increase --max-bytes).", + message: "Log tail truncated (increase --limit or --max-bytes).", }) ) { return; @@ -785,7 +785,7 @@ export function registerLogsCli(program: Command) { } } if (payload.truncated) { - if (!errorLine("Log tail truncated (increase --max-bytes).")) { + if (!errorLine("Log tail truncated (increase --limit or --max-bytes).")) { return; } } diff --git a/src/logging/log-tail.test.ts b/src/logging/log-tail.test.ts index 1d1a36610404..4c88e64aadce 100644 --- a/src/logging/log-tail.test.ts +++ b/src/logging/log-tail.test.ts @@ -85,6 +85,26 @@ describe("readConfiguredLogTail", () => { expect(result.lines).toEqual(["old line", "recent one", "recent two"]); }); + it("reports truncation when the line limit omits complete records", async () => { + const { readConfiguredLogTail } = await import("./log-tail.js"); + const tempDir = tempDirs.make("openclaw-log-tail-"); + const file = path.join(tempDir, "openclaw-2026-01-22.log"); + const content = "one\ntwo\nthree\n"; + + await fs.writeFile(file, content); + setLoggerOverride({ file }); + + const result = await readConfiguredLogTail({ limit: 2, maxBytes: 100 }); + + expect(result).toMatchObject({ + lines: ["two", "three"], + cursor: Buffer.byteLength(content), + size: Buffer.byteLength(content), + truncated: true, + reset: false, + }); + }); + it("falls back only within the active profile's rolling log family", async () => { const tempDir = tempDirs.make("openclaw-log-tail-"); const missing = path.join(tempDir, "openclaw-dev-2026-01-22.log"); diff --git a/src/logging/log-tail.ts b/src/logging/log-tail.ts index b53536b48a45..73c574487b7a 100644 --- a/src/logging/log-tail.ts +++ b/src/logging/log-tail.ts @@ -138,6 +138,7 @@ async function readLogSlice(params: { lines = lines.slice(0, -1); } if (lines.length > limit) { + truncated = true; lines = lines.slice(lines.length - limit); } diff --git a/test/e2e/qa-lab/runtime/remote-log-tailing-runtime.ts b/test/e2e/qa-lab/runtime/remote-log-tailing-runtime.ts index d05fe21d697f..97a9e6d12c0e 100644 --- a/test/e2e/qa-lab/runtime/remote-log-tailing-runtime.ts +++ b/test/e2e/qa-lab/runtime/remote-log-tailing-runtime.ts @@ -100,7 +100,11 @@ export async function runRemoteLogTailing(repoRoot: string, outputRoot: string) lines: string[]; truncated: boolean; }; - if (first.lines.length !== 2 || !first.lines.some((line) => line.includes("qa-line-three"))) { + if ( + first.lines.length !== 2 || + !first.lines.some((line) => line.includes("qa-line-three")) || + !first.truncated + ) { throw new Error(`logs.tail did not honor limit: ${JSON.stringify(first)}`); } const bounded = (await gateway.call("logs.tail", { limit: 20, maxBytes: 96 })) as {