From 3963453f8dbd16f6c9ada77dc653c9a07682e734 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 6 Jul 2026 16:45:24 -0500 Subject: [PATCH] fix(core): exclude media bytes from compaction estimate --- packages/core/src/session/compaction.ts | 12 +++- packages/core/test/session-compaction.test.ts | 56 ++++++++++++++++++- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/packages/core/src/session/compaction.ts b/packages/core/src/session/compaction.ts index 375d6e5266..bbce833c03 100644 --- a/packages/core/src/session/compaction.ts +++ b/packages/core/src/session/compaction.ts @@ -80,7 +80,17 @@ export interface Interface { export class Service extends Context.Service()("@opencode/v2/SessionCompaction") {} -const estimate = (value: unknown) => Token.estimate(JSON.stringify(value)) +const estimate = (value: unknown) => + Token.estimate( + JSON.stringify(value, (_key, item: unknown) => { + if (typeof item !== "object" || item === null || !("type" in item)) return item + // Providers account for native media separately; its base64 encoding is not prompt text. + if (item.type === "media" && "data" in item) return { ...item, data: "[media bytes]" } + if (item.type === "file" && "uri" in item && typeof item.uri === "string" && item.uri.startsWith("data:")) + return { ...item, uri: item.uri.slice(0, item.uri.indexOf(",") + 1) + "[media bytes]" } + return item + }), + ) const truncate = (value: string) => value.length <= TOOL_OUTPUT_MAX_CHARS ? value : `${value.slice(0, TOOL_OUTPUT_MAX_CHARS)}\n[truncated]` diff --git a/packages/core/test/session-compaction.test.ts b/packages/core/test/session-compaction.test.ts index dd5e9fb749..a89f50b70c 100644 --- a/packages/core/test/session-compaction.test.ts +++ b/packages/core/test/session-compaction.test.ts @@ -1,6 +1,7 @@ import { expect, test } from "bun:test" -import { LLMClient, LLMEvent, Model, type LLMRequest } from "@opencode-ai/llm" +import { LLM, LLMClient, LLMEvent, Message, Model, type LLMRequest } from "@opencode-ai/llm" import { OpenAIChat } from "@opencode-ai/llm/protocols" +import { Base64, FileAttachment } from "@opencode-ai/schema/prompt" import { Config } from "@opencode-ai/core/config" import { Database } from "@opencode-ai/core/database/database" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" @@ -68,6 +69,59 @@ test("compaction describes tool media without embedding base64", () => { expect(serialized).not.toContain(base64) }) +it.effect("does not count media base64 as text context", () => + Effect.gen(function* () { + requests = [] + const compaction = yield* SessionCompaction.Service + const text = "context ".repeat(4_000) + const data = Base64.make(Buffer.alloc(64 * 1024).toString("base64")) + const file = FileAttachment.make({ + data, + mime: "image/png", + source: { type: "inline" }, + name: "screenshot.png", + }) + const inputModel = Model.make({ + id: "media-model", + provider: "test", + route: OpenAIChat.route.with({ limits: { context: 30_000, output: 1_000 } }), + }) + const messages = [ + SessionMessage.User.make({ + id: SessionMessage.ID.create(), + type: "user", + text, + time: { created: DateTime.makeUnsafe(0) }, + }), + SessionMessage.User.make({ + id: SessionMessage.ID.create(), + type: "user", + text: "Inspect this image", + files: [file], + time: { created: DateTime.makeUnsafe(1) }, + }), + ] + + expect( + yield* compaction.compactIfNeeded({ + sessionID: SessionV2.ID.make("ses_media_compaction"), + messages, + request: LLM.request({ + model: inputModel, + messages: [ + Message.user(text), + Message.user([ + { type: "text", text: "Inspect this image" }, + { type: "media", mediaType: "image/png", data, filename: "screenshot.png" }, + ]), + ], + }), + }), + ).toBe(false) + expect(requests).toHaveLength(0) + }), +) + it.effect("manual compaction summarizes short context instead of no-op", () => Effect.gen(function* () { requests = []