Skip to content

Commit b7ba7c3

Browse files
BSG2000Copilotclawsweeper[bot]Takhoffman
authored
fix(cli): preserve first line of channels logs at window boundary (#84106)
Summary: - The PR updates `openclaw channels logs` tail-window reading to keep a complete first line when the 1 MB window starts on a newline boundary, adds a regression test, and adds a changelog entry. - Reproducibility: yes. Source inspection on current main shows the unconditional first-line drop, and the PR ... s provide terminal before/after CLI output for a 2 MB log whose tail window starts exactly after a newline. Automerge notes: - PR branch already contained follow-up commit before automerge: Merge remote-tracking branch 'origin/main' into fix/channels-logs-dro… - PR branch already contained follow-up commit before automerge: fix(cli): preserve first line of channels logs at window boundary Validation: - ClawSweeper review passed for head 284b312. - Required merge gates passed before the squash merge. Prepared head SHA: 284b312 Review: #84106 (comment) Co-authored-by: BSG2000 <bsg2000@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: takhoffman Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
1 parent f07c874 commit b7ba7c3

3 files changed

Lines changed: 67 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai
1010

1111
### Fixes
1212

13+
- CLI/channels: preserve the first line of `openclaw channels logs` output when the rolling tail window starts exactly on a line boundary, mirroring the already-fixed `readLogSlice` behavior in `src/logging/log-tail.ts`.
1314
- Control UI: treat terminal session status as authoritative over stale active-run flags so completed terminal runs stop showing abort/live UI. (#84057)
1415
- CLI: preserve embedded equals signs in inline root option values instead of truncating after the second separator. (#83995) Thanks @ThiagoCAltoe.
1516
- Matrix/config: accept `messages.queue.byChannel.matrix` queue overrides and keep queue provider schema/type keys aligned for Matrix, Google Chat, and Mattermost. Thanks @bdjben.

src/commands/channels.logs.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,65 @@ describe("channelsLogsCommand", () => {
141141
expect(payload.lines.map((line) => line.message)).toEqual(["current sent"]);
142142
});
143143

144+
it("returns the first line of the tail window when start aligns with a line boundary", async () => {
145+
// MAX_BYTES in readTailLines is 1_000_000. We build a file of 2_000_000 bytes
146+
// made of 10_000 lines each exactly 200 bytes (199 payload + "\n"), so the
147+
// read window starts at byte offset 1_000_000 which is exactly on a line
148+
// boundary (byte 999_999 is the trailing "\n" of the previous line).
149+
// Without checking the byte before the window, readTailLines drops line 5000 silently.
150+
const LINE_SIZE = 200;
151+
const TOTAL_LINES = 10_000;
152+
const FIRST_INDEX = 5000; // first line of the tail window after alignment
153+
154+
const buildLine = (message: string) => {
155+
const base = logLine({
156+
module: "gateway/channels/slack/send",
157+
message,
158+
});
159+
const payloadLen = LINE_SIZE - 1; // reserve 1 byte for newline
160+
// Re-emit with a padded message so total byte length is constant.
161+
const padNeeded = payloadLen - Buffer.byteLength(base);
162+
if (padNeeded < 0) {
163+
throw new Error(`base log line too long: ${Buffer.byteLength(base)} > ${payloadLen}`);
164+
}
165+
const padded = logLine({
166+
module: "gateway/channels/slack/send",
167+
message: message + " ".repeat(padNeeded),
168+
});
169+
if (Buffer.byteLength(padded) !== payloadLen) {
170+
throw new Error(`padded line wrong size: ${Buffer.byteLength(padded)} vs ${payloadLen}`);
171+
}
172+
return padded + "\n";
173+
};
174+
175+
const handle = await fs.open(logPath, "w");
176+
try {
177+
for (let i = 0; i < TOTAL_LINES; i++) {
178+
let message: string;
179+
if (i === FIRST_INDEX) {
180+
message = "first-line-in-window";
181+
} else if (i === TOTAL_LINES - 1) {
182+
message = "last-line";
183+
} else {
184+
message = "filler";
185+
}
186+
await handle.write(buildLine(message));
187+
}
188+
} finally {
189+
await handle.close();
190+
}
191+
192+
await channelsLogsCommand(
193+
{ channel: "slack", json: true, lines: String(TOTAL_LINES) },
194+
runtime,
195+
);
196+
197+
const payload = readJsonPayload();
198+
const messages = payload.lines.map((line) => line.message.trimEnd());
199+
expect(messages[0]).toBe("first-line-in-window");
200+
expect(messages[messages.length - 1]).toBe("last-line");
201+
});
202+
144203
it("does not fall back to rolling logs for a missing custom log file", async () => {
145204
const configuredFile = path.join(tempDir, "custom-channel.log");
146205
const fallbackFile = path.join(tempDir, "openclaw-2026-04-25.log");

src/commands/channels/logs.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ async function readTailLines(file: string, limit: number): Promise<string[]> {
6363
const start = Math.max(0, size - MAX_BYTES);
6464
const handle = await fs.open(file, "r");
6565
try {
66+
let prefix = "";
67+
if (start > 0) {
68+
const prefixBuf = Buffer.alloc(1);
69+
const prefixRead = await handle.read(prefixBuf, 0, 1, start - 1);
70+
prefix = prefixBuf.toString("utf8", 0, prefixRead.bytesRead);
71+
}
6672
const length = Math.max(0, size - start);
6773
if (length === 0) {
6874
return [];
@@ -71,7 +77,7 @@ async function readTailLines(file: string, limit: number): Promise<string[]> {
7177
const readResult = await handle.read(buffer, 0, length, start);
7278
const text = buffer.toString("utf8", 0, readResult.bytesRead);
7379
let lines = text.split("\n");
74-
if (start > 0) {
80+
if (start > 0 && prefix !== "\n") {
7581
lines = lines.slice(1);
7682
}
7783
if (lines.length && lines[lines.length - 1] === "") {

0 commit comments

Comments
 (0)