diff --git a/apps/server/src/provider/Layers/CodexAdapter.test.ts b/apps/server/src/provider/Layers/CodexAdapter.test.ts index f7c6036885d9dd73920a9af329aa6a03d94621ef..48aff6cd707e74361eb3cda1d92a8e69d332506e 100644 --- a/apps/server/src/provider/Layers/CodexAdapter.test.ts +++ b/apps/server/src/provider/Layers/CodexAdapter.test.ts @@ -448,6 +448,54 @@ sessionErrorLayer("CodexAdapterLive session errors", (it) => { }), ); + it.effect("passes image attachments to Codex by path instead of base64", () => + Effect.gen(function* () { + const adapter = yield* CodexAdapter; + const threadId = asThreadId("thread-image-attachment"); + yield* adapter.startSession({ + provider: ProviderDriverKind.make("codex"), + threadId, + runtimeMode: "full-access", + }); + const runtime = sessionRuntimeFactory.lastRuntime; + NodeAssert.ok(runtime); + runtime.sendTurnImpl.mockClear(); + + const serverConfig = yield* ServerConfig; + const attachmentsDir = serverConfig.attachmentsDir; + const attachmentId = "attachment-local-image-1"; + const attachmentPath = NodePath.join(attachmentsDir, `${attachmentId}.png`); + NodeFS.writeFileSync(attachmentPath, Buffer.alloc(4, 0x89)); + + try { + yield* adapter.sendTurn({ + threadId, + input: "Use this image.", + attachments: [ + { + type: "image", + id: attachmentId, + name: "generated.png", + mimeType: "image/png", + sizeBytes: NodeFS.statSync(attachmentPath).size, + }, + ], + }); + + const input = runtime.sendTurnImpl.mock.calls[0]?.[0]; + NodeAssert.ok(input); + NodeAssert.deepStrictEqual(input.attachments, [ + { + type: "localImage", + path: attachmentPath, + }, + ]); + } finally { + NodeFS.rmSync(attachmentPath, { force: true }); + } + }), + ); + it.effect("passes configured launch args into the session runtime", () => { const runtimeFactory = makeRuntimeFactory(); const layer = Layer.effect( diff --git a/apps/server/src/provider/Layers/CodexAdapter.ts b/apps/server/src/provider/Layers/CodexAdapter.ts index b43755736ca3106caf031a23cb7fd2306a4338c2..ba61d14f5cf56ca4c2025f82151f3e11de8d9389 100644 --- a/apps/server/src/provider/Layers/CodexAdapter.ts +++ b/apps/server/src/provider/Layers/CodexAdapter.ts @@ -36,7 +36,6 @@ import * as NodeCrypto from "node:crypto"; import * as Crypto from "effect/Crypto"; import * as Exit from "effect/Exit"; import * as Fiber from "effect/Fiber"; -import * as FileSystem from "effect/FileSystem"; import * as Queue from "effect/Queue"; import * as Schema from "effect/Schema"; import * as Scope from "effect/Scope"; @@ -67,6 +66,7 @@ import { makeCodexSessionRuntime, type CodexSessionRuntimeError, type CodexSessionRuntimeOptions, + type CodexSessionRuntimeSendTurnInput, type CodexSessionRuntimeShape, } from "./CodexSessionRuntime.ts"; import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts"; @@ -2218,7 +2218,6 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* ( options?: CodexAdapterLiveOptions, ) { const boundInstanceId = options?.instanceId ?? ProviderInstanceId.make("codex"); - const fileSystem = yield* FileSystem.FileSystem; const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; const crypto = yield* Crypto.Crypto; const serverConfig = yield* Effect.service(ServerConfig); @@ -2491,27 +2490,18 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* ( detail: `Invalid attachment id '${attachment.id}'.`, }); } - const bytes = yield* fileSystem.readFile(attachmentPath).pipe( - Effect.mapError( - (cause) => - new ProviderAdapterRequestError({ - provider: PROVIDER, - method: "turn/start", - detail: `Failed to read attachment file: ${cause.message}.`, - cause, - }), - ), - ); return { - type: "image" as const, - url: `data:${attachment.mimeType};base64,${Buffer.from(bytes).toString("base64")}`, + type: "localImage" as const, + path: attachmentPath, }; }); const sendTurn: CodexAdapterShape["sendTurn"] = Effect.fn("sendTurn")(function* (input) { - // Codex ingests images only. Anything else would be base64-encoded as an - // image and rejected or misread; generic files reach the agent through the - // path line ProviderService puts in the prompt. + // Codex ingests images only. Anything else would be inlined as an image + // and rejected or misread; generic files reach the agent through the path + // line ProviderService puts in the prompt. Images are passed by path + // instead of base64 so the turn/start request does not scale with file + // size; the CLI reads the file itself. const codexAttachments = yield* Effect.forEach( (input.attachments ?? []).filter((attachment) => attachment.type === "image"), (attachment) => resolveAttachment(input, attachment), diff --git a/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts b/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts index c14dca7ccd1918584a8dac80c9754f70fb5c2af0..ec113ab7c521cc6c2ff02d9cd40e3d4ad568c4c2 100644 --- a/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts +++ b/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts @@ -179,8 +179,8 @@ describe("buildTurnStartParams", () => { runtimeMode: "full-access", attachments: [ { - type: "image", - url: { secret } as unknown as string, + type: "localImage", + path: { secret } as unknown as string, }, ], }).pipe(Effect.flip), @@ -248,8 +248,8 @@ describe("buildTurnStartParams", () => { interactionMode: "default", attachments: [ { - type: "image", - url: "data:image/png;base64,abc", + type: "localImage", + path: "/tmp/generated.png", }, ], }), @@ -268,8 +268,8 @@ describe("buildTurnStartParams", () => { text: "Implement it", }, { - type: "image", - url: "data:image/png;base64,abc", + type: "localImage", + path: "/tmp/generated.png", }, ], model: "gpt-5.3-codex", diff --git a/apps/server/src/provider/Layers/CodexSessionRuntime.ts b/apps/server/src/provider/Layers/CodexSessionRuntime.ts index c26d6ae446d8c7a2784c853a4251b9a3993a45f8..c8aa069cb2b70f596f34b094bbcae20ca54ab395 100644 --- a/apps/server/src/provider/Layers/CodexSessionRuntime.ts +++ b/apps/server/src/provider/Layers/CodexSessionRuntime.ts @@ -186,8 +186,8 @@ export interface CodexSessionRuntimeOptions { export interface CodexSessionRuntimeSendTurnInput { readonly input?: string; readonly attachments?: ReadonlyArray<{ - readonly type: "image"; - readonly url: string; + readonly type: "localImage"; + readonly path: string; }>; readonly model?: string; readonly serviceTier?: CodexServiceTier | undefined; @@ -614,8 +614,8 @@ export function buildTurnStartParams(input: { readonly runtimeMode: RuntimeMode; readonly prompt?: string; readonly attachments?: ReadonlyArray<{ - readonly type: "image"; - readonly url: string; + readonly type: "localImage"; + readonly path: string; }>; readonly model?: string; readonly serviceTier?: CodexServiceTier;