mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-12 17:21:18 +03:00
fix(core): report read length and cap limits and long lines (#51011)
This commit is contained in:
committed by
GitHub
Unverified
parent
ef4938d344
commit
683d470fe4
@@ -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
|
||||
|
||||
@@ -42,10 +42,10 @@ export class MediaIngestLimitError extends Schema.TaggedError<MediaIngestLimitEr
|
||||
|
||||
export class OffsetOutOfRangeError extends Schema.TaggedError<OffsetOutOfRangeError>()(
|
||||
"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<ReturnType<typeof textPage>>,
|
||||
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.
|
||||
|
||||
@@ -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)")
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user