authorgravatar for saphid@gmail.comAlex Southwell <saphid@gmail.com> 2026-09-18 10:25:51+10:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-17 17:25:51-07:00
log3fd21df62da3191f1abf12a0cd74df6de9ed30a2
tree8a79cd92eb2343fd8f8f2e9465b7ac2ce4e652d4
parent5f225395637cb347cfe416c649825a8c7c210014
signature Signed by PGP key B5690EEEBB952194

fix(server): pass Codex image attachments by path to avoid oversized requests (#11050)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

4 files changed, 66 insertions(+), 28 deletions(-)

apps/server/src/provider/Layers/CodexAdapter.test.ts+48
...@@ -448,6 +448,54 @@ sessionErrorLayer("CodexAdapterLive session errors", (it) => {...@@ -448,6 +448,54 @@ sessionErrorLayer("CodexAdapterLive session errors", (it) => {
448 }),448 }),
449 );449 );
450450
451 it.effect("passes image attachments to Codex by path instead of base64", () =>
452 Effect.gen(function* () {
453 const adapter = yield* CodexAdapter;
454 const threadId = asThreadId("thread-image-attachment");
455 yield* adapter.startSession({
456 provider: ProviderDriverKind.make("codex"),
457 threadId,
458 runtimeMode: "full-access",
459 });
460 const runtime = sessionRuntimeFactory.lastRuntime;
461 NodeAssert.ok(runtime);
462 runtime.sendTurnImpl.mockClear();
463
464 const serverConfig = yield* ServerConfig;
465 const attachmentsDir = serverConfig.attachmentsDir;
466 const attachmentId = "attachment-local-image-1";
467 const attachmentPath = NodePath.join(attachmentsDir, `${attachmentId}.png`);
468 NodeFS.writeFileSync(attachmentPath, Buffer.alloc(4, 0x89));
469
470 try {
471 yield* adapter.sendTurn({
472 threadId,
473 input: "Use this image.",
474 attachments: [
475 {
476 type: "image",
477 id: attachmentId,
478 name: "generated.png",
479 mimeType: "image/png",
480 sizeBytes: NodeFS.statSync(attachmentPath).size,
481 },
482 ],
483 });
484
485 const input = runtime.sendTurnImpl.mock.calls[0]?.[0];
486 NodeAssert.ok(input);
487 NodeAssert.deepStrictEqual(input.attachments, [
488 {
489 type: "localImage",
490 path: attachmentPath,
491 },
492 ]);
493 } finally {
494 NodeFS.rmSync(attachmentPath, { force: true });
495 }
496 }),
497 );
498
451 it.effect("passes configured launch args into the session runtime", () => {499 it.effect("passes configured launch args into the session runtime", () => {
452 const runtimeFactory = makeRuntimeFactory();500 const runtimeFactory = makeRuntimeFactory();
453 const layer = Layer.effect(501 const layer = Layer.effect(
apps/server/src/provider/Layers/CodexAdapter.ts+8-18
...@@ -36,7 +36,6 @@ import * as NodeCrypto from "node:crypto";...@@ -36,7 +36,6 @@ import * as NodeCrypto from "node:crypto";
36import * as Crypto from "effect/Crypto";36import * as Crypto from "effect/Crypto";
37import * as Exit from "effect/Exit";37import * as Exit from "effect/Exit";
38import * as Fiber from "effect/Fiber";38import * as Fiber from "effect/Fiber";
39import * as FileSystem from "effect/FileSystem";
40import * as Queue from "effect/Queue";39import * as Queue from "effect/Queue";
41import * as Schema from "effect/Schema";40import * as Schema from "effect/Schema";
42import * as Scope from "effect/Scope";41import * as Scope from "effect/Scope";
...@@ -67,6 +66,7 @@ import {...@@ -67,6 +66,7 @@ import {
67 makeCodexSessionRuntime,66 makeCodexSessionRuntime,
68 type CodexSessionRuntimeError,67 type CodexSessionRuntimeError,
69 type CodexSessionRuntimeOptions,68 type CodexSessionRuntimeOptions,
69 type CodexSessionRuntimeSendTurnInput,
70 type CodexSessionRuntimeShape,70 type CodexSessionRuntimeShape,
71} from "./CodexSessionRuntime.ts";71} from "./CodexSessionRuntime.ts";
72import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts";72import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts";
...@@ -2218,7 +2218,6 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (...@@ -2218,7 +2218,6 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (
2218 options?: CodexAdapterLiveOptions,2218 options?: CodexAdapterLiveOptions,
2219) {2219) {
2220 const boundInstanceId = options?.instanceId ?? ProviderInstanceId.make("codex");2220 const boundInstanceId = options?.instanceId ?? ProviderInstanceId.make("codex");
2221 const fileSystem = yield* FileSystem.FileSystem;
2222 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;2221 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
2223 const crypto = yield* Crypto.Crypto;2222 const crypto = yield* Crypto.Crypto;
2224 const serverConfig = yield* Effect.service(ServerConfig);2223 const serverConfig = yield* Effect.service(ServerConfig);
...@@ -2491,27 +2490,18 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (...@@ -2491,27 +2490,18 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (
2491 detail: `Invalid attachment id '${attachment.id}'.`,2490 detail: `Invalid attachment id '${attachment.id}'.`,
2492 });2491 });
2493 }2492 }
2494 const bytes = yield* fileSystem.readFile(attachmentPath).pipe(
2495 Effect.mapError(
2496 (cause) =>
2497 new ProviderAdapterRequestError({
2498 provider: PROVIDER,
2499 method: "turn/start",
2500 detail: `Failed to read attachment file: ${cause.message}.`,
2501 cause,
2502 }),
2503 ),
2504 );
2505 return {2493 return {
2506 type: "image" as const,2494 type: "localImage" as const,
2507 url: `data:${attachment.mimeType};base64,${Buffer.from(bytes).toString("base64")}`,2495 path: attachmentPath,
2508 };2496 };
2509 });2497 });
25102498
2511 const sendTurn: CodexAdapterShape["sendTurn"] = Effect.fn("sendTurn")(function* (input) {2499 const sendTurn: CodexAdapterShape["sendTurn"] = Effect.fn("sendTurn")(function* (input) {
2512 // Codex ingests images only. Anything else would be base64-encoded as an2500 // Codex ingests images only. Anything else would be inlined as an image
2513 // image and rejected or misread; generic files reach the agent through the2501 // and rejected or misread; generic files reach the agent through the path
2514 // path line ProviderService puts in the prompt.2502 // line ProviderService puts in the prompt. Images are passed by path
2503 // instead of base64 so the turn/start request does not scale with file
2504 // size; the CLI reads the file itself.
2515 const codexAttachments = yield* Effect.forEach(2505 const codexAttachments = yield* Effect.forEach(
2516 (input.attachments ?? []).filter((attachment) => attachment.type === "image"),2506 (input.attachments ?? []).filter((attachment) => attachment.type === "image"),
2517 (attachment) => resolveAttachment(input, attachment),2507 (attachment) => resolveAttachment(input, attachment),
apps/server/src/provider/Layers/CodexSessionRuntime.test.ts+6-6
...@@ -179,8 +179,8 @@ describe("buildTurnStartParams", () => {...@@ -179,8 +179,8 @@ describe("buildTurnStartParams", () => {
179 runtimeMode: "full-access",179 runtimeMode: "full-access",
180 attachments: [180 attachments: [
181 {181 {
182 type: "image",182 type: "localImage",
183 url: { secret } as unknown as string,183 path: { secret } as unknown as string,
184 },184 },
185 ],185 ],
186 }).pipe(Effect.flip),186 }).pipe(Effect.flip),
...@@ -248,8 +248,8 @@ describe("buildTurnStartParams", () => {...@@ -248,8 +248,8 @@ describe("buildTurnStartParams", () => {
248 interactionMode: "default",248 interactionMode: "default",
249 attachments: [249 attachments: [
250 {250 {
251 type: "image",251 type: "localImage",
252 url: "data:image/png;base64,abc",252 path: "/tmp/generated.png",
253 },253 },
254 ],254 ],
255 }),255 }),
...@@ -268,8 +268,8 @@ describe("buildTurnStartParams", () => {...@@ -268,8 +268,8 @@ describe("buildTurnStartParams", () => {
268 text: "Implement it",268 text: "Implement it",
269 },269 },
270 {270 {
271 type: "image",271 type: "localImage",
272 url: "data:image/png;base64,abc",272 path: "/tmp/generated.png",
273 },273 },
274 ],274 ],
275 model: "gpt-5.3-codex",275 model: "gpt-5.3-codex",
apps/server/src/provider/Layers/CodexSessionRuntime.ts+4-4
...@@ -186,8 +186,8 @@ export interface CodexSessionRuntimeOptions {...@@ -186,8 +186,8 @@ export interface CodexSessionRuntimeOptions {
186export interface CodexSessionRuntimeSendTurnInput {186export interface CodexSessionRuntimeSendTurnInput {
187 readonly input?: string;187 readonly input?: string;
188 readonly attachments?: ReadonlyArray<{188 readonly attachments?: ReadonlyArray<{
189 readonly type: "image";189 readonly type: "localImage";
190 readonly url: string;190 readonly path: string;
191 }>;191 }>;
192 readonly model?: string;192 readonly model?: string;
193 readonly serviceTier?: CodexServiceTier | undefined;193 readonly serviceTier?: CodexServiceTier | undefined;
...@@ -614,8 +614,8 @@ export function buildTurnStartParams(input: {...@@ -614,8 +614,8 @@ export function buildTurnStartParams(input: {
614 readonly runtimeMode: RuntimeMode;614 readonly runtimeMode: RuntimeMode;
615 readonly prompt?: string;615 readonly prompt?: string;
616 readonly attachments?: ReadonlyArray<{616 readonly attachments?: ReadonlyArray<{
617 readonly type: "image";617 readonly type: "localImage";
618 readonly url: string;618 readonly path: string;
619 }>;619 }>;
620 readonly model?: string;620 readonly model?: string;
621 readonly serviceTier?: CodexServiceTier;621 readonly serviceTier?: CodexServiceTier;