refactor(core): tune retained media budget

This commit is contained in:
Aiden Cline
2026-08-04 23:39:05 -05:00
parent 0c89e24aac
commit caec8d6638
3 changed files with 40 additions and 18 deletions
+8 -6
View File
@@ -23,7 +23,7 @@ const DEFAULT_BUFFER = 20_000
const DEFAULT_KEEP_TOKENS = 15_000
const OUTPUT_TOKEN_MAX = 32_000
const TOOL_OUTPUT_MAX_CHARS = 2_000
const IMAGE_TOKEN_ESTIMATE = 2_000
const MEDIA_TOKEN_ESTIMATE = 1_500
const SUMMARY_TEMPLATE = `Output exactly the Markdown structure shown inside <template> and keep the section order unchanged. Do not include the <template> tags in your response.
<template>
## Objective
@@ -127,9 +127,12 @@ export const serializeToolContent = (content: SessionMessage.ToolStateCompleted[
)
.join("\n")
export const estimateImageTokens = (message: SessionMessage.Info) => {
const isEstimatedMedia = (mime: string) =>
mime.toLowerCase().startsWith("image/") || mime.toLowerCase() === "application/pdf"
export const estimateMediaTokens = (message: SessionMessage.Info) => {
if (message.type === "user")
return (message.files?.filter((file) => file.mime.toLowerCase().startsWith("image/")).length ?? 0) * IMAGE_TOKEN_ESTIMATE
return (message.files?.filter((file) => isEstimatedMedia(file.mime)).length ?? 0) * MEDIA_TOKEN_ESTIMATE
if (message.type !== "assistant") return 0
return (
message.content
@@ -138,8 +141,7 @@ export const estimateImageTokens = (message: SessionMessage.Info) => {
? (part.state.content ?? [])
: [],
)
.filter((content) => content.type === "file" && content.mime.toLowerCase().startsWith("image/")).length *
IMAGE_TOKEN_ESTIMATE
.filter((content) => content.type === "file" && isEstimatedMedia(content.mime)).length * MEDIA_TOKEN_ESTIMATE
)
}
@@ -205,7 +207,7 @@ const select = (
let total = 0
let split = conversation.length
for (let index = conversation.length - 1; index >= 0; index--) {
const next = total + Token.estimate(conversation[index].text) + estimateImageTokens(conversation[index].message)
const next = total + Token.estimate(conversation[index].text) + estimateMediaTokens(conversation[index].message)
if (split < conversation.length && next > tokens) break
total = next
split = index
+28 -8
View File
@@ -114,7 +114,7 @@ test("compaction describes tool media without embedding base64", () => {
expect(serialized).not.toContain(base64)
})
test("compaction estimates image context without counting base64", () => {
test("compaction estimates media context without counting base64", () => {
const image = FileAttachment.make({
data: Base64.make("a".repeat(10_000)),
mime: "image/png",
@@ -125,11 +125,11 @@ test("compaction estimates image context without counting base64", () => {
id: SessionMessage.ID.create(),
type: "user",
text: "Compare these images.",
files: [image, image],
files: [image, image, FileAttachment.make({ ...image, mime: "application/pdf" })],
time: { created: DateTime.makeUnsafe(0) },
})
expect(SessionCompaction.estimateImageTokens(message)).toBe(4_000)
expect(SessionCompaction.estimateMediaTokens(message)).toBe(4_500)
})
test("compaction prompt requires the checkpoint headings in order", () => {
@@ -197,7 +197,7 @@ it.effect("auto compaction reserves a buffer below the prompt ceiling", () =>
}),
)
it.effect("manual compaction summarizes short context instead of no-op", () =>
it.effect("manual compaction preserves ordered media in the retained tail", () =>
Effect.gen(function* () {
requests = []
const db = (yield* Database.Service).db
@@ -209,7 +209,7 @@ it.effect("manual compaction summarizes short context instead of no-op", () =>
const userMessage = {
id: SessionMessage.ID.create(),
type: "user" as const,
text: "Manual compaction should include this short conversation.",
text: `Manual compaction should include this older conversation. ${"older context ".repeat(4_500)}`,
time: { created: DateTime.makeUnsafe(0) },
}
const recentMessage = SessionMessage.User.make({
@@ -223,9 +223,21 @@ it.effect("manual compaction summarizes short context instead of no-op", () =>
source: { type: "inline" },
name: "prompt.pdf",
}),
FileAttachment.make({
data: Base64.make("aW1hZ2U="),
mime: "image/png",
source: { type: "inline" },
name: "prompt.png",
}),
],
time: { created: DateTime.makeUnsafe(1) },
})
const latestMessage = SessionMessage.User.make({
id: SessionMessage.ID.create(),
type: "user",
text: "Newest text after the retained media.",
time: { created: DateTime.makeUnsafe(2) },
})
yield* db
.insert(ProjectTable)
.values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] })
@@ -261,7 +273,7 @@ it.effect("manual compaction summarizes short context instead of no-op", () =>
expect(
yield* compaction.compactManual({
session,
messages: [userMessage, recentMessage],
messages: [userMessage, recentMessage, latestMessage],
inputID: SessionMessage.ID.make("msg_manual_compaction"),
}),
).toEqual({ status: "completed" })
@@ -278,13 +290,15 @@ it.effect("manual compaction summarizes short context instead of no-op", () =>
"x-opencode-client": "opencode",
})
expect(requests[0]?.generation).toBeUndefined()
expect(JSON.stringify(requests[0]?.messages)).toContain("Manual compaction should include this short conversation.")
expect(JSON.stringify(requests[0]?.messages)).toContain("Manual compaction should include this older conversation.")
expect(yield* store.context(sessionID)).toMatchObject([
{
type: "compaction",
reason: "manual",
summary: "manual summary",
recent: expect.stringContaining("[Attached application/pdf: prompt.pdf]"),
recent: expect.stringMatching(
/\[User\]: Compare the retained media\.\n\[Attached application\/pdf: prompt\.pdf\]\n\[Attached image\/png: prompt\.png\]\n\n\[User\]: Newest text after the retained media\./,
),
media: [
{
type: "file",
@@ -292,6 +306,12 @@ it.effect("manual compaction summarizes short context instead of no-op", () =>
mime: "application/pdf",
name: "prompt.pdf",
},
{
type: "file",
uri: "data:image/png;base64,aW1hZ2U=",
mime: "image/png",
name: "prompt.png",
},
],
},
])