@caupulican/pi-adaptative 0.80.28 → 0.80.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,45 +8,66 @@ export interface OutputSnapshot {
8
8
  content: string;
9
9
  truncation: TruncationResult;
10
10
  fullOutputPath?: string;
11
+ fullOutputError?: string;
12
+ }
13
+ export interface OutputPreview {
14
+ content: string;
15
+ skippedLines: number;
11
16
  }
12
17
  /**
13
18
  * Incrementally tracks streaming output with bounded memory.
14
19
  *
15
- * Appends decode chunks with a streaming UTF-8 decoder, keeps only a decoded
16
- * tail for display snapshots, and opens a temp file when the full output needs
17
- * to be preserved.
20
+ * Appends decode chunks with a streaming UTF-8 decoder, keeps a bounded tail of
21
+ * logical lines, and opens a temp file when the full output needs preserving.
22
+ * Snapshot and preview work is bounded by configured output limits, never by
23
+ * total command history.
18
24
  */
19
25
  export declare class OutputAccumulator {
20
26
  private readonly maxLines;
21
27
  private readonly maxBytes;
22
- private readonly maxRollingBytes;
23
28
  private readonly tempFilePrefix;
24
29
  private readonly decoder;
25
30
  private rawChunks;
26
- private tailText;
27
- private tailBytes;
28
- private tailStartsAtLineBoundary;
31
+ private tailLines;
32
+ private tailLineBytes;
33
+ private tailLineStoredBytes;
34
+ private tailStart;
35
+ private tailStoredBytes;
36
+ private currentLineText;
37
+ private currentLineBytes;
38
+ private currentLineStoredBytes;
39
+ private lastCompletedLineBytes;
29
40
  private totalRawBytes;
30
41
  private totalDecodedBytes;
31
42
  private completedLines;
32
43
  private totalLines;
33
- private currentLineBytes;
34
44
  private hasOpenLine;
35
45
  private finished;
36
46
  private tempFilePath;
37
- private tempFileStream;
47
+ private tempFileFd;
48
+ private tempFileError;
38
49
  constructor(options?: OutputAccumulatorOptions);
39
50
  append(data: Buffer): void;
40
51
  finish(): void;
41
52
  snapshot(options?: {
42
53
  persistIfTruncated?: boolean;
43
54
  }): OutputSnapshot;
55
+ preview(maxLines: number, maxBytes?: number): OutputPreview;
56
+ previewSnapshot(maxLines: number, maxBytes?: number, options?: {
57
+ persistIfFullTruncated?: boolean;
58
+ }): OutputSnapshot;
44
59
  closeTempFile(): Promise<void>;
45
60
  getLastLineBytes(): number;
61
+ private appendBlock;
46
62
  private appendDecodedText;
47
- private trimTail;
48
- private getSnapshotText;
63
+ private appendToCurrentLine;
64
+ private pushCompletedCurrentLine;
65
+ private trimStoredTail;
66
+ private completedTailLineCount;
67
+ private buildSnapshot;
49
68
  private shouldUseTempFile;
50
- private ensureTempFile;
69
+ private fullOutputPath;
70
+ private tryEnsureTempFile;
71
+ private recordTempFileError;
51
72
  }
52
73
  //# sourceMappingURL=output-accumulator.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"output-accumulator.d.ts","sourceRoot":"","sources":["../../../src/core/tools/output-accumulator.ts"],"names":[],"mappings":"AAIA,OAAO,EAAwC,KAAK,gBAAgB,EAAgB,MAAM,eAAe,CAAC;AAE1G,MAAM,WAAW,wBAAwB;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,cAAc;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,gBAAgB,CAAC;IAC7B,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAWD;;;;;;GAMG;AACH,qBAAa,iBAAiB;IAC7B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAE7C,OAAO,CAAC,SAAS,CAAgB;IACjC,OAAO,CAAC,QAAQ,CAAM;IACtB,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,wBAAwB,CAAQ;IACxC,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,QAAQ,CAAS;IAEzB,OAAO,CAAC,YAAY,CAAqB;IACzC,OAAO,CAAC,cAAc,CAA0B;IAEhD,YAAY,OAAO,GAAE,wBAA6B,EAKjD;IAED,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAczB;IAED,MAAM,IAAI,IAAI,CASb;IAED,QAAQ,CAAC,OAAO,GAAE;QAAE,kBAAkB,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,cAAc,CA4BvE;IAEK,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAqBnC;IAED,gBAAgB,IAAI,MAAM,CAEzB;IAED,OAAO,CAAC,iBAAiB;IA+BzB,OAAO,CAAC,QAAQ;IAiBhB,OAAO,CAAC,eAAe;IASvB,OAAO,CAAC,iBAAiB;IAMzB,OAAO,CAAC,cAAc;CAWtB","sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport { createWriteStream, type WriteStream } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, type TruncationResult, truncateTail } from \"./truncate.ts\";\n\nexport interface OutputAccumulatorOptions {\n\tmaxLines?: number;\n\tmaxBytes?: number;\n\ttempFilePrefix?: string;\n}\n\nexport interface OutputSnapshot {\n\tcontent: string;\n\ttruncation: TruncationResult;\n\tfullOutputPath?: string;\n}\n\nfunction defaultTempFilePath(prefix: string): string {\n\tconst id = randomBytes(8).toString(\"hex\");\n\treturn join(tmpdir(), `${prefix}-${id}.log`);\n}\n\nfunction byteLength(text: string): number {\n\treturn Buffer.byteLength(text, \"utf-8\");\n}\n\n/**\n * Incrementally tracks streaming output with bounded memory.\n *\n * Appends decode chunks with a streaming UTF-8 decoder, keeps only a decoded\n * tail for display snapshots, and opens a temp file when the full output needs\n * to be preserved.\n */\nexport class OutputAccumulator {\n\tprivate readonly maxLines: number;\n\tprivate readonly maxBytes: number;\n\tprivate readonly maxRollingBytes: number;\n\tprivate readonly tempFilePrefix: string;\n\tprivate readonly decoder = new TextDecoder();\n\n\tprivate rawChunks: Buffer[] = [];\n\tprivate tailText = \"\";\n\tprivate tailBytes = 0;\n\tprivate tailStartsAtLineBoundary = true;\n\tprivate totalRawBytes = 0;\n\tprivate totalDecodedBytes = 0;\n\tprivate completedLines = 0;\n\tprivate totalLines = 0;\n\tprivate currentLineBytes = 0;\n\tprivate hasOpenLine = false;\n\tprivate finished = false;\n\n\tprivate tempFilePath: string | undefined;\n\tprivate tempFileStream: WriteStream | undefined;\n\n\tconstructor(options: OutputAccumulatorOptions = {}) {\n\t\tthis.maxLines = options.maxLines ?? DEFAULT_MAX_LINES;\n\t\tthis.maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;\n\t\tthis.maxRollingBytes = Math.max(this.maxBytes * 2, 1);\n\t\tthis.tempFilePrefix = options.tempFilePrefix ?? \"pi-output\";\n\t}\n\n\tappend(data: Buffer): void {\n\t\tif (this.finished) {\n\t\t\tthrow new Error(\"Cannot append to a finished output accumulator\");\n\t\t}\n\n\t\tthis.totalRawBytes += data.length;\n\t\tthis.appendDecodedText(this.decoder.decode(data, { stream: true }));\n\n\t\tif (this.tempFileStream || this.shouldUseTempFile()) {\n\t\t\tthis.ensureTempFile();\n\t\t\tthis.tempFileStream?.write(data);\n\t\t} else if (data.length > 0) {\n\t\t\tthis.rawChunks.push(data);\n\t\t}\n\t}\n\n\tfinish(): void {\n\t\tif (this.finished) {\n\t\t\treturn;\n\t\t}\n\t\tthis.finished = true;\n\t\tthis.appendDecodedText(this.decoder.decode());\n\t\tif (this.shouldUseTempFile()) {\n\t\t\tthis.ensureTempFile();\n\t\t}\n\t}\n\n\tsnapshot(options: { persistIfTruncated?: boolean } = {}): OutputSnapshot {\n\t\tconst tailTruncation = truncateTail(this.getSnapshotText(), {\n\t\t\tmaxLines: this.maxLines,\n\t\t\tmaxBytes: this.maxBytes,\n\t\t});\n\t\tconst truncated = this.totalLines > this.maxLines || this.totalDecodedBytes > this.maxBytes;\n\t\tconst truncatedBy = truncated\n\t\t\t? (tailTruncation.truncatedBy ?? (this.totalDecodedBytes > this.maxBytes ? \"bytes\" : \"lines\"))\n\t\t\t: null;\n\t\tconst truncation: TruncationResult = {\n\t\t\t...tailTruncation,\n\t\t\ttruncated,\n\t\t\ttruncatedBy,\n\t\t\ttotalLines: this.totalLines,\n\t\t\ttotalBytes: this.totalDecodedBytes,\n\t\t\tmaxLines: this.maxLines,\n\t\t\tmaxBytes: this.maxBytes,\n\t\t};\n\n\t\tif (options.persistIfTruncated && truncation.truncated) {\n\t\t\tthis.ensureTempFile();\n\t\t}\n\n\t\treturn {\n\t\t\tcontent: truncation.content,\n\t\t\ttruncation,\n\t\t\tfullOutputPath: this.tempFilePath,\n\t\t};\n\t}\n\n\tasync closeTempFile(): Promise<void> {\n\t\tif (!this.tempFileStream) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst stream = this.tempFileStream;\n\t\tthis.tempFileStream = undefined;\n\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tconst onError = (error: Error) => {\n\t\t\t\tstream.off(\"finish\", onFinish);\n\t\t\t\treject(error);\n\t\t\t};\n\t\t\tconst onFinish = () => {\n\t\t\t\tstream.off(\"error\", onError);\n\t\t\t\tresolve();\n\t\t\t};\n\t\t\tstream.once(\"error\", onError);\n\t\t\tstream.once(\"finish\", onFinish);\n\t\t\tstream.end();\n\t\t});\n\t}\n\n\tgetLastLineBytes(): number {\n\t\treturn this.currentLineBytes;\n\t}\n\n\tprivate appendDecodedText(text: string): void {\n\t\tif (text.length === 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst bytes = byteLength(text);\n\t\tthis.totalDecodedBytes += bytes;\n\t\tthis.tailText += text;\n\t\tthis.tailBytes += bytes;\n\t\tif (this.tailBytes > this.maxRollingBytes * 2) {\n\t\t\tthis.trimTail();\n\t\t}\n\n\t\tlet newlines = 0;\n\t\tlet lastNewline = -1;\n\t\tfor (let i = text.indexOf(\"\\n\"); i !== -1; i = text.indexOf(\"\\n\", i + 1)) {\n\t\t\tnewlines++;\n\t\t\tlastNewline = i;\n\t\t}\n\t\tif (newlines === 0) {\n\t\t\tthis.currentLineBytes += bytes;\n\t\t\tthis.hasOpenLine = true;\n\t\t} else {\n\t\t\tthis.completedLines += newlines;\n\t\t\tconst tail = text.slice(lastNewline + 1);\n\t\t\tthis.currentLineBytes = byteLength(tail);\n\t\t\tthis.hasOpenLine = tail.length > 0;\n\t\t}\n\t\tthis.totalLines = this.completedLines + (this.hasOpenLine ? 1 : 0);\n\t}\n\n\tprivate trimTail(): void {\n\t\tconst buffer = Buffer.from(this.tailText, \"utf-8\");\n\t\tif (buffer.length <= this.maxRollingBytes) {\n\t\t\tthis.tailBytes = buffer.length;\n\t\t\treturn;\n\t\t}\n\n\t\tlet start = buffer.length - this.maxRollingBytes;\n\t\twhile (start < buffer.length && (buffer[start] & 0xc0) === 0x80) {\n\t\t\tstart++;\n\t\t}\n\n\t\tthis.tailStartsAtLineBoundary = start === 0 ? this.tailStartsAtLineBoundary : buffer[start - 1] === 0x0a;\n\t\tthis.tailText = buffer.subarray(start).toString(\"utf-8\");\n\t\tthis.tailBytes = byteLength(this.tailText);\n\t}\n\n\tprivate getSnapshotText(): string {\n\t\tif (this.tailStartsAtLineBoundary) {\n\t\t\treturn this.tailText;\n\t\t}\n\n\t\tconst firstNewline = this.tailText.indexOf(\"\\n\");\n\t\treturn firstNewline === -1 ? this.tailText : this.tailText.slice(firstNewline + 1);\n\t}\n\n\tprivate shouldUseTempFile(): boolean {\n\t\treturn (\n\t\t\tthis.totalRawBytes > this.maxBytes || this.totalDecodedBytes > this.maxBytes || this.totalLines > this.maxLines\n\t\t);\n\t}\n\n\tprivate ensureTempFile(): void {\n\t\tif (this.tempFilePath) {\n\t\t\treturn;\n\t\t}\n\t\tthis.tempFilePath = defaultTempFilePath(this.tempFilePrefix);\n\t\tthis.tempFileStream = createWriteStream(this.tempFilePath);\n\t\tfor (const chunk of this.rawChunks) {\n\t\t\tthis.tempFileStream.write(chunk);\n\t\t}\n\t\tthis.rawChunks = [];\n\t}\n}\n"]}
