From df3e34ce241b6feef33a5da3b64a429f293fcb44 Mon Sep 17 00:00:00 2001 From: Bil0000 <62337003+Bil0000@users.noreply.github.com> Date: Thu, 24 Sep 2026 15:34:18 +0200 Subject: [PATCH 1/2] fix(contracts): read large file attachments from newer clients --- packages/contracts/src/orchestration.test.ts | 13 +++++-------- packages/contracts/src/orchestration.ts | 5 +---- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/contracts/src/orchestration.test.ts b/packages/contracts/src/orchestration.test.ts index 7c7bade4502e..caa56e89a976 100644 --- a/packages/contracts/src/orchestration.test.ts +++ b/packages/contracts/src/orchestration.test.ts @@ -35,7 +35,6 @@ import { ThreadTurnStartRequestedPayload, SnapShotAccessibility, isProviderSendTurnSupportedImageMimeType, - PROVIDER_SEND_TURN_MAX_FILE_BYTES, } from "./orchestration.ts"; import { ProviderInstanceId } from "./providerInstance.ts"; @@ -358,9 +357,6 @@ it.effect("tolerates attachment types from newer builds when decoding messages", }), ); -// The tolerant member must not catch malformed known attachments: a file over -// the size cap or an image with a bad mime has to fail its own schema, not -// slide through the open one with those constraints unchecked. it.effect("rejects malformed known attachment types instead of tolerating them", () => Effect.gen(function* () { const base = { @@ -380,10 +376,11 @@ it.effect("rejects malformed known attachment types instead of tolerating them", updatedAt: "2026-01-01T00:00:00.000Z", }); - const oversizedFile = yield* Effect.exit( - decode({ ...base, type: "file", sizeBytes: PROVIDER_SEND_TURN_MAX_FILE_BYTES + 1 }), - ); - assert.strictEqual(Exit.isFailure(oversizedFile), true); + const largeFile = yield* decode({ ...base, type: "file", sizeBytes: 75_000_000 }); + assert.strictEqual(largeFile.attachments?.[0]?.sizeBytes, 75_000_000); + + const emptyFile = yield* Effect.exit(decode({ ...base, type: "file", sizeBytes: 0 })); + assert.strictEqual(Exit.isFailure(emptyFile), true); const badMimeImage = yield* Effect.exit( decode({ ...base, type: "image", mimeType: "application/pdf", sizeBytes: 12 }), diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts index 3e323e4964d5..4b93a13359ea 100644 --- a/packages/contracts/src/orchestration.ts +++ b/packages/contracts/src/orchestration.ts @@ -318,10 +318,7 @@ export const ChatFileAttachment = Schema.Struct({ id: ChatAttachmentId, name: TrimmedNonEmptyString.check(Schema.isMaxLength(255)), mimeType: TrimmedNonEmptyString.check(Schema.isMaxLength(100)), - sizeBytes: NonNegativeInt.check( - Schema.isGreaterThanOrEqualTo(1), - Schema.isLessThanOrEqualTo(PROVIDER_SEND_TURN_MAX_FILE_BYTES), - ), + sizeBytes: NonNegativeInt.check(Schema.isGreaterThanOrEqualTo(1)), /** Clipboard text folded by a client. Providers keep these path-only so the agent can inspect the file selectively instead of eagerly spending the same context the fold is intended to preserve. */ From b7bf8c9b9a4783e6d65c20749fa65e63f580df15 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 25 Sep 2026 13:40:21 -0700 Subject: [PATCH 2/2] test(contracts): pin file attachment reads above the upload cap The reader regression test decoded a fixed 75 MB file. Once the upload cap rises to 100 MB that value sits under the cap and no longer proves that a reader tolerates files a newer build accepted. Derive it from the cap, and note on the schema why sizeBytes has no upper bound. Co-Authored-By: Claude Opus 5.5 (1M context) --- packages/contracts/src/orchestration.test.ts | 7 +++++-- packages/contracts/src/orchestration.ts | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/contracts/src/orchestration.test.ts b/packages/contracts/src/orchestration.test.ts index caa56e89a976..091922d15e3f 100644 --- a/packages/contracts/src/orchestration.test.ts +++ b/packages/contracts/src/orchestration.test.ts @@ -35,6 +35,7 @@ import { ThreadTurnStartRequestedPayload, SnapShotAccessibility, isProviderSendTurnSupportedImageMimeType, + PROVIDER_SEND_TURN_MAX_FILE_BYTES, } from "./orchestration.ts"; import { ProviderInstanceId } from "./providerInstance.ts"; @@ -376,8 +377,10 @@ it.effect("rejects malformed known attachment types instead of tolerating them", updatedAt: "2026-01-01T00:00:00.000Z", }); - const largeFile = yield* decode({ ...base, type: "file", sizeBytes: 75_000_000 }); - assert.strictEqual(largeFile.attachments?.[0]?.sizeBytes, 75_000_000); + // A newer build may raise the upload cap; this build must still read those files. + const aboveUploadCap = PROVIDER_SEND_TURN_MAX_FILE_BYTES + 1; + const largeFile = yield* decode({ ...base, type: "file", sizeBytes: aboveUploadCap }); + assert.strictEqual(largeFile.attachments?.[0]?.sizeBytes, aboveUploadCap); const emptyFile = yield* Effect.exit(decode({ ...base, type: "file", sizeBytes: 0 })); assert.strictEqual(Exit.isFailure(emptyFile), true); diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts index 4b93a13359ea..9f73f668b290 100644 --- a/packages/contracts/src/orchestration.ts +++ b/packages/contracts/src/orchestration.ts @@ -318,6 +318,9 @@ export const ChatFileAttachment = Schema.Struct({ id: ChatAttachmentId, name: TrimmedNonEmptyString.check(Schema.isMaxLength(255)), mimeType: TrimmedNonEmptyString.check(Schema.isMaxLength(100)), + /** No upper bound: history and thread streams can carry files that a newer + build accepted under a higher limit. `AttachmentCreateUploadUrlInput` + enforces the upload cap. */ sizeBytes: NonNegativeInt.check(Schema.isGreaterThanOrEqualTo(1)), /** Clipboard text folded by a client. Providers keep these path-only so the agent can inspect the file selectively instead of eagerly spending the