Skip to content

Commit 19abda3

Browse files
authored
fix(files): read offset 0 as the first line instead of rejecting it (#8588)
* fix(files): read offset 0 as the first line instead of rejecting it * test(files): cover the agent CLI forwarding --offset 0
1 parent c19db87 commit 19abda3

10 files changed

Lines changed: 72 additions & 19 deletions

File tree

‎apps/docs/content/docs/cli/files.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ sim files versions read <fileId> <version> [options]
271271
| Option | Required | Description |
272272
| --- | --- | --- |
273273
| `--max-bytes <value>` | No | Optional ceiling on the source bytes fed to the parser, lowering but never raising the server limit. |
274-
| `--offset <value>` | No | First line to return, 1-based. Absent starts at the first line. |
274+
| `--offset <value>` | No | First line to return, 1-based; 0 also starts at the first line. Absent starts at the first line. |
275275
| `--limit <value>` | No | How many lines to return from `offset`. Absent reads to the end. |
276276

277277
</CommandTable>
@@ -497,7 +497,7 @@ sim files read <fileId> [options]
497497
| Option | Required | Description |
498498
| --- | --- | --- |
499499
| `--max-bytes <value>` | No | Optional ceiling on the source bytes fed to the parser, lowering but never raising the server limit. |
500-
| `--offset <value>` | No | First line to return, 1-based. Absent starts at the first line. |
500+
| `--offset <value>` | No | First line to return, 1-based; 0 also starts at the first line. Absent starts at the first line. |
501501
| `--limit <value>` | No | How many lines to return from `offset`. Absent reads to the end. |
502502

503503
</CommandTable>

‎apps/docs/content/docs/cli/reference.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1039,7 +1039,7 @@ sim files versions read <fileId> <version> [options]
10391039
| Option | Required | Description |
10401040
| --- | --- | --- |
10411041
| `--max-bytes <value>` | No | Optional ceiling on the source bytes fed to the parser, lowering but never raising the server limit. |
1042-
| `--offset <value>` | No | First line to return, 1-based. Absent starts at the first line. |
1042+
| `--offset <value>` | No | First line to return, 1-based; 0 also starts at the first line. Absent starts at the first line. |
10431043
| `--limit <value>` | No | How many lines to return from `offset`. Absent reads to the end. |
10441044

10451045
</CommandTable>
@@ -1281,7 +1281,7 @@ sim files read <fileId> [options]
12811281
| Option | Required | Description |
12821282
| --- | --- | --- |
12831283
| `--max-bytes <value>` | No | Optional ceiling on the source bytes fed to the parser, lowering but never raising the server limit. |
1284-
| `--offset <value>` | No | First line to return, 1-based. Absent starts at the first line. |
1284+
| `--offset <value>` | No | First line to return, 1-based; 0 also starts at the first line. Absent starts at the first line. |
12851285
| `--limit <value>` | No | How many lines to return from `offset`. Absent reads to the end. |
12861286

12871287
</CommandTable>

‎apps/docs/openapi-v2-files-audit.json‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -790,11 +790,11 @@
790790
"name": "offset",
791791
"in": "query",
792792
"required": false,
793-
"description": "First line to return, 1-based. Absent starts at the first line.",
793+
"description": "First line to return, 1-based; 0 also starts at the first line. Absent starts at the first line.",
794794
"schema": {
795-
"description": "First line to return, 1-based. Absent starts at the first line.",
795+
"description": "First line to return, 1-based; 0 also starts at the first line. Absent starts at the first line.",
796796
"type": "integer",
797-
"minimum": 1,
797+
"minimum": 0,
798798
"maximum": 9007199254740991
799799
}
800800
},
@@ -1241,11 +1241,11 @@
12411241
"name": "offset",
12421242
"in": "query",
12431243
"required": false,
1244-
"description": "First line to return, 1-based. Absent starts at the first line.",
1244+
"description": "First line to return, 1-based; 0 also starts at the first line. Absent starts at the first line.",
12451245
"schema": {
1246-
"description": "First line to return, 1-based. Absent starts at the first line.",
1246+
"description": "First line to return, 1-based; 0 also starts at the first line. Absent starts at the first line.",
12471247
"type": "integer",
1248-
"minimum": 1,
1248+
"minimum": 0,
12491249
"maximum": 9007199254740991
12501250
}
12511251
},

‎apps/sim/app/api/v2/files/[fileId]/text/route.test.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,16 @@ describe('GET /api/v2/files/[fileId]/text', () => {
112112
expect(mocks.readText).not.toHaveBeenCalled()
113113
})
114114