1
+ {"version":3,"file":"output-accumulator.d.ts","sourceRoot":"","sources":["../../../src/core/tools/output-accumulator.ts"],"names":[],"mappings":"AAIA,OAAO,EAAwC,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAE5F,MAAM,WAAW,wBAAwB;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,cAAc;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,gBAAgB,CAAC;IAC7B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,aAAa;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;CACrB;AAwCD;;;;;;;GAOG;AACH,qBAAa,iBAAiB;IAC7B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAE7C,OAAO,CAAC,SAAS,CAAgB;IACjC,OAAO,CAAC,SAAS,CAAgB;IACjC,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,mBAAmB,CAAgB;IAC3C,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,eAAe,CAAM;IAC7B,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,sBAAsB,CAAK;IACnC,OAAO,CAAC,sBAAsB,CAAK;IACnC,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,QAAQ,CAAS;IAEzB,OAAO,CAAC,YAAY,CAAqB;IACzC,OAAO,CAAC,UAAU,CAAqB;IACvC,OAAO,CAAC,aAAa,CAAqB;IAE1C,YAAY,OAAO,GAAE,wBAA6B,EAIjD;IAED,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAQzB;IAED,MAAM,IAAI,IAAI,CASb;IAED,QAAQ,CAAC,OAAO,GAAE;QAAE,kBAAkB,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,cAAc,CAYvE;IAED,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,SAAgB,GAAG,aAAa,CAMjE;IAED,eAAe,CACd,QAAQ,EAAE,MAAM,EAChB,QAAQ,SAAgB,EACxB,OAAO,GAAE;QAAE,sBAAsB,CAAC,EAAE,OAAO,CAAA;KAAO,GAChD,cAAc,CAUhB;IAEK,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAWnC;IAED,gBAAgB,IAAI,MAAM,CAEzB;IAED,OAAO,CAAC,WAAW;IAkBnB,OAAO,CAAC,iBAAiB;IAyBzB,OAAO,CAAC,mBAAmB;IAyB3B,OAAO,CAAC,wBAAwB;IAchC,OAAO,CAAC,cAAc;IActB,OAAO,CAAC,sBAAsB;IAI9B,OAAO,CAAC,aAAa;IA4ErB,OAAO,CAAC,iBAAiB;IAMzB,OAAO,CAAC,cAAc;IAItB,OAAO,CAAC,iBAAiB;IAqBzB,OAAO,CAAC,mBAAmB;CAY3B","sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport { closeSync, openSync, writeSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, type TruncationResult } from \"./truncate.ts\";\n\nexport interface OutputAccumulatorOptions {\n\tmaxLines?: number;\n\tmaxBytes?: number;\n\ttempFilePrefix?: string;\n}\n\nexport interface OutputSnapshot {\n\tcontent: string;\n\ttruncation: TruncationResult;\n\tfullOutputPath?: string;\n\tfullOutputError?: string;\n}\n\nexport interface OutputPreview {\n\tcontent: string;\n\tskippedLines: number;\n}\n\nfunction defaultTempFilePath(prefix: string): string {\n\tconst id = randomBytes(8).toString(\"hex\");\n\treturn join(tmpdir(), `${prefix}-${id}.log`);\n}\n\nconst MAX_APPEND_CHUNK_BYTES = 64 * 1024;\n\nfunction byteLength(text: string): number {\n\treturn Buffer.byteLength(text, \"utf-8\");\n}\n\nfunction formatIoError(error: unknown): string {\n\tif (error instanceof Error) {\n\t\tconst code = (error as NodeJS.ErrnoException).code;\n\t\treturn code ? `${code}: ${error.message}` : error.message;\n\t}\n\treturn String(error);\n}\n\nfunction tailUtf8String(text: string, maxBytes: number): { text: string; bytes: number } {\n\tif (maxBytes <= 0 || text.length === 0) {\n\t\treturn { text: \"\", bytes: 0 };\n\t}\n\n\tconst buffer = Buffer.from(text, \"utf-8\");\n\tif (buffer.length <= maxBytes) {\n\t\treturn { text, bytes: buffer.length };\n\t}\n\n\tlet start = buffer.length - maxBytes;\n\twhile (start < buffer.length && (buffer[start] & 0xc0) === 0x80) {\n\t\tstart++;\n\t}\n\n\tconst result = buffer.subarray(start).toString(\"utf-8\");\n\treturn { text: result, bytes: byteLength(result) };\n}\n\n/**\n * Incrementally tracks streaming output with bounded memory.\n *\n * Appends decode chunks with a streaming UTF-8 decoder, keeps a bounded tail of\n * logical lines, and opens a temp file when the full output needs preserving.\n * Snapshot and preview work is bounded by configured output limits, never by\n * total command history.\n */\nexport class OutputAccumulator {\n\tprivate readonly maxLines: number;\n\tprivate readonly maxBytes: number;\n\tprivate readonly tempFilePrefix: string;\n\tprivate readonly decoder = new TextDecoder();\n\n\tprivate rawChunks: Buffer[] = [];\n\tprivate tailLines: string[] = [];\n\tprivate tailLineBytes: number[] = [];\n\tprivate tailLineStoredBytes: number[] = [];\n\tprivate tailStart = 0;\n\tprivate tailStoredBytes = 0;\n\tprivate currentLineText = \"\";\n\tprivate currentLineBytes = 0;\n\tprivate currentLineStoredBytes = 0;\n\tprivate lastCompletedLineBytes = 0;\n\tprivate totalRawBytes = 0;\n\tprivate totalDecodedBytes = 0;\n\tprivate completedLines = 0;\n\tprivate totalLines = 0;\n\tprivate hasOpenLine = false;\n\tprivate finished = false;\n\n\tprivate tempFilePath: string | undefined;\n\tprivate tempFileFd: number | undefined;\n\tprivate tempFileError: string | undefined;\n\n\tconstructor(options: OutputAccumulatorOptions = {}) {\n\t\tthis.maxLines = options.maxLines ?? DEFAULT_MAX_LINES;\n\t\tthis.maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;\n\t\tthis.tempFilePrefix = options.tempFilePrefix ?? \"pi-output\";\n\t}\n\n\tappend(data: Buffer): void {\n\t\tif (this.finished) {\n\t\t\tthrow new Error(\"Cannot append to a finished output accumulator\");\n\t\t}\n\n\t\tfor (let offset = 0; offset < data.length; offset += MAX_APPEND_CHUNK_BYTES) {\n\t\t\tthis.appendBlock(data.subarray(offset, offset + MAX_APPEND_CHUNK_BYTES));\n\t\t}\n\t}\n\n\tfinish(): void {\n\t\tif (this.finished) {\n\t\t\treturn;\n\t\t}\n\t\tthis.finished = true;\n\t\tthis.appendDecodedText(this.decoder.decode());\n\t\tif (this.shouldUseTempFile()) {\n\t\t\tthis.tryEnsureTempFile();\n\t\t}\n\t}\n\n\tsnapshot(options: { persistIfTruncated?: boolean } = {}): OutputSnapshot {\n\t\tconst snapshot = this.buildSnapshot(this.maxLines, this.maxBytes);\n\n\t\tif (options.persistIfTruncated && snapshot.truncation.truncated) {\n\t\t\tthis.tryEnsureTempFile();\n\t\t}\n\n\t\treturn {\n\t\t\t...snapshot,\n\t\t\tfullOutputPath: this.fullOutputPath(),\n\t\t\tfullOutputError: this.tempFileError,\n\t\t};\n\t}\n\n\tpreview(maxLines: number, maxBytes = this.maxBytes): OutputPreview {\n\t\tconst snapshot = this.previewSnapshot(maxLines, maxBytes);\n\t\treturn {\n\t\t\tcontent: snapshot.content,\n\t\t\tskippedLines: Math.max(0, this.totalLines - snapshot.truncation.outputLines),\n\t\t};\n\t}\n\n\tpreviewSnapshot(\n\t\tmaxLines: number,\n\t\tmaxBytes = this.maxBytes,\n\t\toptions: { persistIfFullTruncated?: boolean } = {},\n\t): OutputSnapshot {\n\t\tconst snapshot = this.buildSnapshot(maxLines, maxBytes);\n\t\tif (options.persistIfFullTruncated && this.shouldUseTempFile()) {\n\t\t\tthis.tryEnsureTempFile();\n\t\t}\n\t\treturn {\n\t\t\t...snapshot,\n\t\t\tfullOutputPath: this.fullOutputPath(),\n\t\t\tfullOutputError: this.tempFileError,\n\t\t};\n\t}\n\n\tasync closeTempFile(): Promise<void> {\n\t\tconst fd = this.tempFileFd;\n\t\tif (fd === undefined) {\n\t\t\treturn;\n\t\t}\n\t\tthis.tempFileFd = undefined;\n\t\ttry {\n\t\t\tcloseSync(fd);\n\t\t} catch (error) {\n\t\t\tthis.tempFileError ??= formatIoError(error);\n\t\t}\n\t}\n\n\tgetLastLineBytes(): number {\n\t\treturn this.hasOpenLine ? this.currentLineBytes : this.lastCompletedLineBytes;\n\t}\n\n\tprivate appendBlock(data: Buffer): void {\n\t\tthis.totalRawBytes += data.length;\n\t\tthis.appendDecodedText(this.decoder.decode(data, { stream: true }));\n\n\t\tif (this.tempFileFd !== undefined || this.shouldUseTempFile()) {\n\t\t\tif (this.tryEnsureTempFile() && this.tempFileFd !== undefined) {\n\t\t\t\ttry {\n\t\t\t\t\twriteSync(this.tempFileFd, data);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tthis.recordTempFileError(error);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (data.length > 0) {\n\t\t\t// Copy retained chunks: Buffer.subarray would pin a large caller buffer in memory.\n\t\t\tthis.rawChunks.push(Buffer.from(data));\n\t\t}\n\t}\n\n\tprivate appendDecodedText(text: string): void {\n\t\tif (text.length === 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.totalDecodedBytes += byteLength(text);\n\n\t\tlet segmentStart = 0;\n\t\tfor (\n\t\t\tlet newlineIndex = text.indexOf(\"\\n\");\n\t\t\tnewlineIndex !== -1;\n\t\t\tnewlineIndex = text.indexOf(\"\\n\", segmentStart)\n\t\t) {\n\t\t\tthis.appendToCurrentLine(text.slice(segmentStart, newlineIndex));\n\t\t\tthis.pushCompletedCurrentLine();\n\t\t\tsegmentStart = newlineIndex + 1;\n\t\t}\n\n\t\tif (segmentStart < text.length) {\n\t\t\tthis.appendToCurrentLine(text.slice(segmentStart));\n\t\t}\n\n\t\tthis.totalLines = this.completedLines + (this.hasOpenLine ? 1 : 0);\n\t}\n\n\tprivate appendToCurrentLine(segment: string): void {\n\t\tif (segment.length === 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst segmentBytes = byteLength(segment);\n\t\tthis.currentLineBytes += segmentBytes;\n\t\tthis.hasOpenLine = true;\n\n\t\tif (segmentBytes >= this.maxBytes) {\n\t\t\tconst tail = tailUtf8String(segment, this.maxBytes);\n\t\t\tthis.currentLineText = tail.text;\n\t\t\tthis.currentLineStoredBytes = tail.bytes;\n\t\t\treturn;\n\t\t}\n\n\t\tthis.currentLineText += segment;\n\t\tthis.currentLineStoredBytes += segmentBytes;\n\t\tif (this.currentLineStoredBytes > this.maxBytes) {\n\t\t\tconst tail = tailUtf8String(this.currentLineText, this.maxBytes);\n\t\t\tthis.currentLineText = tail.text;\n\t\t\tthis.currentLineStoredBytes = tail.bytes;\n\t\t}\n\t}\n\n\tprivate pushCompletedCurrentLine(): void {\n\t\tthis.completedLines++;\n\t\tthis.lastCompletedLineBytes = this.currentLineBytes;\n\t\tthis.tailLines.push(this.currentLineText);\n\t\tthis.tailLineBytes.push(this.currentLineBytes);\n\t\tthis.tailLineStoredBytes.push(this.currentLineStoredBytes);\n\t\tthis.tailStoredBytes += this.currentLineStoredBytes;\n\t\tthis.currentLineText = \"\";\n\t\tthis.currentLineBytes = 0;\n\t\tthis.currentLineStoredBytes = 0;\n\t\tthis.hasOpenLine = false;\n\t\tthis.trimStoredTail();\n\t}\n\n\tprivate trimStoredTail(): void {\n\t\twhile (this.completedTailLineCount() > this.maxLines || this.tailStoredBytes > this.maxBytes) {\n\t\t\tthis.tailStoredBytes -= this.tailLineStoredBytes[this.tailStart] ?? 0;\n\t\t\tthis.tailStart++;\n\t\t}\n\n\t\tif (this.tailStart > 1024 && this.tailStart * 2 > this.tailLines.length) {\n\t\t\tthis.tailLines = this.tailLines.slice(this.tailStart);\n\t\t\tthis.tailLineBytes = this.tailLineBytes.slice(this.tailStart);\n\t\t\tthis.tailLineStoredBytes = this.tailLineStoredBytes.slice(this.tailStart);\n\t\t\tthis.tailStart = 0;\n\t\t}\n\t}\n\n\tprivate completedTailLineCount(): number {\n\t\treturn this.tailLines.length - this.tailStart;\n\t}\n\n\tprivate buildSnapshot(maxLines: number, maxBytes: number): OutputSnapshot {\n\t\tconst truncated = this.totalLines > maxLines || this.totalDecodedBytes > maxBytes;\n\t\tconst outputLines: string[] = [];\n\t\tlet outputBytes = 0;\n\t\tlet outputLineCount = 0;\n\t\tlet truncatedBy: \"lines\" | \"bytes\" = this.totalLines > maxLines ? \"lines\" : \"bytes\";\n\t\tlet lastLinePartial = false;\n\t\tlet readCurrent = this.hasOpenLine;\n\t\tlet completedIndex = this.tailLines.length - 1;\n\n\t\twhile (outputLineCount < maxLines) {\n\t\t\tlet line: string;\n\t\t\tlet lineBytes: number;\n\t\t\tlet storedBytes: number;\n\t\t\tif (readCurrent) {\n\t\t\t\tline = this.currentLineText;\n\t\t\t\tlineBytes = this.currentLineBytes;\n\t\t\t\tstoredBytes = this.currentLineStoredBytes;\n\t\t\t\treadCurrent = false;\n\t\t\t} else {\n\t\t\t\tif (completedIndex < this.tailStart) break;\n\t\t\t\tline = this.tailLines[completedIndex] ?? \"\";\n\t\t\t\tlineBytes = this.tailLineBytes[completedIndex] ?? 0;\n\t\t\t\tstoredBytes = this.tailLineStoredBytes[completedIndex] ?? 0;\n\t\t\t\tcompletedIndex--;\n\t\t\t}\n\n\t\t\tconst separatorBytes = outputLineCount > 0 ? 1 : 0;\n\t\t\tconst fullLineBytes = lineBytes + separatorBytes;\n\t\t\tif (outputBytes + fullLineBytes > maxBytes) {\n\t\t\t\ttruncatedBy = \"bytes\";\n\t\t\t\tif (outputLineCount === 0) {\n\t\t\t\t\tconst partial =\n\t\t\t\t\t\tlineBytes > maxBytes ? tailUtf8String(line, maxBytes) : { text: line, bytes: storedBytes };\n\t\t\t\t\toutputLines.unshift(partial.text);\n\t\t\t\t\toutputBytes = partial.bytes;\n\t\t\t\t\toutputLineCount = 1;\n\t\t\t\t\tlastLinePartial = lineBytes > partial.bytes;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\toutputLines.unshift(line);\n\t\t\toutputBytes += storedBytes + separatorBytes;\n\t\t\toutputLineCount++;\n\t\t}\n\n\t\tlet content = outputLines.join(\"\\n\");\n\t\tif (!truncated && !this.hasOpenLine && this.totalLines > 0) {\n\t\t\tcontent += \"\\n\";\n\t\t\toutputBytes += 1;\n\t\t}\n\n\t\tconst effectiveTruncatedBy = truncated ? truncatedBy : null;\n\t\tconst truncation: TruncationResult = {\n\t\t\tcontent,\n\t\t\ttruncated,\n\t\t\ttruncatedBy: effectiveTruncatedBy,\n\t\t\ttotalLines: this.totalLines,\n\t\t\ttotalBytes: this.totalDecodedBytes,\n\t\t\toutputLines: outputLineCount,\n\t\t\toutputBytes,\n\t\t\tlastLinePartial,\n\t\t\tfirstLineExceedsLimit: false,\n\t\t\tmaxLines,\n\t\t\tmaxBytes,\n\t\t};\n\n\t\treturn {\n\t\t\tcontent,\n\t\t\ttruncation,\n\t\t\tfullOutputPath: this.fullOutputPath(),\n\t\t\tfullOutputError: this.tempFileError,\n\t\t};\n\t}\n\n\tprivate shouldUseTempFile(): boolean {\n\t\treturn (\n\t\t\tthis.totalRawBytes > this.maxBytes || this.totalDecodedBytes > this.maxBytes || this.totalLines > this.maxLines\n\t\t);\n\t}\n\n\tprivate fullOutputPath(): string | undefined {\n\t\treturn this.tempFileError === undefined ? this.tempFilePath : undefined;\n\t}\n\n\tprivate tryEnsureTempFile(): boolean {\n\t\tif (this.tempFileError !== undefined) {\n\t\t\treturn false;\n\t\t}\n\t\tif (this.tempFileFd !== undefined) {\n\t\t\treturn true;\n\t\t}\n\t\ttry {\n\t\t\tthis.tempFilePath ??= defaultTempFilePath(this.tempFilePrefix);\n\t\t\tthis.tempFileFd = openSync(this.tempFilePath, \"w\");\n\t\t\tfor (const chunk of this.rawChunks) {\n\t\t\t\twriteSync(this.tempFileFd, chunk);\n\t\t\t}\n\t\t\tthis.rawChunks = [];\n\t\t\treturn true;\n\t\t} catch (error) {\n\t\t\tthis.recordTempFileError(error);\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tprivate recordTempFileError(error: unknown): void {\n\t\tthis.tempFileError ??= formatIoError(error);\n\t\tconst fd = this.tempFileFd;\n\t\tthis.tempFileFd = undefined;\n\t\tif (fd !== undefined) {\n\t\t\ttry {\n\t\t\t\tcloseSync(fd);\n\t\t\t} catch (closeError) {\n\t\t\t\tthis.tempFileError += `; close failed: ${formatIoError(closeError)}`;\n\t\t\t}\n\t\t}\n\t}\n}\n"]}
@@ -1,59 +1,81 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { createWriteStream } from "node:fs";
2
+ import { closeSync, openSync, writeSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
- import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, truncateTail } from "./truncate.js";
5
+ import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES } from "./truncate.js";
6
6
  function defaultTempFilePath(prefix) {
7
7
  const id = randomBytes(8).toString("hex");
8
8
  return join(tmpdir(), `${prefix}-${id}.log`);
9
9
  }
