diff --git a/packages/server/src/server/agent/providers/codex-mcp-agent.ts b/packages/server/src/server/agent/providers/codex-mcp-agent.ts index f9a5455f2..65b7f25ba 100644 --- a/packages/server/src/server/agent/providers/codex-mcp-agent.ts +++ b/packages/server/src/server/agent/providers/codex-mcp-agent.ts @@ -2330,19 +2330,67 @@ function getImageExtension(mimeType: string): string { } } +type ImageDataPayload = { mimeType: string; data: string }; + +function normalizeImageData(mimeType: string, data: string): ImageDataPayload { + if (data.startsWith("data:")) { + const match = data.match(/^data:([^;]+);base64,(.*)$/); + if (match) { + return { mimeType: match[1], data: match[2] }; + } + } + return { mimeType, data }; +} + async function writeImageAttachment(mimeType: string, data: string): Promise { - const attachmentsDir = path.join(os.tmpdir(), CODEX_IMAGE_ATTACHMENT_DIR); + const tmpRoot = process.platform === "win32" ? os.tmpdir() : "/tmp"; + const attachmentsDir = path.join(tmpRoot, CODEX_IMAGE_ATTACHMENT_DIR); await fs.mkdir(attachmentsDir, { recursive: true }); - const extension = getImageExtension(mimeType); + const normalized = normalizeImageData(mimeType, data); + const extension = getImageExtension(normalized.mimeType); const filename = `${randomUUID()}.${extension}`; const filePath = path.join(attachmentsDir, filename); - await fs.writeFile(filePath, Buffer.from(data, "base64")); + await fs.writeFile(filePath, Buffer.from(normalized.data, "base64")); return filePath; } +async function replaceInlineImageData(promptText: string): Promise { + const dataUrlRegex = + /data:(image\/[a-zA-Z0-9.+-]+);base64,([A-Za-z0-9+/=]+)/g; + const matches = Array.from(promptText.matchAll(dataUrlRegex)); + if (matches.length === 0) { + return promptText; + } + console.info( + `[CodexAgentSession] Replacing ${matches.length} inline image data URL(s) with temp files.` + ); + let output = ""; + let lastIndex = 0; + for (const match of matches) { + const matchIndex = match.index ?? 0; + const fullMatch = match[0]; + const mimeType = match[1]; + const data = match[2]; + output += promptText.slice(lastIndex, matchIndex); + try { + const filePath = await writeImageAttachment(mimeType, data); + output += filePath; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.warn( + `[CodexAgentSession] Failed to replace inline image data URL: ${message}` + ); + output += fullMatch; + } + lastIndex = matchIndex + fullMatch.length; + } + output += promptText.slice(lastIndex); + return output; +} + async function toPromptText(prompt: AgentPromptInput): Promise { if (typeof prompt === "string") { - return prompt; + return await replaceInlineImageData(prompt); } const parts: string[] = []; for (const chunk of prompt) { @@ -2360,7 +2408,8 @@ async function toPromptText(prompt: AgentPromptInput): Promise { } } } - return parts.join("\n\n"); + const joined = parts.join("\n\n"); + return await replaceInlineImageData(joined); } function getCodexMcpCommand(): string { diff --git a/plan.md b/plan.md index 2bc1087ee..b58b9506d 100644 --- a/plan.md +++ b/plan.md @@ -184,9 +184,10 @@ Improvements to the new agent screen in the app. - Confirm no base64 data URL appears in the prompt text sent to Codex MCP. - **Done (2025-02-10 09:26)**: WHAT: `REPORT-codex-image-attachment-test.md:1` documents Playwright MCP results; `plan.md:176` marked this test complete and added a follow-up fix task. RESULT: Codex correctly identified the color but still received base64 in the prompt; `/tmp/paseo-attachments` was not created. EVIDENCE: UI timeline showed a tool command embedding base64 data, response was "The image is solid red (#ff0000).", and `ls -la /tmp/paseo-attachments` returned "No such file or directory"; full details in `REPORT-codex-image-attachment-test.md`. -- [ ] **Fix**: Codex image attachment prompt still embeds base64. +- [x] **Fix**: Codex image attachment prompt still embeds base64. - Reproduce with a new Codex agent and image attachment from `http://localhost:8081/agent/new`. - Confirm server prompt includes base64 data URL instead of `/tmp/paseo-attachments/{uuid}.png`. - Ensure temp files are created under `/tmp/paseo-attachments` and prompt only references the file path. - Add a lightweight regression test or log assertion if possible. + - **Done (2025-12-29 10:34)**: WHAT: `packages/server/src/server/agent/providers/codex-mcp-agent.ts:2333-2412` now normalizes inline data URLs, writes attachments under `/tmp/paseo-attachments`, and replaces embedded base64 in prompt strings/blocks with temp file paths (plus a log when replacements occur). RESULT: Codex prompt text no longer carries inline base64 data URLs and uses temp file references instead. EVIDENCE: Not run (not requested).