115+
/**
116+
* Callers used to zero-based paging send `offset=0` for the first window. It
117+
* names no other line, so it reads from the first one instead of failing.
118+
*/
119+
it('reads offset 0 as the first line', async () => {
120+
const response = await GET(textRequest(`workspaceId=${WORKSPACE_ID}&offset=0&limit=2`), context)
121+
122+
expect(response.status).toBe(200)
123+
})
124+
115125
it('conceals a cross-tenant file as a missing file', async () => {
116126
mocks.readText.mockRejectedValueOnce(new NoWorkspaceAccessError())
117127

‎apps/sim/lib/api/contracts/tools/file.ts‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -209,11 +209,12 @@ export const fileManageContentBodySchema = z
209209
fileId: fileIdSelectionSchema.optional(),
210210
fileInput: z.unknown().optional(),
211211
/**
212-
* First line to return, 1-based. Applied to each selected file separately,
213-
* because a selection can be several files and one running offset across
214-
* them would depend on an ordering the caller cannot see.
212+
* First line to return, 1-based; 0 also starts at the first line, so a
213+
* zero-based caller's first window reads instead of failing. Applied to each
214+
* selected file separately, because a selection can be several files and one
215+
* running offset across them would depend on an ordering the caller cannot see.
215216
*/
216-
offset: z.number().int().min(1, 'offset starts at line 1').optional(),
217+
offset: z.number().int().min(0, 'offset cannot be negative').optional(),
217218
/** How many lines to return from `offset`. Absent reads to the end. */
218219
limit: z.number().int().min(1, 'limit must be at least 1').optional(),
219220
})

‎apps/sim/lib/api/contracts/v2/files.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -853,9 +853,11 @@ export const v2ReadFileTextQuerySchema = z
853853
offset: z.coerce
854854
.number()
855855
.int()
856-
.min(1, 'offset starts at line 1')
856+
.min(0, 'offset cannot be negative')
857857
.optional()
858-
.describe('First line to return, 1-based. Absent starts at the first line.'),
858+
.describe(
859+
'First line to return, 1-based; 0 also starts at the first line. Absent starts at the first line.'
860+
),
859861
limit: z.coerce
860862
.number()
861863
.int()

‎apps/sim/lib/internal/file/operations.test.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -808,6 +808,21 @@ describe('file manage operations', () => {
808808
},
809809
})
810810
})
811+
it('reads offset 0 as the first line of each selected file', async () => {
812+
const response = await POST(
813+
createMockRequest('POST', {
814+
operation: 'content',
815+
workspaceId: 'workspace-1',
816+
fileId: 'file-1',
817+
offset: 0,
818+
limit: 1,
819+
})
820+
)
821+
expect(response.status).toBe(200)
822+
expect(await response.json()).toMatchObject({
823+
data: { contents: ['content:file-1.txt'], lineRanges: [{ offset: 1, lineCount: 1 }] },
824+
})
825+
})
811826
it('does not bypass complete extraction limits with a raw-text fallback', async () => {
812827
vi.mocked(isSupportedFileType).mockReturnValueOnce(true)
813828
vi.mocked(parseBuffer).mockRejectedValueOnce(

‎apps/sim/lib/mothership/agent-cli/engines/file-read.test.ts‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,11 +205,28 @@ describe('content-aware files read augmentation', () => {
205205
}
206206
)
207207

208+
it('forwards --offset 0 to the text API instead of refusing it', async () => {
209+
mockResolveWorkspaceFileReference.mockResolvedValue({
210+
...file,
211+
name: 'notes.txt',
212+
type: 'text/plain',
213+
})
214+
mocks.request.mockResolvedValue(textResponse('notes.txt'))
215+
const result = await runEngine('files read', ['uploads/notes.txt'], runtime, {
216+
offset: '0',
217+
limit: '2',
218+
})
219+
expect(result.exitCode).toBe(0)
220+
expect(mocks.request).toHaveBeenCalledWith(`/api/v2/files/${fileId}/text`, {
221+
query: { workspaceId, offset: '0', limit: '2' },
222+
})
223+
})
224+
208225
it('rejects incompatible flags and invalid byte/line bounds before lookup', async () => {
209226
for (const flags of [
210227
{ render: true, limit: '2' } as const,
211228
{ pages: '1', offset: '1' },
212-
{ offset: '0' },
229+
{ offset: '-1' },
213230
{ offset: true } as const,
214231
{ limit: true } as const,
215232
{ 'max-bytes': true } as const,

‎apps/sim/lib/workspace-files/text-lines.test.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ describe('file text line windows', () => {
2525
}
2626
}
2727
)
28+
it('reads offset 0 as the first line and reports the 1-based offset it used', () => {
29+
expect(sliceFileTextLines('one\ntwo\nthree', 0, 2, false)).toEqual({
30+
text: 'one\ntwo',
31+
lineRange: { offset: 1, lineCount: 2, totalLines: 3, totalLinesExact: true },
32+
})
33+
})
2834
it('reads a late window in a newline-dense document', () => {
2935
const result = sliceFileTextLines(
3036
`${'abc\n'.repeat(1_000_000)}tail needle\n`,

‎packages/sim-cli/src/generated/v2-api.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20276,7 +20276,8 @@ export const V2_OPERATIONS = {
2027620276
},
2027720277
offset: {
2027820278
kind: 'integer',
20279-
describe: 'First line to return, 1-based. Absent starts at the first line.',
20279+
describe:
20280+
'First line to return, 1-based; 0 also starts at the first line. Absent starts at the first line.',
2028020281
},
2028120282
limit: {
2028220283
kind: 'integer',
@@ -20300,7 +20301,8 @@ export const V2_OPERATIONS = {
2030020301
},
2030120302
offset: {
2030220303
kind: 'integer',
20303-
describe: 'First line to return, 1-based. Absent starts at the first line.',
20304+
describe:
20305+
'First line to return, 1-based; 0 also starts at the first line. Absent starts at the first line.',
2030420306
},
2030520307
limit: {
2030620308
kind: 'integer',

0 commit comments

Comments
 (0)