@@ -37,6 +37,7 @@ import { Snapshot } from "./snapshot"
3737import { SessionRevert } from "./session/revert"
3838import { Session } from "@opencode-ai/schema/session"
3939import { FSUtil } from "./fs-util"
40+ import { Image } from "./image"
4041import { Mime } from "./mime"
4142import type { EventLog } from "@opencode-ai/schema/event-log"
4243import { SkillV2 } from "./skill"
@@ -531,9 +532,13 @@ const layer = Layer.effect(
531532 // continues from the reverted boundary rather than stale post-boundary history.
532533 if ( session . revert )
533534 yield * SessionRevert . commit ( session ) . pipe ( Effect . provideService ( EventV2 . Service , events ) )
534- const prompt = yield * resolvePrompt ( { text : input . text , files : input . files , agents : input . agents } ) . pipe (
535- Effect . provideService ( FSUtil . Service , fs ) ,
536- )
535+ // Resolved lazily so prompt admission only boots location services when an
536+ // image attachment actually needs the resizer.
537+ const image = Image . Service . pipe ( Effect . provide ( locations . get ( session . location ) ) )
538+ const prompt = yield * resolvePrompt (
539+ { text : input . text , files : input . files , agents : input . agents } ,
540+ image ,
541+ ) . pipe ( Effect . provideService ( FSUtil . Service , fs ) )
537542 const messageID = input . id ?? SessionMessage . ID . create ( )
538543 const admittedInput = SessionPending . Message . make ( {
539544 type : "user" ,
@@ -859,10 +864,13 @@ function synthesizeTerminalShellInfo(started: ShellSchema.Info): ShellSchema.Inf
859864 }
860865}
861866
862- const resolvePrompt = Effect . fn ( "V2Session.resolvePrompt" ) ( function * ( input : PromptInput . Prompt ) {
867+ const resolvePrompt = Effect . fn ( "V2Session.resolvePrompt" ) ( function * (
868+ input : PromptInput . Prompt ,
869+ image : Effect . Effect < Image . Interface > ,
870+ ) {
863871 const fs = yield * FSUtil . Service
864872 const files = input . files
865- ? yield * Effect . forEach ( input . files , ( file ) => materializeAttachment ( fs , file ) , { concurrency : 8 } )
873+ ? yield * Effect . forEach ( input . files , ( file ) => materializeAttachment ( fs , file , image ) , { concurrency : 8 } )
866874 : undefined
867875 return Prompt . make ( { text : input . text , agents : input . agents , files } )
868876} )
@@ -872,6 +880,7 @@ const MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024
872880const materializeAttachment = Effect . fn ( "V2Session.materializeAttachment" ) ( function * (
873881 fs : FSUtil . Interface ,
874882 input : PromptInput . FileAttachment ,
883+ image : Effect . Effect < Image . Interface > ,
875884) {
876885 const resolved = input . uri . startsWith ( "data:" )
877886 ? {
@@ -900,16 +909,42 @@ const materializeAttachment = Effect.fn("V2Session.materializeAttachment")(funct
900909 . join ( "\n" ) ,
901910 )
902911 : resolved . bytes
903- return FileAttachment . create ( {
904- data : Base64 . make ( Buffer . from ( content ) . toString ( "base64" ) ) ,
912+ const normalized = yield * normalizeImageAttachment (
913+ input ,
914+ Base64 . make ( Buffer . from ( content ) . toString ( "base64" ) ) ,
905915 mime ,
916+ image ,
917+ )
918+ return FileAttachment . create ( {
919+ data : normalized . data ,
920+ mime : normalized . mime ,
906921 source : resolved . source ,
907922 name : input . name ?? resolved . name ,
908923 description : input . description ,
909924 mention : input . mention ,
910925 } )
911926} )
912927
928+ // V1 parity: bound image attachments at prompt admission so oversized images never
929+ // reach persistence or providers. A missing resizer keeps the original image; an
930+ // image that cannot fit the configured limits fails the prompt.
931+ const normalizeImageAttachment = Effect . fn ( "V2Session.normalizeImageAttachment" ) ( function * (
932+ input : PromptInput . FileAttachment ,
933+ data : Base64 ,
934+ mime : string ,
935+ image : Effect . Effect < Image . Interface > ,
936+ ) {
937+ if ( ! mime . startsWith ( "image/" ) ) return { data, mime }
938+ const service = yield * image
939+ const label = input . name ?? ( input . uri . startsWith ( "data:" ) ? "inline attachment" : input . uri )
940+ const content = { uri : label , content : data , encoding : "base64" as const , mime }
941+ const normalized = yield * service . normalize ( label , content ) . pipe (
942+ Effect . catchTag ( "Image.ResizerUnavailableError" , ( ) => Effect . succeed ( content ) ) ,
943+ Effect . mapError ( ( error ) => new AttachmentError ( { uri : label , message : error . message } ) ) ,
944+ )
945+ return { data : Base64 . make ( normalized . content ) , mime : normalized . mime }
946+ } )
947+
913948const readFileAttachment = Effect . fn ( "V2Session.readFileAttachment" ) ( function * ( fs : FSUtil . Interface , uri : string ) {
914949 const url = yield * Effect . try ( {
915950 try : ( ) => new URL ( uri ) ,
0 commit comments