From 683d470fe4ac3de30fa42222eba632615208bf19 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Thu, 24 Sep 2026 00:20:52 -0500 Subject: [PATCH] fix(core): report read length and cap limits and long lines (#51011) --- packages/core/src/tool/plugin/read.ts | 2 +- packages/core/src/tool/read-filesystem.ts | 29 ++++++++++++++----- .../core/test/tool-read-filesystem.test.ts | 2 +- packages/core/test/tool-read.test.ts | 5 +++- 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/packages/core/src/tool/plugin/read.ts b/packages/core/src/tool/plugin/read.ts index 541889d8dcb..cc0f7b0a8b6 100644 --- a/packages/core/src/tool/plugin/read.ts +++ b/packages/core/src/tool/plugin/read.ts @@ -20,7 +20,7 @@ const LocationInput = Schema.Struct({ description: "The line or directory entry to start reading from (1-based)", }), limit: ReadToolFileSystem.PageInput.fields.limit.annotate({ - description: "The maximum number of lines or directory entries to read (defaults to 2000)", + description: "The maximum number of lines or directory entries to read (defaults to and capped at 2000)", }), }) export const Input = LocationInput diff --git a/packages/core/src/tool/read-filesystem.ts b/packages/core/src/tool/read-filesystem.ts index abafaea32b9..e4b3b56eb8c 100644 --- a/packages/core/src/tool/read-filesystem.ts +++ b/packages/core/src/tool/read-filesystem.ts @@ -42,10 +42,10 @@ export class MediaIngestLimitError extends Schema.TaggedError()( "ReadTool.OffsetOutOfRangeError", - { offset: Schema.Number }, + { offset: Schema.Number, lines: Schema.Number }, ) { override get message() { - return `Offset ${this.offset} is out of range` + return `Offset ${this.offset} is out of range for this file (${this.lines} ${this.lines === 1 ? "line" : "lines"})` } } @@ -68,7 +68,7 @@ export type ReadError = export const PageInput = Schema.Struct({ offset: Schema.optionalKey(NonNegativeInt), - limit: Schema.optionalKey(NonNegativeInt.check(Schema.isLessThanOrEqualTo(MAX_READ_LINES))), + limit: Schema.optionalKey(NonNegativeInt), }) export type PageInput = typeof PageInput.Type @@ -155,7 +155,7 @@ export const read = Effect.fn("ReadTool.read")(function* ( type: "file" as const, uri: pathToFileURL(input).href, name: path.basename(input), - content: new TextDecoder().decode(first.bytes), + content: new TextDecoder().decode(first.bytes).split("\n").map(clampLine).join("\n"), encoding: "utf8" as const, mime: mimeType(input), } @@ -164,7 +164,13 @@ export const read = Effect.fn("ReadTool.read")(function* ( if (first.bytes.length >= first.info.size) { const result = textPage(first.bytes, true, page) if (result === undefined) return yield* Effect.die("Read page did not settle for a complete first chunk") - return yield* makeTextPage(input, resource, result, first.bytes.subarray(0, result.consumed).includes(0)) + return yield* makeTextPage( + input, + resource, + result, + first.bytes.subarray(0, result.consumed).includes(0), + lineCount(textLeaf(first.bytes).summary.lines, first.bytes.at(-1)), + ) } const offset = page.offset || 1 @@ -201,7 +207,7 @@ export const read = Effect.fn("ReadTool.read")(function* ( checked += leaf.summary.bytes return length > 0 && leaf.bytes.subarray(0, length).includes(0) }) - return yield* makeTextPage(input, resource, translated, binary) + return yield* makeTextPage(input, resource, translated, binary, lineCount(lines, leaves.at(-1)?.bytes.at(-1))) } } @@ -234,10 +240,11 @@ const makeTextPage = Effect.fnUntraced(function* ( resource: string, result: NonNullable>, binary: boolean, + lines: number, ) { if (binary) return yield* new BinaryFileError({ resource }) if (result.entries.length === 0 && result.offset !== 1) - return yield* new OffsetOutOfRangeError({ offset: result.offset }) + return yield* new OffsetOutOfRangeError({ offset: result.offset, lines }) return new TextPage({ type: "text-page", content: result.entries.join("\n"), @@ -298,7 +305,7 @@ const textPage = (bytes: Uint8Array, eof: boolean, page: PageInput) => { next = line break } - const text = value.length > MAX_LINE_LENGTH ? value.slice(0, MAX_LINE_LENGTH) + MAX_LINE_SUFFIX : value + const text = clampLine(value) const lineSize = Buffer.byteLength(text, "utf-8") + (entries.length > 0 ? 1 : 0) if (size + lineSize > MAX_READ_BYTES) { next = line @@ -316,6 +323,12 @@ const textPage = (bytes: Uint8Array, eof: boolean, page: PageInput) => { return { entries, offset, next, consumed } } +const clampLine = (line: string) => + line.length > MAX_LINE_LENGTH ? line.slice(0, MAX_LINE_LENGTH) + MAX_LINE_SUFFIX : line + +// A trailing newline ends the last line instead of starting an empty one. +const lineCount = (newlines: number, last: number | undefined) => newlines + (last === undefined || last === 10 ? 0 : 1) + type TextSummary = { readonly bytes: number; readonly lines: number } // Request-local augmented rope. Subtree byte and newline weights locate a line // like an order-statistic query without repeatedly decoding the accumulated text. diff --git a/packages/core/test/tool-read-filesystem.test.ts b/packages/core/test/tool-read-filesystem.test.ts index 7904468f39e..184e0448a5f 100644 --- a/packages/core/test/tool-read-filesystem.test.ts +++ b/packages/core/test/tool-read-filesystem.test.ts @@ -223,7 +223,7 @@ describe("ReadToolFileSystem", () => { ) expect(error).toBeInstanceOf(ReadToolFileSystem.OffsetOutOfRangeError) - expect(error.message).toBe("Offset 2 is out of range") + expect(error.message).toBe("Offset 2 is out of range for this file (1 line)") }), ) diff --git a/packages/core/test/tool-read.test.ts b/packages/core/test/tool-read.test.ts index f24c5c4aab9..04ff8c522e8 100644 --- a/packages/core/test/tool-read.test.ts +++ b/packages/core/test/tool-read.test.ts @@ -577,7 +577,10 @@ describe("ReadTool", () => { Effect.gen(function* () { const registry = yield* Tool.Service for (const [error, message] of [ - [new ReadToolFileSystem.OffsetOutOfRangeError({ offset: 10 }), "Offset 10 is out of range"], + [ + new ReadToolFileSystem.OffsetOutOfRangeError({ offset: 10, lines: 3 }), + "Offset 10 is out of range for this file (3 lines)", + ], [ new ReadToolFileSystem.PathKindError({ resource: "socket", expected: "a file" }), "Path is not a file: socket",