Skip to content

Commit 15165ab

Browse files
committed
improvement(copilot): align the VFS read caps and share the read placeholders
Follow-up review of the bounded image read. Three consistency defects and one shared-definition gap, none of which changed what the limits protect against. - derive the image source cap from the FormData upload ceiling instead of a number of its own. That cap exists for the same failure mode — a route holding an entire file in worker memory — so anything uploadable stays readable, and the tighter value was refusing images that read fine before for no gain - report a HEIF past the WebAssembly transcoder's own ceiling as a size refusal; it was falling through and telling the model the file could not be decoded - classify an oversized document like an oversized file or image. One of the three size refusals was reported to the model as a successful one-line read - report the observed size, not the recorded one, when the download cap trips — the recorded size is the figure that cap exists to distrust - move the read placeholders into one module that builds and matches them. Producers and matchers sat in four files and had already drifted twice - stop emitting trace outcomes absent from the generated contract, and derive the "vision limit" wording from the constant instead of restating it
1 parent c11bfe6 commit 15165ab

9 files changed

Lines changed: 259 additions & 109 deletions

File tree

apps/sim/lib/copilot/tools/handlers/vfs.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,24 @@ describe('vfs handlers oversize policy', () => {
196196
expect(result.error).toContain('too large')
197197
})
198198

199+
it('returns an undecodable image placeholder as content, not as a size failure', async () => {
200+
const vfs = makeVfs()
201+
// Not a size problem — the bytes were read fine and the reason is already in the
202+
// message, so the model should see it rather than a "too large, use grep" error.
203+
vfs.readFileContent.mockResolvedValue({
204+
content: '[Image unavailable: bomb.png (90 Bytes). It is too large to decode safely.]',
205+
totalLines: 1,
206+
})
207+
getOrMaterializeVFS.mockResolvedValue(vfs)
208+
209+
const result = await executeVfsRead(
210+
{ path: 'files/bomb.png/content' },
211+
{ userId: 'user-1', workflowId: 'wf-1', workspaceId: 'ws-1' }
212+
)
213+
214+
expect(result.success).toBe(true)
215+
})
216+
199217
it('reads canonical file leaf metadata without fetching dynamic content', async () => {
200218
const vfs = makeVfs()
201219
vfs.read.mockReturnValue({

apps/sim/lib/copilot/tools/handlers/vfs.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { getOrMaterializeVFS } from '@/lib/copilot/vfs'
88
import type { GrepCountEntry, GrepMatch } from '@/lib/copilot/vfs/operations'
99
import { WorkspaceFileGrepError } from '@/lib/copilot/vfs/operations'
1010
import { encodeVfsSegment } from '@/lib/copilot/vfs/path-utils'
11+
import { isOversizedReadPlaceholder } from '@/lib/copilot/vfs/read-placeholders'
1112
import {
1213
importWorkspaceFileSecretProvenanceForModelView,
1314
type WorkspaceFileSecretProvenanceIdentity,
@@ -76,14 +77,6 @@ function serializedResultSize(value: unknown): number {
7677
}
7778
}
7879

79-
function isOversizedReadPlaceholder(content: string): boolean {
80-
return (
81-
content.startsWith('[File too large to display inline:') ||
82-
content.startsWith('[Image too large to read inline:') ||
83-
content.startsWith('[Compiled artifact too large:')
84-
)
85-
}
86-
8780
function hasModelAttachment(result: unknown): boolean {
8881
if (!result || typeof result !== 'object') {
8982
return false

apps/sim/lib/copilot/vfs/file-reader.test.ts

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@ vi.mock('@/lib/uploads/contexts/workspace/workspace-file-manager', () => ({
1414
fetchWorkspaceFileBuffer,
1515
}))
1616

17-
import { readFileRecord } from '@/lib/copilot/vfs/file-reader'
17+
import {
18+
MAX_IMAGE_READ_BYTES,
19+
MAX_IMAGE_SOURCE_BYTES,
20+
readFileRecord,
21+
} from '@/lib/copilot/vfs/file-reader'
1822
import { PayloadSizeLimitError } from '@/lib/core/utils/stream-limits'
19-
20-
const MAX_IMAGE_READ_BYTES = 5 * 1024 * 1024
21-
const MAX_IMAGE_SOURCE_BYTES = 25 * 1024 * 1024
23+
import { MAX_TRANSCODE_INPUT_BYTES } from '@/lib/uploads/server/heic'
2224

2325
async function makeNoisePng(width: number, height: number): Promise<Buffer> {
2426
const sharp = (await import('sharp')).default
@@ -89,17 +91,34 @@ describe('readFileRecord', () => {
8991
SHARP_TEST_TIMEOUT_MS
9092
)
9193

92-
it('reports the too-large placeholder when a understated record.size hides an oversized object', async () => {
93-
// `record.size` is client-declared, so the download cap is the check that holds —
94-
// and breaching it must still read as "too large", not as a failed read.
94+
it('reports the too-large placeholder when an understated record.size hides an oversized object', async () => {
9595
fetchWorkspaceFileBuffer.mockRejectedValue(
96-
new PayloadSizeLimitError({ label: 'workspace file', maxBytes: MAX_IMAGE_SOURCE_BYTES })
96+
new PayloadSizeLimitError({
97+
label: 'workspace file',
98+
maxBytes: MAX_IMAGE_SOURCE_BYTES,
99+
observedBytes: MAX_IMAGE_SOURCE_BYTES + 5_000,
100+
})
97101
)
98102

99103
const result = await readFileRecord(imageRecord('understated.png', 1024))
100104

101105
expect(result?.attachment).toBeUndefined()
102106
expect(result?.content).toContain('Image too large to read inline')
107+
// The observed size, not the understated 1024 the cap exists to distrust.
108+
expect(result?.content).toContain(`${MAX_IMAGE_SOURCE_BYTES + 5_000} bytes`)
109+
})
110+
111+
it('reports an oversized HEIF as a size refusal, not as a corrupt file', async () => {
112+
// `ftyp`+`heic` brand, past the WebAssembly transcoder's own tighter ceiling.
113+
const heif = Buffer.alloc(MAX_TRANSCODE_INPUT_BYTES + 1)
114+
heif.write('ftypheic', 4, 'ascii')
115+
fetchWorkspaceFileBuffer.mockResolvedValue(heif)
116+
117+
const result = await readFileRecord(imageRecord('photo.heic', heif.length, 'image/heic'))
118+
119+
expect(result?.attachment).toBeUndefined()
120+
expect(result?.content).toContain('It is too large to decode safely.')
121+
expect(result?.content).not.toContain('It could not be decoded.')
103122
})
104123

105124
it('rejects an oversized image on its stored size before fetching it', async () => {

0 commit comments

Comments
 (0)