10
+ const MAX_APPEND_CHUNK_BYTES = 64 * 1024;
10
11
  function byteLength(text) {
11
12
  return Buffer.byteLength(text, "utf-8");
12
13
  }
14
+ function formatIoError(error) {
15
+ if (error instanceof Error) {
16
+ const code = error.code;
17
+ return code ? `${code}: ${error.message}` : error.message;
18
+ }
19
+ return String(error);
20
+ }
21
+ function tailUtf8String(text, maxBytes) {
22
+ if (maxBytes <= 0 || text.length === 0) {
23
+ return { text: "", bytes: 0 };
24
+ }
25
+ const buffer = Buffer.from(text, "utf-8");
26
+ if (buffer.length <= maxBytes) {
27
+ return { text, bytes: buffer.length };
28
+ }
29
+ let start = buffer.length - maxBytes;
30
+ while (start < buffer.length && (buffer[start] & 0xc0) === 0x80) {
31
+ start++;
32
+ }
33
+ const result = buffer.subarray(start).toString("utf-8");
34
+ return { text: result, bytes: byteLength(result) };
35
+ }
13
36
  /**
14
37
  * Incrementally tracks streaming output with bounded memory.
15
38
  *
16
- * Appends decode chunks with a streaming UTF-8 decoder, keeps only a decoded
17
- * tail for display snapshots, and opens a temp file when the full output needs
18
- * to be preserved.
39
+ * Appends decode chunks with a streaming UTF-8 decoder, keeps a bounded tail of
40
+ * logical lines, and opens a temp file when the full output needs preserving.
41
+ * Snapshot and preview work is bounded by configured output limits, never by
42
+ * total command history.
19
43
  */
