Skip to content

Commit c2e8091

Browse files
committed
fix(copilot): report the accurate reason and byte size on image rejection
- track whether any resize rung produced an encode, rather than whether any threw: a rung that throws followed by rungs that encode but never fit was reported as undecodable, and the span outcome disagreed with the placeholder - format the rejected size with includeBytes, since formatFileSize collapses anything under 1KB to '0 Bytes' — which is every decompression bomb
1 parent 2f9716b commit c2e8091

2 files changed

Lines changed: 15 additions & 7 deletions

File tree

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ describe('readFileRecord', () => {
8282

8383
expect(result?.attachment).toBeUndefined()
8484
expect(result?.content).toContain('It is too large to decode safely.')
85+
// The byte count must survive formatting — a sub-1KB bomb formatted without
86+
// `includeBytes` collapses to "0 Bytes" next to the real reason.
87+
expect(result?.content).toContain(`(${bomb.length} Bytes)`)
8588
},
8689
SHARP_TEST_TIMEOUT_MS
8790
)

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

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,10 @@ async function prepareImageForVision(
277277
span.setAttribute(TraceAttr.CopilotVfsHasAlpha, hasAlpha)
278278

279279
let attempts = 0
280-
let decodeFailed = false
280+
// Whether any rung got as far as producing an encoded buffer. That, not
281+
// "did anything throw", is what separates "cannot be decoded at all" from
282+
// "decodes fine, just never small enough".
283+
let encodedAny = false
281284
for (const dimension of IMAGE_RESIZE_DIMENSIONS) {
282285
for (const quality of IMAGE_QUALITY_STEPS) {
283286
attempts += 1
@@ -305,6 +308,7 @@ async function prepareImageForVision(
305308
mediaType: 'image/jpeg',
306309
}
307310

311+
encodedAny = true
308312
span.addEvent(TraceEvent.CopilotVfsResizeAttempt, {
309313
[TraceAttr.CopilotVfsResizeDimension]: dimension,
310314
[TraceAttr.CopilotVfsResizeQuality]: quality,
@@ -347,7 +351,6 @@ async function prepareImageForVision(
347351
// rungs re-decode the identical source and only change the encoder, so
348352
// repeating a failed decode there is pure waste. A smaller dimension is
349353
// worth trying — libvips shrinks JPEG on load, so it decodes less.
350-
decodeFailed = true
351354
logger.warn('Failed image resize attempt for VFS read', {
352355
mediaType,
353356
dimension,
@@ -367,13 +370,15 @@ async function prepareImageForVision(
367370
span.setAttributes({
368371
[TraceAttr.CopilotVfsResized]: false,
369372
[TraceAttr.CopilotVfsResizeAttempts]: attempts,
370-
[TraceAttr.CopilotVfsOutcome]: CopilotVfsOutcome.RejectedTooLargeAfterResize,
373+
[TraceAttr.CopilotVfsOutcome]: encodedAny
374+
? CopilotVfsOutcome.RejectedTooLargeAfterResize
375+
: 'rejected_resize_failed',
371376
})
372377
return {
373378
ok: false,
374-
reason: decodeFailed
375-
? VisionImageRejection.Undecodable
376-
: VisionImageRejection.TooLargeAfterResize,
379+
reason: encodedAny
380+
? VisionImageRejection.TooLargeAfterResize
381+
: VisionImageRejection.Undecodable,
377382
}
378383
} catch (err) {
379384
recordSpanError(span, err)
@@ -448,7 +453,7 @@ export async function readFileRecord(record: WorkspaceFileRecord): Promise<FileR
448453
if (!prepared.ok) {
449454
span.setAttribute(TraceAttr.CopilotVfsReadOutcome, CopilotVfsReadOutcome.ImageTooLarge)
450455
return {
451-
content: `[Image unavailable: ${record.name} (${formatFileSize(record.size)}). ${prepared.reason}]`,
456+
content: `[Image unavailable: ${record.name} (${formatFileSize(record.size, { includeBytes: true })}). ${prepared.reason}]`,
452457
totalLines: 1,
453458
}
454459
}

0 commit comments

Comments
 (0)