mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
fix(core): trigger compaction at 85% of the input window
This commit is contained in:
@@ -37,9 +37,8 @@ import { toLLMMessages } from "./runner/to-llm-message.js"
|
||||
import type { AgentNotFoundError } from "./error.js"
|
||||
import type { Instructions } from "../instructions/index.js"
|
||||
|
||||
const DEFAULT_BUFFER = 20_000
|
||||
const AUTO_THRESHOLD = 0.85
|
||||
const DEFAULT_KEEP_TOKENS = 15_000
|
||||
const OUTPUT_TOKEN_MAX = 32_000
|
||||
const TOOL_OUTPUT_MAX_CHARS = 2_000
|
||||
const IMAGE_TOKEN_ESTIMATE = 1_500
|
||||
const PDF_TOKEN_ESTIMATE = 2_000
|
||||
@@ -89,7 +88,7 @@ const LEGACY_HEADING = "## Additional Context"
|
||||
|
||||
export type Settings = {
|
||||
auto: boolean
|
||||
buffer: number
|
||||
buffer?: number
|
||||
tokens: number
|
||||
}
|
||||
|
||||
@@ -401,7 +400,7 @@ export const layer = Layer.effect(
|
||||
|
||||
const state = State.create<Settings & { readonly native: NativeStrategy[] }, Editor>({
|
||||
name: "session-compaction",
|
||||
initial: () => ({ auto: true, buffer: DEFAULT_BUFFER, tokens: DEFAULT_KEEP_TOKENS, native: [] }),
|
||||
initial: () => ({ auto: true, tokens: DEFAULT_KEEP_TOKENS, native: [] }),
|
||||
editor: (editor) => ({
|
||||
configure: (settings) => {
|
||||
if (settings.auto !== undefined) editor.auto = settings.auto
|
||||
@@ -754,11 +753,9 @@ export const layer = Layer.effect(
|
||||
const limit = input.resolved.limit
|
||||
const context = limit.context
|
||||
if (context <= 0) return false
|
||||
const output = Math.min(limit.output, OUTPUT_TOKEN_MAX)
|
||||
const promptCeiling = Math.min(
|
||||
limit.input === undefined ? Number.POSITIVE_INFINITY : limit.input - config.buffer,
|
||||
context - Math.max(output, config.buffer),
|
||||
)
|
||||
const usable = Math.min(context, limit.input ?? context)
|
||||
const promptCeiling =
|
||||
config.buffer === undefined ? Math.floor(usable * AUTO_THRESHOLD) : usable - config.buffer
|
||||
return estimateTokens(input) >= promptCeiling
|
||||
}
|
||||
const compactManual = Effect.fn("SessionCompaction.compactManual")(function* (input: ManualInput) {
|
||||
|
||||
@@ -172,5 +172,5 @@ const input = (tokens: number) => {
|
||||
},
|
||||
}
|
||||
}
|
||||
const bufferedInput = input(85_000)
|
||||
const bufferedInput = input(82_000)
|
||||
const nearInput = input(95_000)
|
||||
|
||||
@@ -153,7 +153,7 @@ test("compaction prompts prohibit task execution", () => {
|
||||
expect(SessionCompaction.buildPrompt(update)).toContain("Do not continue the task or call tools")
|
||||
})
|
||||
|
||||
it.effect("auto compaction estimates current content against the buffered prompt ceiling", () =>
|
||||
it.effect("auto compaction uses 85% by default and a configured buffer instead", () =>
|
||||
Effect.gen(function* () {
|
||||
const compaction = yield* SessionCompaction.Service
|
||||
const session = Session.Info.make({
|
||||
@@ -205,23 +205,27 @@ it.effect("auto compaction estimates current content against the buffered prompt
|
||||
}
|
||||
|
||||
const inputLimited = { context: 400_000, input: 272_000, output: 128_000 }
|
||||
expect(compaction.required(input(251_999, inputLimited))).toBe(false)
|
||||
expect(compaction.required(input(252_000, inputLimited))).toBe(true)
|
||||
expect(compaction.required(input(231_199, inputLimited))).toBe(false)
|
||||
expect(compaction.required(input(231_200, inputLimited))).toBe(true)
|
||||
const native = (tokens: number, limit: { context: number; input?: number; output: number } = inputLimited) => {
|
||||
const selected = input(tokens, limit)
|
||||
return { ...selected, resolved: { ...selected.resolved, compaction: { type: "native" as const } } }
|
||||
}
|
||||
expect(compaction.required(native(251_999))).toBe(false)
|
||||
expect(compaction.required(native(252_000))).toBe(true)
|
||||
expect(compaction.required(native(231_199))).toBe(false)
|
||||
expect(compaction.required(native(231_200))).toBe(true)
|
||||
expect(compaction.required(native(1_000_000, { context: 0, input: undefined, output: 0 }))).toBe(false)
|
||||
|
||||
const contextLimited = { context: 100_000, output: 10_000 }
|
||||
expect(compaction.required(input(79_999, contextLimited))).toBe(false)
|
||||
expect(compaction.required(input(80_000, contextLimited))).toBe(true)
|
||||
expect(compaction.required(input(84_999, contextLimited))).toBe(false)
|
||||
expect(compaction.required(input(85_000, contextLimited))).toBe(true)
|
||||
|
||||
const outputLimited = { context: 100_000, output: 30_000 }
|
||||
expect(compaction.required(input(69_999, outputLimited))).toBe(false)
|
||||
expect(compaction.required(input(70_000, outputLimited))).toBe(true)
|
||||
expect(compaction.required(input(84_999, outputLimited))).toBe(false)
|
||||
expect(compaction.required(input(85_000, outputLimited))).toBe(true)
|
||||
|
||||
const smallWindow = { context: 32_000, output: 32_000 }
|
||||
expect(compaction.required(input(27_199, smallWindow))).toBe(false)
|
||||
expect(compaction.required(input(27_200, smallWindow))).toBe(true)
|
||||
|
||||
const assistant = input(79_000, contextLimited).messages[0]
|
||||
const tool = SessionMessage.AssistantTool.make({
|
||||
@@ -233,7 +237,9 @@ it.effect("auto compaction estimates current content against the buffered prompt
|
||||
})
|
||||
const grown = { ...input(79_000, contextLimited), messages: [{ ...assistant, content: [tool] }] }
|
||||
expect(SessionCompaction.estimateTokens(grown)).toBe(80_000)
|
||||
expect(compaction.required(grown)).toBe(true)
|
||||
expect(compaction.required(grown)).toBe(false)
|
||||
const near = input(84_000, contextLimited)
|
||||
expect(compaction.required({ ...near, messages: [{ ...near.messages[0], content: [tool] }] })).toBe(true)
|
||||
|
||||
const interrupted = { ...assistant, id: SessionMessage.ID.create(), tokens: undefined }
|
||||
expect(SessionCompaction.estimateTokens({ ...grown, messages: [...grown.messages, interrupted] })).toBe(80_001)
|
||||
@@ -285,6 +291,13 @@ it.effect("auto compaction estimates current content against the buffered prompt
|
||||
time: { created: 0, completed: 0 },
|
||||
})
|
||||
expect(compaction.required({ ...grown, messages: [checkpoint] })).toBe(false)
|
||||
|
||||
yield* compaction.transform((editor) => editor.configure({ buffer: 10_000 }))
|
||||
expect(compaction.required(input(89_999, contextLimited))).toBe(false)
|
||||
expect(compaction.required(input(90_000, contextLimited))).toBe(true)
|
||||
yield* compaction.transform((editor) => editor.configure({ buffer: 0 }))
|
||||
expect(compaction.required(input(99_999, contextLimited))).toBe(false)
|
||||
expect(compaction.required(input(100_000, contextLimited))).toBe(true)
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -2883,7 +2883,7 @@ describe("SessionRunnerLLM", () => {
|
||||
agent.steps = 2
|
||||
}),
|
||||
)
|
||||
yield* s.llm.push(TestLLM.textWithUsage("Earlier answer", "before-native", 10_000))
|
||||
yield* s.llm.push(TestLLM.textWithUsage("Earlier answer", "before-native", 36_000))
|
||||
yield* s.runPrompt("First real request")
|
||||
const checkpoint = (encrypted: string) =>
|
||||
CompactionCheckpointResponse.make({
|
||||
@@ -2903,7 +2903,7 @@ describe("SessionRunnerLLM", () => {
|
||||
const installed = (yield* s.messages).filter((message) => message.type === "compaction")
|
||||
expect(installed).toMatchObject([{ status: "completed", reason: "auto", providerContext: { version: 1 } }])
|
||||
// New input without a post-checkpoint usage anchor must not retrigger compaction.
|
||||
yield* s.llm.push(TestLLM.textWithUsage("Measured", "measured", 10_000))
|
||||
yield* s.llm.push(TestLLM.textWithUsage("Measured", "measured", 36_000))
|
||||
yield* s.runPrompt("Third real request")
|
||||
expect(s.requests).toHaveLength(5)
|
||||
yield* s.llm.push(checkpoint("second"), TestLLM.textWithUsage("Continued", "continued", 10_000))
|
||||
@@ -2929,7 +2929,7 @@ describe("SessionRunnerLLM", () => {
|
||||
s.currentModel = LanguageModel.make({ id: "native", provider: "openai", route: OpenAIResponses.route })
|
||||
modelLimits.set("native", { context: 42_000, output: 32_000 })
|
||||
s.compaction = { type: "native" }
|
||||
yield* s.llm.push(TestLLM.textWithUsage("Earlier answer", "before-native", 10_000))
|
||||
yield* s.llm.push(TestLLM.textWithUsage("Earlier answer", "before-native", 36_000))
|
||||
yield* s.runPrompt("Original durable request")
|
||||
yield* s.llm.push(
|
||||
CompactionCheckpointResponse.make({
|
||||
|
||||
@@ -78,17 +78,20 @@ Reusing that ID for another record returns a conflict.
|
||||
## Automatic
|
||||
|
||||
Before each model call, OpenCode estimates the final size of the system prompt,
|
||||
messages, and advertised tools. It starts compaction at this ceiling:
|
||||
messages, and advertised tools. By default it starts compaction at 85% of the
|
||||
smaller context or input limit. A configured `buffer` replaces that percentage
|
||||
with an absolute number of tokens to reserve:
|
||||
|
||||
```text
|
||||
estimated tokens >= min(input limit - buffer, context limit - max(output reserve, buffer))
|
||||
usable window = min(context limit, input limit if present)
|
||||
compact when estimated tokens >= (buffer configured ? usable window - buffer : floor(85% of usable window))
|
||||
```
|
||||
|
||||
For example, with a 128,000-token input limit and the default 20,000-token
|
||||
buffer, the input-limit side of the ceiling is 108,000 tokens.
|
||||
For example, with a 200,000-token context and a 128,000-token input limit,
|
||||
the default threshold is 108,800 tokens:
|
||||
|
||||
```text
|
||||
128,000 - 20,000 = 108,000
|
||||
128,000 × 85% = 108,800
|
||||
```
|
||||
|
||||
The estimate follows these rules:
|
||||
@@ -97,8 +100,7 @@ The estimate follows these rules:
|
||||
newer content are then added.
|
||||
- Without provider usage, OpenCode estimates text, media, instructions, and
|
||||
tools locally.
|
||||
- The reserved model output is capped at 32,000 tokens.
|
||||
- A model without an input limit is constrained by its context limit instead.
|
||||
- Without a separate input limit, the context limit sets the threshold.
|
||||
- A successful checkpoint rebuilds the same pending model step. It does not
|
||||
promote the input again or spend another agent step.
|
||||
|
||||
@@ -132,11 +134,11 @@ Add `compaction` to any [OpenCode configuration file](/config):
|
||||
| --- | ---: | --- |
|
||||
| `auto` | `true` | Enables preflight checks and one provider-overflow recovery attempt. It does not control manual compaction. |
|
||||
| `keep.tokens` | `15000` | Approximate recent serialized context retained beside a local summary, or real user input retained for a provider checkpoint. |
|
||||
| `buffer` | `20000` | Safety margin below an explicit input limit. Without one, it is the minimum context reserve; a larger model output allowance wins. |
|
||||
| `buffer` | unset | Replaces the 85% threshold with `usable window - buffer`. Set it to reserve a fixed number of tokens. |
|
||||
|
||||
`keep.tokens` and `buffer` accept non-negative integers. Larger
|
||||
`keep.tokens` preserves more recent detail but leaves less room for new work;
|
||||
larger `buffer` starts automatic compaction earlier.
|
||||
larger `buffer` starts automatic compaction earlier when configured.
|
||||
|
||||
## Providers
|
||||
|
||||
@@ -161,7 +163,7 @@ A model setting overrides the provider setting.
|
||||
|
||||
| Topic | Provider behavior |
|
||||
| --- | --- |
|
||||
| Threshold | OpenCode uses the model's usable input ceiling configured by its context, input, and output limits plus the global compaction buffer. |
|
||||
| Threshold | OpenCode starts at 85% of the smaller context or input limit, unless `buffer` replaces it with a fixed reserve. |
|
||||
| Scheduling | Checkpoints run at normal safe step boundaries. `compaction.auto: false` disables all new automatic work. |
|
||||
| Usage | After a native checkpoint is installed, automatic checks wait for fresh model usage because encrypted checkpoint bytes cannot provide a meaningful token count. |
|
||||
| OpenAI | Responses routes use a streamed compaction trigger when supported; endpoint-only routes use the standalone compaction endpoint. Deployment and model support vary. |
|
||||
|
||||
Reference in New Issue
Block a user