20
44
  export class OutputAccumulator {
21
45
  maxLines;
22
46
  maxBytes;
23
- maxRollingBytes;
24
47
  tempFilePrefix;
25
48
  decoder = new TextDecoder();
26
49
  rawChunks = [];
27
- tailText = "";
28
- tailBytes = 0;
29
- tailStartsAtLineBoundary = true;
50
+ tailLines = [];
51
+ tailLineBytes = [];
52
+ tailLineStoredBytes = [];
53
+ tailStart = 0;
54
+ tailStoredBytes = 0;
55
+ currentLineText = "";
56
+ currentLineBytes = 0;
57
+ currentLineStoredBytes = 0;
58
+ lastCompletedLineBytes = 0;
30
59
  totalRawBytes = 0;
31
60
  totalDecodedBytes = 0;
32
61
  completedLines = 0;
33
62
  totalLines = 0;
34
- currentLineBytes = 0;
35
63
  hasOpenLine = false;
36
64
  finished = false;
37
65
  tempFilePath;
38
- tempFileStream;
66
+ tempFileFd;
67
+ tempFileError;
39
68
  constructor(options = {}) {
40
69
  this.maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
41
70
  this.maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
42
- this.maxRollingBytes = Math.max(this.maxBytes * 2, 1);
43
71
  this.tempFilePrefix = options.tempFilePrefix ?? "pi-output";
44
72
  }
45
73
  append(data) {
46
74
  if (this.finished) {
47
75
  throw new Error("Cannot append to a finished output accumulator");
48
76
  }
49
- this.totalRawBytes += data.length;
50
- this.appendDecodedText(this.decoder.decode(data, { stream: true }));
51
- if (this.tempFileStream || this.shouldUseTempFile()) {
52
- this.ensureTempFile();
53
- this.tempFileStream?.write(data);
54
- }
55
- else if (data.length > 0) {
56
- this.rawChunks.push(data);
77
+ for (let offset = 0; offset < data.length; offset += MAX_APPEND_CHUNK_BYTES) {
78
+ this.appendBlock(data.subarray(offset, offset + MAX_APPEND_CHUNK_BYTES));
57
79
  }
58
80
  }
59
81
  finish() {
@@ -63,122 +85,246 @@ export class OutputAccumulator {
63
85
  this.finished = true;
64
86
  this.appendDecodedText(this.decoder.decode());
65
87
  if (this.shouldUseTempFile()) {
66
- this.ensureTempFile();
88
+ this.tryEnsureTempFile();
67
89
  }
68
90
  }
69
91
  snapshot(options = {}) {
70
- const tailTruncation = truncateTail(this.getSnapshotText(), {
71
- maxLines: this.maxLines,
72
- maxBytes: this.maxBytes,
73
- });
74
- const truncated = this.totalLines > this.maxLines || this.totalDecodedBytes > this.maxBytes;
75
- const truncatedBy = truncated
76
- ? (tailTruncation.truncatedBy ?? (this.totalDecodedBytes > this.maxBytes ? "bytes" : "lines"))
77
- : null;
78
- const truncation = {
79
- ...tailTruncation,
80
- truncated,
81
- truncatedBy,
82
- totalLines: this.totalLines,
83
- totalBytes: this.totalDecodedBytes,
84
- maxLines: this.maxLines,
85
- maxBytes: this.maxBytes,
92
+ const snapshot = this.buildSnapshot(this.maxLines, this.maxBytes);
93
+ if (options.persistIfTruncated && snapshot.truncation.truncated) {
94
+ this.tryEnsureTempFile();
95
+ }
96
+ return {
97
+ ...snapshot,
98
+ fullOutputPath: this.fullOutputPath(),
99
+ fullOutputError: this.tempFileError,
100
+ };
101
+ }
102
+ preview(maxLines, maxBytes = this.maxBytes) {
103
+ const snapshot = this.previewSnapshot(maxLines, maxBytes);
104
+ return {
105
+ content: snapshot.content,
106
+ skippedLines: Math.max(0, this.totalLines - snapshot.truncation.outputLines),
86
107
  };
87
- if (options.persistIfTruncated && truncation.truncated) {
88
- this.ensureTempFile();
108
+ }
109
+ previewSnapshot(maxLines, maxBytes = this.maxBytes, options = {}) {
110
+ const snapshot = this.buildSnapshot(maxLines, maxBytes);
111
+ if (options.persistIfFullTruncated && this.shouldUseTempFile()) {
112
+ this.tryEnsureTempFile();
89
113
  }
90
114
  return {
91
- content: truncation.content,
92
- truncation,
93
- fullOutputPath: this.tempFilePath,
115
+ ...snapshot,
116
+ fullOutputPath: this.fullOutputPath(),
117
+ fullOutputError: this.tempFileError,
94
118
  };
95
119
  }
96
120
  async closeTempFile() {
97
- if (!this.tempFileStream) {
121
+ const fd = this.tempFileFd;
122
+ if (fd === undefined) {
98
123
  return;
99
124
  }
100
- const stream = this.tempFileStream;
101
- this.tempFileStream = undefined;
102
- await new Promise((resolve, reject) => {
103
- const onError = (error) => {
104
- stream.off("finish", onFinish);
105
- reject(error);
106
- };
107
- const onFinish = () => {
108
- stream.off("error", onError);
109
- resolve();
110
- };
111
- stream.once("error", onError);
112
- stream.once("finish", onFinish);
113
- stream.end();
114
- });
125
+ this.tempFileFd = undefined;
126
+ try {
127
+ closeSync(fd);
128
+ }
129
+ catch (error) {
130
+ this.tempFileError ??= formatIoError(error);
131
+ }
115
132
  }
116
133
  getLastLineBytes() {
117
- return this.currentLineBytes;
134
+ return this.hasOpenLine ? this.currentLineBytes : this.lastCompletedLineBytes;
135
+ }
136
+ appendBlock(data) {
137
+ this.totalRawBytes += data.length;
138
+ this.appendDecodedText(this.decoder.decode(data, { stream: true }));
139
+ if (this.tempFileFd !== undefined || this.shouldUseTempFile()) {
140
+ if (this.tryEnsureTempFile() && this.tempFileFd !== undefined) {
141
+ try {
142
+ writeSync(this.tempFileFd, data);
143
+ }
144
+ catch (error) {
145
+ this.recordTempFileError(error);
146
+ }
147
+ }
148
+ }
149
+ else if (data.length > 0) {
150
+ // Copy retained chunks: Buffer.subarray would pin a large caller buffer in memory.
151
+ this.rawChunks.push(Buffer.from(data));
152
+ }
118
153
  }
119
154
  appendDecodedText(text) {
120
155
  if (text.length === 0) {
121
156
  return;
122
157
  }
123
- const bytes = byteLength(text);
124
- this.totalDecodedBytes += bytes;
125
- this.tailText += text;
126
- this.tailBytes += bytes;
127
- if (this.tailBytes > this.maxRollingBytes * 2) {
128
- this.trimTail();
129
- }
130
- let newlines = 0;
131
- let lastNewline = -1;
132
- for (let i = text.indexOf("\n"); i !== -1; i = text.indexOf("\n", i + 1)) {
133
- newlines++;
134
- lastNewline = i;
135
- }
136
- if (newlines === 0) {
137
- this.currentLineBytes += bytes;
138
- this.hasOpenLine = true;
139
- }
140
- else {
141
- this.completedLines += newlines;
142
- const tail = text.slice(lastNewline + 1);
143
- this.currentLineBytes = byteLength(tail);
144
- this.hasOpenLine = tail.length > 0;
158
+ this.totalDecodedBytes += byteLength(text);
159
+ let segmentStart = 0;
160
+ for (let newlineIndex = text.indexOf("\n"); newlineIndex !== -1; newlineIndex = text.indexOf("\n", segmentStart)) {
161
+ this.appendToCurrentLine(text.slice(segmentStart, newlineIndex));
162
+ this.pushCompletedCurrentLine();
163
+ segmentStart = newlineIndex + 1;
164
+ }
165
+ if (segmentStart < text.length) {
166
+ this.appendToCurrentLine(text.slice(segmentStart));
145
167
  }
146
168
  this.totalLines = this.completedLines + (this.hasOpenLine ? 1 : 0);
147
169
  }
148
- trimTail() {
149
- const buffer = Buffer.from(this.tailText, "utf-8");
150
- if (buffer.length <= this.maxRollingBytes) {
151
- this.tailBytes = buffer.length;
170
+ appendToCurrentLine(segment) {
171
+ if (segment.length === 0) {
172
+ return;
173
+ }
174
+ const segmentBytes = byteLength(segment);
175
+ this.currentLineBytes += segmentBytes;
176
+ this.hasOpenLine = true;
177
+ if (segmentBytes >= this.maxBytes) {
178
+ const tail = tailUtf8String(segment, this.maxBytes);
179
+ this.currentLineText = tail.text;
180
+ this.currentLineStoredBytes = tail.bytes;
152
181
  return;
153
182
  }
154
- let start = buffer.length - this.maxRollingBytes;
155
- while (start < buffer.length && (buffer[start] & 0xc0) === 0x80) {
156
- start++;
183
+ this.currentLineText += segment;
184
+ this.currentLineStoredBytes += segmentBytes;
185
+ if (this.currentLineStoredBytes > this.maxBytes) {
186
+ const tail = tailUtf8String(this.currentLineText, this.maxBytes);
187
+ this.currentLineText = tail.text;
188
+ this.currentLineStoredBytes = tail.bytes;
157
189
  }
158
- this.tailStartsAtLineBoundary = start === 0 ? this.tailStartsAtLineBoundary : buffer[start - 1] === 0x0a;
159
- this.tailText = buffer.subarray(start).toString("utf-8");
160
- this.tailBytes = byteLength(this.tailText);
161
190
  }
162
- getSnapshotText() {
163
- if (this.tailStartsAtLineBoundary) {
164
- return this.tailText;
191
+ pushCompletedCurrentLine() {
192
+ this.completedLines++;
193
+ this.lastCompletedLineBytes = this.currentLineBytes;
194
+ this.tailLines.push(this.currentLineText);
195
+ this.tailLineBytes.push(this.currentLineBytes);
196
+ this.tailLineStoredBytes.push(this.currentLineStoredBytes);
197
+ this.tailStoredBytes += this.currentLineStoredBytes;
198
+ this.currentLineText = "";
199
+ this.currentLineBytes = 0;
200
+ this.currentLineStoredBytes = 0;
201
+ this.hasOpenLine = false;
202
+ this.trimStoredTail();
203
+ }
204
+ trimStoredTail() {
205
+ while (this.completedTailLineCount() > this.maxLines || this.tailStoredBytes > this.maxBytes) {
206
+ this.tailStoredBytes -= this.tailLineStoredBytes[this.tailStart] ?? 0;
207
+ this.tailStart++;
208
+ }
209
+ if (this.tailStart > 1024 && this.tailStart * 2 > this.tailLines.length) {
210
+ this.tailLines = this.tailLines.slice(this.tailStart);
211
+ this.tailLineBytes = this.tailLineBytes.slice(this.tailStart);
212
+ this.tailLineStoredBytes = this.tailLineStoredBytes.slice(this.tailStart);
213
+ this.tailStart = 0;
165
214
  }
166
- const firstNewline = this.tailText.indexOf("\n");
167
- return firstNewline === -1 ? this.tailText : this.tailText.slice(firstNewline + 1);
215
+ }
216
+ completedTailLineCount() {
217
+ return this.tailLines.length - this.tailStart;
218
+ }
219
+ buildSnapshot(maxLines, maxBytes) {
220
+ const truncated = this.totalLines > maxLines || this.totalDecodedBytes > maxBytes;
221
+ const outputLines = [];
222
+ let outputBytes = 0;
223
+ let outputLineCount = 0;
224
+ let truncatedBy = this.totalLines > maxLines ? "lines" : "bytes";
225
+ let lastLinePartial = false;
226
+ let readCurrent = this.hasOpenLine;
227
+ let completedIndex = this.tailLines.length - 1;
228
+ while (outputLineCount < maxLines) {
229
+ let line;
230
+ let lineBytes;
231
+ let storedBytes;
232
+ if (readCurrent) {
233
+ line = this.currentLineText;
234
+ lineBytes = this.currentLineBytes;
235
+ storedBytes = this.currentLineStoredBytes;
236
+ readCurrent = false;
237
+ }
238
+ else {
239
+ if (completedIndex < this.tailStart)
240
+ break;
241
+ line = this.tailLines[completedIndex] ?? "";
242
+ lineBytes = this.tailLineBytes[completedIndex] ?? 0;
243
+ storedBytes = this.tailLineStoredBytes[completedIndex] ?? 0;
244
+ completedIndex--;
245
+ }
246
+ const separatorBytes = outputLineCount > 0 ? 1 : 0;
247
+ const fullLineBytes = lineBytes + separatorBytes;
248
+ if (outputBytes + fullLineBytes > maxBytes) {
249
+ truncatedBy = "bytes";
250
+ if (outputLineCount === 0) {
251
+ const partial = lineBytes > maxBytes ? tailUtf8String(line, maxBytes) : { text: line, bytes: storedBytes };
252
+ outputLines.unshift(partial.text);
253
+ outputBytes = partial.bytes;
254
+ outputLineCount = 1;
255
+ lastLinePartial = lineBytes > partial.bytes;
256
+ }
257
+ break;
258
+ }
259
+ outputLines.unshift(line);
260
+ outputBytes += storedBytes + separatorBytes;
261
+ outputLineCount++;
262
+ }
263
+ let content = outputLines.join("\n");
264
+ if (!truncated && !this.hasOpenLine && this.totalLines > 0) {
265
+ content += "\n";
266
+ outputBytes += 1;
267
+ }
268
+ const effectiveTruncatedBy = truncated ? truncatedBy : null;
269
+ const truncation = {
270
+ content,
271
+ truncated,
272
+ truncatedBy: effectiveTruncatedBy,
273
+ totalLines: this.totalLines,
274
+ totalBytes: this.totalDecodedBytes,
275
+ outputLines: outputLineCount,
276
+ outputBytes,
277
+ lastLinePartial,
278
+ firstLineExceedsLimit: false,
279
+ maxLines,
280
+ maxBytes,
281
+ };
282
+ return {
283
+ content,
284
+ truncation,
285
+ fullOutputPath: this.fullOutputPath(),
286
+ fullOutputError: this.tempFileError,
287
+ };
168
288
  }
169
289
  shouldUseTempFile() {
170
290
  return (this.totalRawBytes > this.maxBytes || this.totalDecodedBytes > this.maxBytes || this.totalLines > this.maxLines);
171
291
  }
172
- ensureTempFile() {
173
- if (this.tempFilePath) {
174
- return;
292
+ fullOutputPath() {
293
+ return this.tempFileError === undefined ? this.tempFilePath : undefined;
294
+ }
295
+ tryEnsureTempFile() {
296
+ if (this.tempFileError !== undefined) {
297
+ return false;
298
+ }
299
+ if (this.tempFileFd !== undefined) {
300
+ return true;
175
301
  }
176
- this.tempFilePath = defaultTempFilePath(this.tempFilePrefix);
177
- this.tempFileStream = createWriteStream(this.tempFilePath);
178
- for (const chunk of this.rawChunks) {
179
- this.tempFileStream.write(chunk);
302
+ try {
303
+ this.tempFilePath ??= defaultTempFilePath(this.tempFilePrefix);
304
+ this.tempFileFd = openSync(this.tempFilePath, "w");
305
+ for (const chunk of this.rawChunks) {
306
+ writeSync(this.tempFileFd, chunk);
307
+ }
308
+ this.rawChunks = [];
309
+ return true;
310
+ }
311
+ catch (error) {
312
+ this.recordTempFileError(error);
313
+ return false;
314
+ }
315
+ }
316
+ recordTempFileError(error) {
317
+ this.tempFileError ??= formatIoError(error);
318
+ const fd = this.tempFileFd;
319
+ this.tempFileFd = undefined;
320
+ if (fd !== undefined) {
321
+ try {
322
+ closeSync(fd);
323
+ }
324
+ catch (closeError) {
325
+ this.tempFileError += `; close failed: ${formatIoError(closeError)}`;
326
+ }
180
327
  }
181
- this.rawChunks = [];
182
328
  }
183
329
  }
184
330
  //# sourceMappingURL=output-accumulator.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"output-accumulator.js","sourceRoot":"","sources":["../../../src/core/tools/output-accumulator.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport { createWriteStream, type WriteStream } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, type TruncationResult, truncateTail } from \"./truncate.ts\";\n\nexport interface OutputAccumulatorOptions {\n\tmaxLines?: number;\n\tmaxBytes?: number;\n\ttempFilePrefix?: string;\n}\n\nexport interface OutputSnapshot {\n\tcontent: string;\n\ttruncation: TruncationResult;\n\tfullOutputPath?: string;\n}\n\nfunction defaultTempFilePath(prefix: string): string {\n\tconst id = randomBytes(8).toString(\"hex\");\n\treturn join(tmpdir(), `${prefix}-${id}.log`);\n}\n\nfunction byteLength(text: string): number {\n\treturn Buffer.byteLength(text, \"utf-8\");\n}\n\n/**\n * Incrementally tracks streaming output with bounded memory.\n *\n * Appends decode chunks with a streaming UTF-8 decoder, keeps only a decoded\n * tail for display snapshots, and opens a temp file when the full output needs\n * to be preserved.\n */\nexport class OutputAccumulator {\n\tprivate readonly maxLines: number;\n\tprivate readonly maxBytes: number;\n\tprivate readonly maxRollingBytes: number;\n\tprivate readonly tempFilePrefix: string;\n\tprivate readonly decoder = new TextDecoder();\n\n\tprivate rawChunks: Buffer[] = [];\n\tprivate tailText = \"\";\n\tprivate tailBytes = 0;\n\tprivate tailStartsAtLineBoundary = true;\n\tprivate totalRawBytes = 0;\n\tprivate totalDecodedBytes = 0;\n\tprivate completedLines = 0;\n\tprivate totalLines = 0;\n\tprivate currentLineBytes = 0;\n\tprivate hasOpenLine = false;\n\tprivate finished = false;\n\n\tprivate tempFilePath: string | undefined;\n\tprivate tempFileStream: WriteStream | undefined;\n\n\tconstructor(options: OutputAccumulatorOptions = {}) {\n\t\tthis.maxLines = options.maxLines ?? DEFAULT_MAX_LINES;\n\t\tthis.maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;\n\t\tthis.maxRollingBytes = Math.max(this.maxBytes * 2, 1);\n\t\tthis.tempFilePrefix = options.tempFilePrefix ?? \"pi-output\";\n\t}\n\n\tappend(data: Buffer): void {\n\t\tif (this.finished) {\n\t\t\tthrow new Error(\"Cannot append to a finished output accumulator\");\n\t\t}\n\n\t\tthis.totalRawBytes += data.length;\n\t\tthis.appendDecodedText(this.decoder.decode(data, { stream: true }));\n\n\t\tif (this.tempFileStream || this.shouldUseTempFile()) {\n\t\t\tthis.ensureTempFile();\n\t\t\tthis.tempFileStream?.write(data);\n\t\t} else if (data.length > 0) {\n\t\t\tthis.rawChunks.push(data);\n\t\t}\n\t}\n\n\tfinish(): void {\n\t\tif (this.finished) {\n\t\t\treturn;\n\t\t}\n\t\tthis.finished = true;\n\t\tthis.appendDecodedText(this.decoder.decode());\n\t\tif (this.shouldUseTempFile()) {\n\t\t\tthis.ensureTempFile();\n\t\t}\n\t}\n\n\tsnapshot(options: { persistIfTruncated?: boolean } = {}): OutputSnapshot {\n\t\tconst tailTruncation = truncateTail(this.getSnapshotText(), {\n\t\t\tmaxLines: this.maxLines,\n\t\t\tmaxBytes: this.maxBytes,\n\t\t});\n\t\tconst truncated = this.totalLines > this.maxLines || this.totalDecodedBytes > this.maxBytes;\n\t\tconst truncatedBy = truncated\n\t\t\t? (tailTruncation.truncatedBy ?? (this.totalDecodedBytes > this.maxBytes ? \"bytes\" : \"lines\"))\n\t\t\t: null;\n\t\tconst truncation: TruncationResult = {\n\t\t\t...tailTruncation,\n\t\t\ttruncated,\n\t\t\ttruncatedBy,\n\t\t\ttotalLines: this.totalLines,\n\t\t\ttotalBytes: this.totalDecodedBytes,\n\t\t\tmaxLines: this.maxLines,\n\t\t\tmaxBytes: this.maxBytes,\n\t\t};\n\n\t\tif (options.persistIfTruncated && truncation.truncated) {\n\t\t\tthis.ensureTempFile();\n\t\t}\n\n\t\treturn {\n\t\t\tcontent: truncation.content,\n\t\t\ttruncation,\n\t\t\tfullOutputPath: this.tempFilePath,\n\t\t};\n\t}\n\n\tasync closeTempFile(): Promise<void> {\n\t\tif (!this.tempFileStream) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst stream = this.tempFileStream;\n\t\tthis.tempFileStream = undefined;\n\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tconst onError = (error: Error) => {\n\t\t\t\tstream.off(\"finish\", onFinish);\n\t\t\t\treject(error);\n\t\t\t};\n\t\t\tconst onFinish = () => {\n\t\t\t\tstream.off(\"error\", onError);\n\t\t\t\tresolve();\n\t\t\t};\n\t\t\tstream.once(\"error\", onError);\n\t\t\tstream.once(\"finish\", onFinish);\n\t\t\tstream.end();\n\t\t});\n\t}\n\n\tgetLastLineBytes(): number {\n\t\treturn this.currentLineBytes;\n\t}\n\n\tprivate appendDecodedText(text: string): void {\n\t\tif (text.length === 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst bytes = byteLength(text);\n\t\tthis.totalDecodedBytes += bytes;\n\t\tthis.tailText += text;\n\t\tthis.tailBytes += bytes;\n\t\tif (this.tailBytes > this.maxRollingBytes * 2) {\n\t\t\tthis.trimTail();\n\t\t}\n\n\t\tlet newlines = 0;\n\t\tlet lastNewline = -1;\n\t\tfor (let i = text.indexOf(\"\\n\"); i !== -1; i = text.indexOf(\"\\n\", i + 1)) {\n\t\t\tnewlines++;\n\t\t\tlastNewline = i;\n\t\t}\n\t\tif (newlines === 0) {\n\t\t\tthis.currentLineBytes += bytes;\n\t\t\tthis.hasOpenLine = true;\n\t\t} else {\n\t\t\tthis.completedLines += newlines;\n\t\t\tconst tail = text.slice(lastNewline + 1);\n\t\t\tthis.currentLineBytes = byteLength(tail);\n\t\t\tthis.hasOpenLine = tail.length > 0;\n\t\t}\n\t\tthis.totalLines = this.completedLines + (this.hasOpenLine ? 1 : 0);\n\t}\n\n\tprivate trimTail(): void {\n\t\tconst buffer = Buffer.from(this.tailText, \"utf-8\");\n\t\tif (buffer.length <= this.maxRollingBytes) {\n\t\t\tthis.tailBytes = buffer.length;\n\t\t\treturn;\n\t\t}\n\n\t\tlet start = buffer.length - this.maxRollingBytes;\n\t\twhile (start < buffer.length && (buffer[start] & 0xc0) === 0x80) {\n\t\t\tstart++;\n\t\t}\n\n\t\tthis.tailStartsAtLineBoundary = start === 0 ? this.tailStartsAtLineBoundary : buffer[start - 1] === 0x0a;\n\t\tthis.tailText = buffer.subarray(start).toString(\"utf-8\");\n\t\tthis.tailBytes = byteLength(this.tailText);\n\t}\n\n\tprivate getSnapshotText(): string {\n\t\tif (this.tailStartsAtLineBoundary) {\n\t\t\treturn this.tailText;\n\t\t}\n\n\t\tconst firstNewline = this.tailText.indexOf(\"\\n\");\n\t\treturn firstNewline === -1 ? this.tailText : this.tailText.slice(firstNewline + 1);\n\t}\n\n\tprivate shouldUseTempFile(): boolean {\n\t\treturn (\n\t\t\tthis.totalRawBytes > this.maxBytes || this.totalDecodedBytes > this.maxBytes || this.totalLines > this.maxLines\n\t\t);\n\t}\n\n\tprivate ensureTempFile(): void {\n\t\tif (this.tempFilePath) {\n\t\t\treturn;\n\t\t}\n\t\tthis.tempFilePath = defaultTempFilePath(this.tempFilePrefix);\n\t\tthis.tempFileStream = createWriteStream(this.tempFilePath);\n\t\tfor (const chunk of this.rawChunks) {\n\t\t\tthis.tempFileStream.write(chunk);\n\t\t}\n\t\tthis.rawChunks = [];\n\t}\n}\n"]}
1
+ {"version":3,"file":"output-accumulator.js","sourceRoot":"","sources":["../../../src/core/tools/output-accumulator.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport { closeSync, openSync, writeSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, type TruncationResult } from \"./truncate.ts\";\n\nexport interface OutputAccumulatorOptions {\n\tmaxLines?: number;\n\tmaxBytes?: number;\n\ttempFilePrefix?: string;\n}\n\nexport interface OutputSnapshot {\n\tcontent: string;\n\ttruncation: TruncationResult;\n\tfullOutputPath?: string;\n\tfullOutputError?: string;\n}\n\nexport interface OutputPreview {\n\tcontent: string;\n\tskippedLines: number;\n}\n\nfunction defaultTempFilePath(prefix: string): string {\n\tconst id = randomBytes(8).toString(\"hex\");\n\treturn join(tmpdir(), `${prefix}-${id}.log`);\n}\n\nconst MAX_APPEND_CHUNK_BYTES = 64 * 1024;\n\nfunction byteLength(text: string): number {\n\treturn Buffer.byteLength(text, \"utf-8\");\n}\n\nfunction formatIoError(error: unknown): string {\n\tif (error instanceof Error) {\n\t\tconst code = (error as NodeJS.ErrnoException).code;\n\t\treturn code ? `${code}: ${error.message}` : error.message;\n\t}\n\treturn String(error);\n}\n\nfunction tailUtf8String(text: string, maxBytes: number): { text: string; bytes: number } {\n\tif (maxBytes <= 0 || text.length === 0) {\n\t\treturn { text: \"\", bytes: 0 };\n\t}\n\n\tconst buffer = Buffer.from(text, \"utf-8\");\n\tif (buffer.length <= maxBytes) {\n\t\treturn { text, bytes: buffer.length };\n\t}\n\n\tlet start = buffer.length - maxBytes;\n\twhile (start < buffer.length && (buffer[start] & 0xc0) === 0x80) {\n\t\tstart++;\n\t}\n\n\tconst result = buffer.subarray(start).toString(\"utf-8\");\n\treturn { text: result, bytes: byteLength(result) };\n}\n\n/**\n * Incrementally tracks streaming output with bounded memory.\n *\n * Appends decode chunks with a streaming UTF-8 decoder, keeps a bounded tail of\n * logical lines, and opens a temp file when the full output needs preserving.\n * Snapshot and preview work is bounded by configured output limits, never by\n * total command history.\n */\nexport class OutputAccumulator {\n\tprivate readonly maxLines: number;\n\tprivate readonly maxBytes: number;\n\tprivate readonly tempFilePrefix: string;\n\tprivate readonly decoder = new TextDecoder();\n\n\tprivate rawChunks: Buffer[] = [];\n\tprivate tailLines: string[] = [];\n\tprivate tailLineBytes: number[] = [];\n\tprivate tailLineStoredBytes: number[] = [];\n\tprivate tailStart = 0;\n\tprivate tailStoredBytes = 0;\n\tprivate currentLineText = \"\";\n\tprivate currentLineBytes = 0;\n\tprivate currentLineStoredBytes = 0;\n\tprivate lastCompletedLineBytes = 0;\n\tprivate totalRawBytes = 0;\n\tprivate totalDecodedBytes = 0;\n\tprivate completedLines = 0;\n\tprivate totalLines = 0;\n\tprivate hasOpenLine = false;\n\tprivate finished = false;\n\n\tprivate tempFilePath: string | undefined;\n\tprivate tempFileFd: number | undefined;\n\tprivate tempFileError: string | undefined;\n\n\tconstructor(options: OutputAccumulatorOptions = {}) {\n\t\tthis.maxLines = options.maxLines ?? DEFAULT_MAX_LINES;\n\t\tthis.maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;\n\t\tthis.tempFilePrefix = options.tempFilePrefix ?? \"pi-output\";\n\t}\n\n\tappend(data: Buffer): void {\n\t\tif (this.finished) {\n\t\t\tthrow new Error(\"Cannot append to a finished output accumulator\");\n\t\t}\n\n\t\tfor (let offset = 0; offset < data.length; offset += MAX_APPEND_CHUNK_BYTES) {\n\t\t\tthis.appendBlock(data.subarray(offset, offset + MAX_APPEND_CHUNK_BYTES));\n\t\t}\n\t}\n\n\tfinish(): void {\n\t\tif (this.finished) {\n\t\t\treturn;\n\t\t}\n\t\tthis.finished = true;\n\t\tthis.appendDecodedText(this.decoder.decode());\n\t\tif (this.shouldUseTempFile()) {\n\t\t\tthis.tryEnsureTempFile();\n\t\t}\n\t}\n\n\tsnapshot(options: { persistIfTruncated?: boolean } = {}): OutputSnapshot {\n\t\tconst snapshot = this.buildSnapshot(this.maxLines, this.maxBytes);\n\n\t\tif (options.persistIfTruncated && snapshot.truncation.truncated) {\n\t\t\tthis.tryEnsureTempFile();\n\t\t}\n\n\t\treturn {\n\t\t\t...snapshot,\n\t\t\tfullOutputPath: this.fullOutputPath(),\n\t\t\tfullOutputError: this.tempFileError,\n\t\t};\n\t}\n\n\tpreview(maxLines: number, maxBytes = this.maxBytes): OutputPreview {\n\t\tconst snapshot = this.previewSnapshot(maxLines, maxBytes);\n\t\treturn {\n\t\t\tcontent: snapshot.content,\n\t\t\tskippedLines: Math.max(0, this.totalLines - snapshot.truncation.outputLines),\n\t\t};\n\t}\n\n\tpreviewSnapshot(\n\t\tmaxLines: number,\n\t\tmaxBytes = this.maxBytes,\n\t\toptions: { persistIfFullTruncated?: boolean } = {},\n\t): OutputSnapshot {\n\t\tconst snapshot = this.buildSnapshot(maxLines, maxBytes);\n\t\tif (options.persistIfFullTruncated && this.shouldUseTempFile()) {\n\t\t\tthis.tryEnsureTempFile();\n\t\t}\n\t\treturn {\n\t\t\t...snapshot,\n\t\t\tfullOutputPath: this.fullOutputPath(),\n\t\t\tfullOutputError: this.tempFileError,\n\t\t};\n\t}\n\n\tasync closeTempFile(): Promise<void> {\n\t\tconst fd = this.tempFileFd;\n\t\tif (fd === undefined) {\n\t\t\treturn;\n\t\t}\n\t\tthis.tempFileFd = undefined;\n\t\ttry {\n\t\t\tcloseSync(fd);\n\t\t} catch (error) {\n\t\t\tthis.tempFileError ??= formatIoError(error);\n\t\t}\n\t}\n\n\tgetLastLineBytes(): number {\n\t\treturn this.hasOpenLine ? this.currentLineBytes : this.lastCompletedLineBytes;\n\t}\n\n\tprivate appendBlock(data: Buffer): void {\n\t\tthis.totalRawBytes += data.length;\n\t\tthis.appendDecodedText(this.decoder.decode(data, { stream: true }));\n\n\t\tif (this.tempFileFd !== undefined || this.shouldUseTempFile()) {\n\t\t\tif (this.tryEnsureTempFile() && this.tempFileFd !== undefined) {\n\t\t\t\ttry {\n\t\t\t\t\twriteSync(this.tempFileFd, data);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tthis.recordTempFileError(error);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (data.length > 0) {\n\t\t\t// Copy retained chunks: Buffer.subarray would pin a large caller buffer in memory.\n\t\t\tthis.rawChunks.push(Buffer.from(data));\n\t\t}\n\t}\n\n\tprivate appendDecodedText(text: string): void {\n\t\tif (text.length === 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.totalDecodedBytes += byteLength(text);\n\n\t\tlet segmentStart = 0;\n\t\tfor (\n\t\t\tlet newlineIndex = text.indexOf(\"\\n\");\n\t\t\tnewlineIndex !== -1;\n\t\t\tnewlineIndex = text.indexOf(\"\\n\", segmentStart)\n\t\t) {\n\t\t\tthis.appendToCurrentLine(text.slice(segmentStart, newlineIndex));\n\t\t\tthis.pushCompletedCurrentLine();\n\t\t\tsegmentStart = newlineIndex + 1;\n\t\t}\n\n\t\tif (segmentStart < text.length) {\n\t\t\tthis.appendToCurrentLine(text.slice(segmentStart));\n\t\t}\n\n\t\tthis.totalLines = this.completedLines + (this.hasOpenLine ? 1 : 0);\n\t}\n\n\tprivate appendToCurrentLine(segment: string): void {\n\t\tif (segment.length === 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst segmentBytes = byteLength(segment);\n\t\tthis.currentLineBytes += segmentBytes;\n\t\tthis.hasOpenLine = true;\n\n\t\tif (segmentBytes >= this.maxBytes) {\n\t\t\tconst tail = tailUtf8String(segment, this.maxBytes);\n\t\t\tthis.currentLineText = tail.text;\n\t\t\tthis.currentLineStoredBytes = tail.bytes;\n\t\t\treturn;\n\t\t}\n\n\t\tthis.currentLineText += segment;\n\t\tthis.currentLineStoredBytes += segmentBytes;\n\t\tif (this.currentLineStoredBytes > this.maxBytes) {\n\t\t\tconst tail = tailUtf8String(this.currentLineText, this.maxBytes);\n\t\t\tthis.currentLineText = tail.text;\n\t\t\tthis.currentLineStoredBytes = tail.bytes;\n\t\t}\n\t}\n\n\tprivate pushCompletedCurrentLine(): void {\n\t\tthis.completedLines++;\n\t\tthis.lastCompletedLineBytes = this.currentLineBytes;\n\t\tthis.tailLines.push(this.currentLineText);\n\t\tthis.tailLineBytes.push(this.currentLineBytes);\n\t\tthis.tailLineStoredBytes.push(this.currentLineStoredBytes);\n\t\tthis.tailStoredBytes += this.currentLineStoredBytes;\n\t\tthis.currentLineText = \"\";\n\t\tthis.currentLineBytes = 0;\n\t\tthis.currentLineStoredBytes = 0;\n\t\tthis.hasOpenLine = false;\n\t\tthis.trimStoredTail();\n\t}\n\n\tprivate trimStoredTail(): void {\n\t\twhile (this.completedTailLineCount() > this.maxLines || this.tailStoredBytes > this.maxBytes) {\n\t\t\tthis.tailStoredBytes -= this.tailLineStoredBytes[this.tailStart] ?? 0;\n\t\t\tthis.tailStart++;\n\t\t}\n\n\t\tif (this.tailStart > 1024 && this.tailStart * 2 > this.tailLines.length) {\n\t\t\tthis.tailLines = this.tailLines.slice(this.tailStart);\n\t\t\tthis.tailLineBytes = this.tailLineBytes.slice(this.tailStart);\n\t\t\tthis.tailLineStoredBytes = this.tailLineStoredBytes.slice(this.tailStart);\n\t\t\tthis.tailStart = 0;\n\t\t}\n\t}\n\n\tprivate completedTailLineCount(): number {\n\t\treturn this.tailLines.length - this.tailStart;\n\t}\n\n\tprivate buildSnapshot(maxLines: number, maxBytes: number): OutputSnapshot {\n\t\tconst truncated = this.totalLines > maxLines || this.totalDecodedBytes > maxBytes;\n\t\tconst outputLines: string[] = [];\n\t\tlet outputBytes = 0;\n\t\tlet outputLineCount = 0;\n\t\tlet truncatedBy: \"lines\" | \"bytes\" = this.totalLines > maxLines ? \"lines\" : \"bytes\";\n\t\tlet lastLinePartial = false;\n\t\tlet readCurrent = this.hasOpenLine;\n\t\tlet completedIndex = this.tailLines.length - 1;\n\n\t\twhile (outputLineCount < maxLines) {\n\t\t\tlet line: string;\n\t\t\tlet lineBytes: number;\n\t\t\tlet storedBytes: number;\n\t\t\tif (readCurrent) {\n\t\t\t\tline = this.currentLineText;\n\t\t\t\tlineBytes = this.currentLineBytes;\n\t\t\t\tstoredBytes = this.currentLineStoredBytes;\n\t\t\t\treadCurrent = false;\n\t\t\t} else {\n\t\t\t\tif (completedIndex < this.tailStart) break;\n\t\t\t\tline = this.tailLines[completedIndex] ?? \"\";\n\t\t\t\tlineBytes = this.tailLineBytes[completedIndex] ?? 0;\n\t\t\t\tstoredBytes = this.tailLineStoredBytes[completedIndex] ?? 0;\n\t\t\t\tcompletedIndex--;\n\t\t\t}\n\n\t\t\tconst separatorBytes = outputLineCount > 0 ? 1 : 0;\n\t\t\tconst fullLineBytes = lineBytes + separatorBytes;\n\t\t\tif (outputBytes + fullLineBytes > maxBytes) {\n\t\t\t\ttruncatedBy = \"bytes\";\n\t\t\t\tif (outputLineCount === 0) {\n\t\t\t\t\tconst partial =\n\t\t\t\t\t\tlineBytes > maxBytes ? tailUtf8String(line, maxBytes) : { text: line, bytes: storedBytes };\n\t\t\t\t\toutputLines.unshift(partial.text);\n\t\t\t\t\toutputBytes = partial.bytes;\n\t\t\t\t\toutputLineCount = 1;\n\t\t\t\t\tlastLinePartial = lineBytes > partial.bytes;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\toutputLines.unshift(line);\n\t\t\toutputBytes += storedBytes + separatorBytes;\n\t\t\toutputLineCount++;\n\t\t}\n\n\t\tlet content = outputLines.join(\"\\n\");\n\t\tif (!truncated && !this.hasOpenLine && this.totalLines > 0) {\n\t\t\tcontent += \"\\n\";\n\t\t\toutputBytes += 1;\n\t\t}\n\n\t\tconst effectiveTruncatedBy = truncated ? truncatedBy : null;\n\t\tconst truncation: TruncationResult = {\n\t\t\tcontent,\n\t\t\ttruncated,\n\t\t\ttruncatedBy: effectiveTruncatedBy,\n\t\t\ttotalLines: this.totalLines,\n\t\t\ttotalBytes: this.totalDecodedBytes,\n\t\t\toutputLines: outputLineCount,\n\t\t\toutputBytes,\n\t\t\tlastLinePartial,\n\t\t\tfirstLineExceedsLimit: false,\n\t\t\tmaxLines,\n\t\t\tmaxBytes,\n\t\t};\n\n\t\treturn {\n\t\t\tcontent,\n\t\t\ttruncation,\n\t\t\tfullOutputPath: this.fullOutputPath(),\n\t\t\tfullOutputError: this.tempFileError,\n\t\t};\n\t}\n\n\tprivate shouldUseTempFile(): boolean {\n\t\treturn (\n\t\t\tthis.totalRawBytes > this.maxBytes || this.totalDecodedBytes > this.maxBytes || this.totalLines > this.maxLines\n\t\t);\n\t}\n\n\tprivate fullOutputPath(): string | undefined {\n\t\treturn this.tempFileError === undefined ? this.tempFilePath : undefined;\n\t}\n\n\tprivate tryEnsureTempFile(): boolean {\n\t\tif (this.tempFileError !== undefined) {\n\t\t\treturn false;\n\t\t}\n\t\tif (this.tempFileFd !== undefined) {\n\t\t\treturn true;\n\t\t}\n\t\ttry {\n\t\t\tthis.tempFilePath ??= defaultTempFilePath(this.tempFilePrefix);\n\t\t\tthis.tempFileFd = openSync(this.tempFilePath, \"w\");\n\t\t\tfor (const chunk of this.rawChunks) {\n\t\t\t\twriteSync(this.tempFileFd, chunk);\n\t\t\t}\n\t\t\tthis.rawChunks = [];\n\t\t\treturn true;\n\t\t} catch (error) {\n\t\t\tthis.recordTempFileError(error);\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tprivate recordTempFileError(error: unknown): void {\n\t\tthis.tempFileError ??= formatIoError(error);\n\t\tconst fd = this.tempFileFd;\n\t\tthis.tempFileFd = undefined;\n\t\tif (fd !== undefined) {\n\t\t\ttry {\n\t\t\t\tcloseSync(fd);\n\t\t\t} catch (closeError) {\n\t\t\t\tthis.tempFileError += `; close failed: ${formatIoError(closeError)}`;\n\t\t\t}\n\t\t}\n\t}\n}\n"]}