{"record":{"id":"a7bb1185d776602e","repo":"CherryHQ/cherry-studio","slug":"invalid-offset-offset-1-file-has-lines-le","errorCode":null,"errorMessage":"Invalid offset: ${offset + 1}. File has ${lines.length} lines.","messagePattern":"Invalid offset: (.+?)\\. File has (.+?) lines\\.","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/ai/mcp/servers/filesystem/tools/read.ts","lineNumber":67,"sourceCode":"    }\n    throw error\n  }\n\n  // Check if file is binary\n  if (await isBinaryFile(validPath)) {\n    throw new Error(`Cannot read binary file: ${filePath}`)\n  }\n\n  // Read file content\n  const content = await fs.readFile(validPath, 'utf-8')\n  const lines = content.split('\\n')\n\n  // Apply offset and limit\n  const offset = (parsed.data.offset || 1) - 1 // Convert to 0-based\n  const limit = parsed.data.limit || DEFAULT_READ_LIMIT\n\n  if (offset < 0 || offset >= lines.length) {\n    throw new Error(`Invalid offset: ${offset + 1}. File has ${lines.length} lines.`)\n  }\n\n  const selectedLines = lines.slice(offset, offset + limit)\n\n  // Format output with line numbers and truncate long lines\n  const output: string[] = []\n  const relativePath = path.relative(baseDir, validPath)\n\n  output.push(`File: ${relativePath}`)\n  if (offset > 0 || limit < lines.length) {\n    output.push(`Lines ${offset + 1} to ${Math.min(offset + limit, lines.length)} of ${lines.length}`)\n  }\n  output.push('')\n\n  selectedLines.forEach((line, index) => {\n    const lineNumber = offset + index + 1\n    const truncatedLine = line.length > MAX_LINE_LENGTH ? line.substring(0, MAX_LINE_LENGTH) + '...' : line\n    output.push(`${lineNumber.toString().padStart(6)}\\t${truncatedLine}`)","sourceCodeStart":49,"sourceCodeEnd":85,"githubUrl":"https://github.com/CherryHQ/cherry-studio/blob/726446b54cd69ffe51a276638672f6d95ca0768c/src/main/ai/mcp/servers/filesystem/tools/read.ts#L49-L85","documentation":"Thrown by the read tool when the 0-based offset (parsed.data.offset converted from 1-based, defaulting to 0) is negative or greater-than-or-equal to the number of lines. Line count comes from content.split('\\n'), so an empty file yields length 1 (a single empty string). Any offset past the last line, or a negative offset from a bad input, triggers this before slicing.","triggerScenarios":"Calling read with offset larger than the file's line count (e.g. offset=500 on a 10-line file); offset=0 which becomes -1 after the 1-based conversion; reading a near-empty file with offset >= 2. Note offset defaults to 1 (0-based 0), so omitting offset is always safe.","commonSituations":"Caller computed offset from a stale line count (file shrunk since); offset passed as 0 by a caller assuming 0-based indexing; paginating past EOF without tracking the prior response's 'more lines' hint; empty file plus offset >= 2.","solutions":["Cap offset at the file's line count before calling, or omit it to read from the start.","Never pass offset=0; the schema is 1-based, so use offset >= 1 (or omit it for the default of 1).","When paginating, derive the next offset from the previous read response and stop when it reports no more lines."],"exampleFix":"// before\nawait handleReadTool({ file_path: 'small.txt', offset: 500 }, baseDir) // throws: Invalid offset\n\n// after\nawait handleReadTool({ file_path: 'small.txt' }, baseDir) // omit offset, read from start","handlingStrategy":"validation","validationCode":"const MAX = Number.MAX_SAFE_INTEGER\nfunction clampOffset(requested: number | undefined, lineCount: number): number | undefined {\n  if (requested === undefined) return undefined // default of 1 is always safe\n  if (requested < 1) return 1\n  if (requested > lineCount) return lineCount\n  return requested\n}","typeGuard":"function isValidOffset(offset: number | undefined, lineCount: number): boolean {\n  const o = (offset ?? 1) - 1\n  return o >= 0 && o < Math.max(lineCount, 1)\n}","tryCatchPattern":"try {\n  await handleReadTool(args, baseDir)\n} catch (e) {\n  if (e instanceof Error && e.message.startsWith('Invalid offset')) {\n    // retry with offset omitted (read from start)\n  } else throw e\n}","preventionTips":["Never pass offset=0; the schema is 1-based.","Derive the next page offset from the previous read's 'more lines' hint, not from a guessed total.","For very small files, omit offset/limit entirely."],"tags":["filesystem","validation","mcp","read-tool","pagination"],"backgroundTag":null,"analyzedSha":"726446b54cd69ffe51a276638672f6d95ca0768c","analyzedAt":"2026-08-12T17:30:37.448Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}