From 52408ac22f0cb209972607f35008974a551f96e1 Mon Sep 17 00:00:00 2001 From: Ese Udom Date: Thu, 22 Oct 2026 13:21:42 +0100 Subject: [PATCH] fix(core): omit inline image data from compaction prompt --- packages/core/src/prompt.ts | 37 ++++++++++++++++++++++- packages/core/src/tests/prompt.test.ts | 41 ++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/packages/core/src/prompt.ts b/packages/core/src/prompt.ts index 24ca1018..6c7a430e 100644 --- a/packages/core/src/prompt.ts +++ b/packages/core/src/prompt.ts @@ -293,6 +293,41 @@ export function getSystemPrompt(_projectRoot: string, options: PromptToolOptions return toolDocs ? `${SYSTEM_PROMPT_BASE}\n\n# Available Tools\n\n${toolDocs}` : SYSTEM_PROMPT_BASE; } +const OMITTED_IMAGE_DATA_URL = "[image data omitted from compaction prompt]"; + +function compactContentParams(contentParams: unknown): unknown { + if (!contentParams) { + return contentParams; + } + + const compactPart = (part: unknown): unknown => { + if (!part || typeof part !== "object") { + return part; + } + const candidate = part as { type?: unknown; image_url?: unknown }; + if (candidate.type !== "image_url") { + return part; + } + const imageUrl = candidate.image_url; + if (!imageUrl || typeof imageUrl !== "object") { + return part; + } + const url = (imageUrl as { url?: unknown }).url; + if (typeof url !== "string" || !url.startsWith("data:")) { + return part; + } + return { + ...candidate, + image_url: { + ...(imageUrl as Record), + url: OMITTED_IMAGE_DATA_URL, + }, + }; + }; + + return Array.isArray(contentParams) ? contentParams.map(compactPart) : compactPart(contentParams); +} + export function getCompactPrompt(sessionMessages: SessionMessage[]): string { const jsonl = sessionMessages .map((message) => @@ -300,7 +335,7 @@ export function getCompactPrompt(sessionMessages: SessionMessage[]): string { id: message.id, role: message.role, content: message.content, - contentParams: message.contentParams, + contentParams: compactContentParams(message.contentParams), messageParams: message.messageParams, createTime: message.createTime, }) diff --git a/packages/core/src/tests/prompt.test.ts b/packages/core/src/tests/prompt.test.ts index d4d73763..e37778d6 100644 --- a/packages/core/src/tests/prompt.test.ts +++ b/packages/core/src/tests/prompt.test.ts @@ -7,11 +7,13 @@ import { fileURLToPath } from "url"; import { buildSkillCatalogPrompt, buildSkillDocumentsPrompt, + getCompactPrompt, getPlanModePrompt, getRuntimeContext, getSystemPrompt, getTools, } from "../prompt"; +import type { SessionMessage } from "../session"; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const tempDirs: string[] = []; @@ -323,3 +325,42 @@ test("deepseek-flash uses native image tools unless multimodal is disabled", () assert.equal(prompt.includes("## UnderstandImage"), !native); } }); + +test("getCompactPrompt omits inline image data payloads", () => { + const buildMessage = (overrides: Partial): SessionMessage => ({ + id: "message", + sessionId: "session", + role: "user", + content: null, + contentParams: null, + messageParams: null, + compacted: false, + visible: true, + createTime: "2026-01-01T00:00:00.000Z", + updateTime: "2026-01-01T00:00:00.000Z", + ...overrides, + }); + + const prompt = getCompactPrompt([ + buildMessage({ + id: "inline", + content: "inline screenshot", + contentParams: [ + { + type: "image_url", + image_url: { url: "data:image/png;base64,AAAA", detail: "high" }, + }, + ], + }), + buildMessage({ + id: "remote", + content: "remote screenshot", + contentParams: [{ type: "image_url", image_url: { url: "https://example.com/a.png" } }], + }), + ]); + + assert.equal(prompt.includes("data:image"), false); + assert.equal(prompt.includes("[image data omitted from compaction prompt]"), true); + assert.equal(prompt.includes("https://example.com/a.png"), true); + assert.equal(prompt.includes('"detail":"high"'), true); +});