From e33e2abc88196ed5fa75197aa1199cd7dc8964dd Mon Sep 17 00:00:00 2001 From: Liam Custer Date: Thu, 5 Nov 2026 13:22:46 -0800 Subject: [PATCH] fix(coding-agent): report the real size of an oversized last line in the bash truncation notice fixes #10015 --- .../src/core/tools/output-accumulator.ts | 10 ++++- .../test/output-accumulator-last-line.test.ts | 41 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 packages/coding-agent/test/output-accumulator-last-line.test.ts diff --git a/packages/coding-agent/src/core/tools/output-accumulator.ts b/packages/coding-agent/src/core/tools/output-accumulator.ts index a2868a848..6b74335c9 100644 --- a/packages/coding-agent/src/core/tools/output-accumulator.ts +++ b/packages/coding-agent/src/core/tools/output-accumulator.ts @@ -48,6 +48,7 @@ export class OutputAccumulator { private completedLines = 0; private totalLines = 0; private currentLineBytes = 0; + private lastCompletedLineBytes = 0; private hasOpenLine = false; private finished = false; @@ -175,8 +176,9 @@ export class OutputAccumulator { } } + /** Bytes of the last line: the open one, or the one the last newline closed. */ getLastLineBytes(): number { - return this.currentLineBytes; + return this.hasOpenLine ? this.currentLineBytes : this.lastCompletedLineBytes; } private appendDecodedText(text: string): void { @@ -194,8 +196,10 @@ export class OutputAccumulator { let newlines = 0; let lastNewline = -1; + let previousNewline = -1; for (let i = text.indexOf("\n"); i !== -1; i = text.indexOf("\n", i + 1)) { newlines++; + previousNewline = lastNewline; lastNewline = i; } if (newlines === 0) { @@ -203,6 +207,10 @@ export class OutputAccumulator { this.hasOpenLine = true; } else { this.completedLines += newlines; + // The line the last newline closed began in an earlier chunk when no newline precedes it here. + this.lastCompletedLineBytes = + (previousNewline === -1 ? this.currentLineBytes : 0) + + byteLength(text.slice(previousNewline + 1, lastNewline)); const tail = text.slice(lastNewline + 1); this.currentLineBytes = byteLength(tail); this.hasOpenLine = tail.length > 0; diff --git a/packages/coding-agent/test/output-accumulator-last-line.test.ts b/packages/coding-agent/test/output-accumulator-last-line.test.ts new file mode 100644 index 000000000..92652d734 --- /dev/null +++ b/packages/coding-agent/test/output-accumulator-last-line.test.ts @@ -0,0 +1,41 @@ +import { rm } from "node:fs/promises"; +import { afterEach, describe, expect, it } from "vitest"; +import { OutputAccumulator } from "../src/core/tools/output-accumulator.ts"; + +describe("OutputAccumulator last line size (#10015)", () => { + const tempFiles: string[] = []; + + afterEach(async () => { + await Promise.all(tempFiles.splice(0).map((path) => rm(path, { force: true }))); + }); + + async function sizeOf(chunks: string[]): Promise<{ lastLineBytes: number; lastLinePartial: boolean }> { + const output = new OutputAccumulator({ maxBytes: 1024 }); + for (const chunk of chunks) output.append(Buffer.from(chunk)); + output.finish(); + const snapshot = output.snapshot(); + await output.closeTempFile(); + if (snapshot.fullOutputPath) tempFiles.push(snapshot.fullOutputPath); + return { lastLineBytes: output.getLastLineBytes(), lastLinePartial: snapshot.truncation.lastLinePartial }; + } + + // #10015: a trailing newline reset the live line counter, so the notice reported the line as 0B. + it("reports the size of an oversized last line that ends with a newline", async () => { + expect(await sizeOf([`${"a".repeat(2000)}\n`])).toEqual({ lastLineBytes: 2000, lastLinePartial: true }); + }); + + it("reports the size of an oversized last line that is still open", async () => { + expect(await sizeOf(["a".repeat(2000)])).toEqual({ lastLineBytes: 2000, lastLinePartial: true }); + }); + + it("sums a line split across chunks", async () => { + expect(await sizeOf(["x\n", "a".repeat(1500), `${"a".repeat(500)}\n`])).toEqual({ + lastLineBytes: 2000, + lastLinePartial: true, + }); + }); + + it("measures only the last line when a chunk holds several", async () => { + expect(await sizeOf([`short\n${"a".repeat(2000)}\n`])).toEqual({ lastLineBytes: 2000, lastLinePartial: true }); + }); +});