Summary
Attaching an image in a format the Photon-based resizer cannot decode (e.g. AVIF, HEIC, BMP, TIFF) — or an image too large to be resized below the inline limit — makes the entire prompt request fail with a 400 instead of degrading gracefully.
Expected Behavior
A single undecodable/oversized image attachment should not abort the whole message. It should be dropped (or replaced with a text note) while the rest of the prompt still sends — exactly like the tool-result path does today.
Current Behavior
packages/opencode/src/session/prompt.ts (user-message path, createUserMessage) runs image.normalize(part) inside a plain Effect.forEach that only catches Image.ResizerUnavailableError:
const parts = yield* Effect.forEach(resolvedParts, (part) =>
part.type === "file" && part.mime.startsWith("image/")
? image.normalize(part).pipe(
Effect.catchIf(
(error) => error instanceof Image.ResizerUnavailableError,
() => Effect.succeed(part),
),
)
: Effect.succeed(part),
)
Effect.forEach fails fast. If image.normalize throws Image.DecodeError (unsupported format) or Image.SizeError (cannot be resized below max_base64_bytes), the whole createUserMessage effect fails → promptSvc.prompt fails → the HTTP handler (httpapi/handlers/session.ts) maps it to a 400 Bad Request. The image message can never be sent.
Verified with the real @silvia-odwyer/photon-node@0.3.4 dependency: PhotonImage.new_from_byteslice throws for AVIF, HEIC, and BMP byte inputs, while PNG/JPEG/WebP decode fine.
The tool-result path (processor.ts) already handles this correctly with Effect.exit + filtering + an [N image omitted...] note. The user-message path is the asymmetric one.
Steps to Reproduce
- In the TUI, paste a local
.avif (or .heic / .bmp) image path — the TUI attachment picker explicitly accepts .avif (packages/tui/src/component/prompt/local-attachment.ts).
- Send the prompt with the attached image.
- The request fails with HTTP 400; the message is never sent.
Impact
Any user with a modern camera/phone screenshot (HEIC) or an AVIF screenshot, or any image that cannot be compressed under the 5MB inline limit, hits a hard failure with no way to send the message at all.
Suggested Fix
Mirror the processor.ts pattern: catch normalize failures per-part (Effect.exit / Effect.catchAll), drop or replace the failing image with a synthetic text note, and keep the prompt alive.
Summary
Attaching an image in a format the Photon-based resizer cannot decode (e.g. AVIF, HEIC, BMP, TIFF) — or an image too large to be resized below the inline limit — makes the entire prompt request fail with a 400 instead of degrading gracefully.
Expected Behavior
A single undecodable/oversized image attachment should not abort the whole message. It should be dropped (or replaced with a text note) while the rest of the prompt still sends — exactly like the tool-result path does today.
Current Behavior
packages/opencode/src/session/prompt.ts(user-message path,createUserMessage) runsimage.normalize(part)inside a plainEffect.forEachthat only catchesImage.ResizerUnavailableError:Effect.forEachfails fast. Ifimage.normalizethrowsImage.DecodeError(unsupported format) orImage.SizeError(cannot be resized belowmax_base64_bytes), the wholecreateUserMessageeffect fails →promptSvc.promptfails → the HTTP handler (httpapi/handlers/session.ts) maps it to a 400 Bad Request. The image message can never be sent.Verified with the real
@silvia-odwyer/photon-node@0.3.4dependency:PhotonImage.new_from_byteslicethrows for AVIF, HEIC, and BMP byte inputs, while PNG/JPEG/WebP decode fine.The tool-result path (
processor.ts) already handles this correctly withEffect.exit+ filtering + an[N image omitted...]note. The user-message path is the asymmetric one.Steps to Reproduce
.avif(or.heic/.bmp) image path — the TUI attachment picker explicitly accepts.avif(packages/tui/src/component/prompt/local-attachment.ts).Impact
Any user with a modern camera/phone screenshot (HEIC) or an AVIF screenshot, or any image that cannot be compressed under the 5MB inline limit, hits a hard failure with no way to send the message at all.
Suggested Fix
Mirror the
processor.tspattern: catch normalize failures per-part (Effect.exit/Effect.catchAll), drop or replace the failing image with a synthetic text note, and keep the prompt alive.