{"record":{"id":"7ed3563221e1503f","repo":"earendil-works/pi","slug":"offset-offset-is-beyond-end-of-file-alllines","errorCode":null,"errorMessage":"Offset ${offset} is beyond end of file (${allLines.length} lines total)","messagePattern":"Offset (.+?) is beyond end of file \\((.+?) lines total\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/agent/src/harness/tools/read.ts","lineNumber":103,"sourceCode":"\t\t\t\t\t\tdetails: undefined,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{ type: \"text\", text: `Read image file [${mimeType}]` },\n\t\t\t\t\t\t{ type: \"image\", data: encodeBase64(bytes), mimeType },\n\t\t\t\t\t] satisfies Array<TextContent | ImageContent>,\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst textContent = new TextDecoder().decode(bytes);\n\t\t\tconst allLines = textContent.split(\"\\n\");\n\t\t\tconst totalFileLines = allLines.length;\n\t\t\tconst startLine = offset ? Math.max(0, offset - 1) : 0;\n\t\t\tconst startLineDisplay = startLine + 1;\n\t\t\tif (startLine >= allLines.length) {\n\t\t\t\tthrow new Error(`Offset ${offset} is beyond end of file (${allLines.length} lines total)`);\n\t\t\t}\n\n\t\t\tlet selectedContent: string;\n\t\t\tlet userLimitedLines: number | undefined;\n\t\t\tif (limit !== undefined) {\n\t\t\t\tconst endLine = Math.min(startLine + limit, allLines.length);\n\t\t\t\tselectedContent = allLines.slice(startLine, endLine).join(\"\\n\");\n\t\t\t\tuserLimitedLines = endLine - startLine;\n\t\t\t} else {\n\t\t\t\tselectedContent = allLines.slice(startLine).join(\"\\n\");\n\t\t\t}\n\n\t\t\tconst truncation = truncateHead(selectedContent);\n\t\t\tlet outputText: string;\n\t\t\tlet details: ReadToolDetails | undefined;\n\t\t\tif (truncation.firstLineExceedsLimit) {\n\t\t\t\tconst firstLineSize = formatSize(new TextEncoder().encode(allLines[startLine]).byteLength);\n\t\t\t\toutputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Use bash: sed -n '${startLineDisplay}p' ${path} | head -c ${DEFAULT_MAX_BYTES}]`;","sourceCodeStart":85,"sourceCodeEnd":121,"githubUrl":"https://github.com/earendil-works/pi/blob/4af9d21d3b4d664e4a29fcabfec85171077248e3/packages/agent/src/harness/tools/read.ts#L85-L121","documentation":"The read tool validates its 1-indexed offset against the line count from splitting the decoded file on \\n (which includes a trailing empty element when the file ends with a newline, and yields length 1 for an empty file). If offset-1 >= that count, the read cannot start and the tool throws with the real total. It is a pagination mistake by the caller or model, not an environment fault; note the trailing element makes the check slightly lenient, so an off-by-one often returns an empty line instead of throwing, and one more triggers this error.","triggerScenarios":"Passing offset larger than the file's line count (offset 50 on a 20-line file); continuing a truncated read with a next-offset computed from an older, longer version of the file; offset >= 2 on an empty file (split of empty string has length 1).","commonSituations":"Agent loops that resume reading with a stale 'Use offset=N to continue' hint after the file was rewritten or truncated; hardcoded offsets in scripts; files that shrink between reads because another process or tool rewrote them.","solutions":["Re-read without offset (or offset 1) to see the file's current contents and real length","Derive the next offset from the tool's own truncation hint in the latest read, never from stale history","Clamp the requested offset to the reported total before retrying","If the file should have been longer, check whether a concurrent write or truncate changed it under you"],"exampleFix":"// before\nconst res = await readTool.execute(id, { path, offset: nextOffset }, signal, undefined, ctx);\n\n// after - clamp offset against the file's current line count\nconst r = await env.readTextFile(absPath, signal);\nconst total = r.ok ? r.value.split(\"\\n\").length : 0;\nconst offset = nextOffset && nextOffset > total ? undefined : nextOffset;\nconst res = await readTool.execute(id, { path, offset }, signal, undefined, ctx);","handlingStrategy":"validation","validationCode":"const r = await env.readTextFile(absolutePath, signal);\nif (r.ok) {\n  const totalLines = r.value.split(\"\\n\").length;\n  if (offset !== undefined && offset - 1 >= totalLines) {\n    offset = undefined; // or Math.max(1, Math.min(offset, totalLines))\n  }\n}\nawait readTool.execute(id, { path, offset, limit }, signal, undefined, ctx);","typeGuard":"const isValidReadOffset = (offset: number | undefined, totalLines: number): boolean =>\n  offset === undefined || (Number.isInteger(offset) && offset >= 1 && offset - 1 < totalLines);","tryCatchPattern":"try {\n  return await readTool.execute(id, input, signal, undefined, ctx);\n} catch (e) {\n  if (e instanceof Error && /Offset \\d+ is beyond end of file/.test(e.message)) {\n    return readTool.execute(id, { ...input, offset: undefined }, signal, undefined, ctx);\n  }\n  throw e;\n}","preventionTips":["Always derive continuation offsets from the read tool's own 'Use offset=N' hint in the same response","Re-read from offset 1 whenever a file may have changed between reads","Treat an unexpected line-count shrink as a signal that something rewrote the file"],"tags":["read-tool","offset","out-of-bounds","line-numbers","pagination"],"backgroundTag":"offset-out-of-range","analyzedSha":"4af9d21d3b4d664e4a29fcabfec85171077248e3","analyzedAt":"2026-08-24T13:07:14.692Z","schemaVersion":2},"datasetVersion":"2026-08-24T17:17:21.512Z"}