Repository navigation
Description
Summary
compaction.reserved is silently ignored for any model whose limit lacks an input field. This was reported in #13980, which was closed by the stale bot without being fixed — the current code still ignores reserved on this code path. Filing this to revive it with a fuller analysis and to tie together several related reports.
Environment
- opencode: 1.17.14 (source pinned to commit
203885d24;overflow.tsidentical on current HEAD) - Model:
zhipuai/glm-5.2(limit = { context: 1000000, output: 131072 }, noinput)
Reproduction
Config:
"compaction": { "auto": true, "reserved": 648928 }Use a built-in model that has no limit.input (e.g. zhipuai/glm-5.2). Observe that compaction does not trigger at input - reserved; it triggers at context - maxOutputTokens = 1000000 - 32000 = 968000. The reserved value has zero effect.
Root cause
packages/opencode/src/session/overflow.ts — usable() has two mutually exclusive branches, and reserved only participates in one:
const reserved =
input.cfg.compaction?.reserved ??
Math.min(COMPACTION_BUFFER, ProviderTransform.maxOutputTokens(input.model, input.outputTokenMax))
return input.model.limit.input
? Math.max(0, input.model.limit.input - reserved) // branch A: reserved applies
: Math.max(0, context - ProviderTransform.maxOutputTokens(input.model, input.outputTokenMax)) // branch B: reserved ignoredModels without limit.input (which is common — most models.dev entries only declare context + output) fall into branch B, where reserved is computed but never used. maxOutputTokens = min(limit.output, OUTPUT_TOKEN_MAX=32000) (from provider/transform.ts), so for glm-5.2 the threshold is 1000000 - 32000 = 968000 regardless of reserved.
The config schema (packages/core/src/v1/config/provider.ts) does support limit.input on custom providers, but there is no way to override limit.input for a built-in provider's model — a custom provider with the same name as a built-in does not replace the built-in entry. So users of built-in providers cannot work around this in config.
Expected vs actual
| Value | |
|---|---|
| Expected threshold | limit.input - reserved (reserved takes effect) |
| Actual threshold | context - maxOutputTokens = 968000 (reserved silently ignored) |
Impact
Silent failure: the user sets reserved, gets no error, but the setting never takes effect. This is a UX trap — there's no signal that the config is being ignored for their model.
Proposed fix
Make reserved apply in branch B as well:
: Math.max(0, context - ProviderTransform.maxOutputTokens(input.model, input.outputTokenMax) - reserved)This makes reserved a consistent "trigger N tokens earlier" knob regardless of whether limit.input is declared. Default reserved is min(COMPACTION_BUFFER=20000, maxOutputTokens), so the default threshold for branch-B models shifts earlier by ~20K — safer (triggers before the hard limit) and consistent with branch A's behavior.
Open semantic question for maintainers: should reserved be subtracted from limit.input (branch A) or from context - maxOutputTokens (branch B)? These differ when limit.input != context - maxOutputTokens. The two branches should agree on the reference point.
Related issues
- Bug: compaction.reserved config is ignored for models without explicit 'input' limit #13980 — the original report of this exact bug; closed by stale bot, unfixed. This issue revives it.
- fix(compaction): output-budget reservation capped at 20K for limit.input models, risking overflow #32656 (open) — sibling problem in the same file, branch A:
reservedcapped at 20K viamin(COMPACTION_BUFFER, …), below real max output. Same class of "reserved not behaving as expected" bug. - [BUG]: Compaction overflow threshold too low when limit.input is set; compaction LLM overflow causes retry loop #30805 / PR fix: compaction context loss, prune re-attach, overflow threshold, missing config key #31891 (closed/merged) — fixed the
limit.inputbranch only; the no-limit.inputbranch (this issue) was missed. - opencode-go/glm-5.2 routed to 262K backend (DigitalOcean) despite 1M advertised context, overflow not classified #36481 (open) — GLM-5.2 specifically, but about backend routing / 262K cap rather than
reservedbeing ignored. Cross-referenced as same-model. - context % under-reports for split-window models (divides by limit.context instead of limit.input) #32119 (open) —
limit.inputvslimit.contextsemantics; relevant to the open question above. - Context bloat occurs severely. I have configured automatic session compression and reserved tokens, yet automatic compression never triggers normally. This issue exists in every version of OpenCode. Please fix this problem as soon as possible. #24683 (open) — same end-user symptom (reserved set, compaction never triggers).
Complementary upstream
The models.dev registry also omits limit.input for these models (systematic across the zhipuai provider). I'll file that separately against models.dev. But the opencode-side fix is needed regardless, since reserved should not be silently dropped for any model.
Activity
diegonix commentedon Jul 30, 2026
Heads-up from testing
glm-5.2: this issue becomes even more relevant once the 32k output cap is bypassed.ollama-cloud/glm-5.2is in the exact affected shape:limit.contextlimit.outputlimit.inputSo once
OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAXis raised, the fallbackusable()branch matters a lot more for this model family. I have not finished a full end-to-end compaction reproduction yet, but GLM 5.2 definitely matches the configuration pattern described here.agent-daryl commentedon Aug 18, 2026
Additional real-world data point (opencode 1.18.18, local model via OpenAI-compatible / llama.cpp)
Confirmed still present on 1.18.18 (issue was reported against 1.17.14). Our stack: custom OpenAI-compatible provider (local llama.cpp server) running a 27B Q8_0 model with declared limit
{ context: 262144, output: 32768 }(noinput), andcompaction: { auto: true, prune: true, tail_turns: 2, reserved: 80000 }.Empirically measured threshold from the session DB:
context − maxOutputTokens, exactly branch B.reserved: 80000had zero effect.User-visible harm (why this matters beyond a silent no-op):
This model's output quality degrades (verbatim context repetition loops + early EOS, i.e. truncated outputs) once context reaches ~71–73% of the window (~185–190k of 262k). The effective trigger (229k) sat ~40k above the degeneration onset, so the model degraded well before compaction ever fired — the session became unusable and had to be abandoned. The whole point of
reservedis to pull the trigger earlier; branch B silently disables it at exactly the moment it is needed.Verified workaround (custom providers):
Adding
"limit": { "input": 240000, "context": 262144, "output": 32768 }to the custom provider's model entry activates branch A: usable = 240000 − 80000 = 160,000 → auto-compaction now fires at 160k, safely before the degeneration onset. So custom/local providers (OpenAI-compatible, local inference servers) can work around this in config — unlike the built-in-provider case called out in the issue, where no override path exists.Suggestion:
When branch B is selected and
cfg.compaction.reservedis explicitly set, eitherreservedin branch B as well (context − maxOutputTokens − reserved), orcompaction.reservedis being ignored because the model has nolimit.input.A silent no-op on a user-facing knob is a UX trap: users believe a safety margin is in force when it isn't.
Happy to share the full incident analysis (timestamps, DB compaction parts, llama.cpp log excerpts) on request.
damsnotdam commentedon Aug 22, 2026
Third-party confirmation on 1.18.21, plus three things this thread does not yet mention
Same shape as @agent-daryl's 2026-08-18 report (custom OpenAI-compatible provider, local 27B, no
limit.input), so I'll skip re-confirming the bug itself and only add what's new.Environment: opencode 1.18.21 (current npm latest), custom OpenAI-compatible provider, MLX
server on Apple M4 Max / 64 GB. Model
Qwen3.8-27B-MLX-4bit, declared{ context: 229376, output: 16384 }, noinput. Branch B threshold measured at229376 - 16384 = 212992— matchesusable()exactly.reservedhad no effect, as reported.Formula verified against 15 automatic compactions across 6 models in 30 days of session DB
(
{"type":"compaction","auto":true}parts), on both 1.18.18 and 1.18.21: every one fired within+31..+3214tokens of the branch-B threshold — the overshoot being just the size of the turn thatcrosses it. So the branch-B path is not merely theoretical, it is the one everyone without
limit.inputis on.1. The
limit.inputworkaround has an undocumented prerequisite: the server never re-reads itsconfig.
This is worth adding to the thread because the workaround appears not to work otherwise, and the
usual verification command actively misleads.
My first attempt changed
opencode.json, confirmed withopencode debug configthat all threelimit.inputvalues were present in the resolved config — and the next session still compacted atthe old threshold. The running server had loaded the config 15 hours earlier and kept the old
value in memory.
opencode debug configproves the file is correct; it says nothing about whatthe server applies.
The control that actually decides is the
message=loading path=…line in the server log, with itstimestamp. Any
opencode.jsonchange needs a server restart. After restarting, compaction firedat the predicted threshold on the very next session.
2. Setting
limit.inputdesynchronises the context percentage shown in the UI.The UI computes its percentage against
limit.context, which stops being the trigger's denominatoronce
limit.inputis set. On my model the indicator reads 57.2% at the exact momentauto-compaction fires, and will never approach ~93% again.
Anyone applying this workaround will see compaction "firing early" and may well revert it. Either the
indicator should use
usable()as its denominator, or this is worth a line in the docs.3. The harm is not only degraded quality — it is throughput, on every token.
@agent-daryl reported repetition loops and early EOS. On my stack the measurable cost showed up
earlier and continuously. Same model, same machine, before and after the workaround:
Nominal throughput for this model on this machine is 27.1 tok/s: the session was running at half
its nominal speed before compaction, purely from context length. Seven server-side rejections
followed. Lowering the trigger did not just avoid the wall, it restored the throughput.
For anyone weighing the trade-off, the steady-state cycle I measured afterwards is
~33k → 131k → ~33k tokens, ~44 turns per 1h07, at the cost of one ~9m30 compaction. Two
consecutive compactions cost 18 min and 9m32; 59–70% of that is prefill, not generation, so
lowering the threshold makes each compaction cheaper, not just more frequent.
Two notes that may help triage
On the open semantic question ("should
reservedsubtract fromlimit.inputor fromcontext - maxOutputTokens?"): from a consumer's point of view the useful property is that theknob be monotonic and model-independent — "trigger N tokens earlier than you otherwise would",
whichever branch computed the baseline. That reading also makes the proposed branch-B fix a
behaviour-preserving default when
reservedis unset.fix(compaction): output-budget reservation capped at 20K for limit.input models, risking overflow #32656 interacts with the workaround recommended in this thread. Setting
limit.inputmovesyou into branch A, which is exactly the branch fix(compaction): output-budget reservation capped at 20K for limit.input models, risking overflow #32656 is about (
reservedcapped atCOMPACTION_BUFFER = 20_000). It happens not to bite for me — mymaxOutputTokensis 16,384,below the cap — but it will bite anyone whose model declares
output > 20000and who follows theadvice here. Worth a cross-reference so the workaround does not hand people the sibling bug.
Compaction has no fallback of its own. If the compaction request is itself rejected by the
server,
SessionProcessorsurfacesContextOverflowErrorwithfinish="error"and stops — noretry, no fallback to a shorter history. My compaction prompts measured 0.25×–0.74× the
session they compact (10 samples; worst case 229,620 → 80,650), so on a saturated session this is
not hypothetical. That ratio may be useful for Compaction request exceeds context window on high-output models (v2) #42448.
agent-daryl commentedon Sep 21, 2026
Data point on your #32656 warning: on 1.18.31 the cap did not bite us, even though we are exactly the profile you said would.
Current config (since 2026-09-15, opencode 1.18.31): model declares
{ context: 262144, input: 240000, output: 32768 }— output well above the 20,000 figure you cited — withcompaction: { auto: true, prune: true, tail_turns: 2, reserved: 50000 }.Measured from our session DB (context size of the last assistant request before each auto-compaction part):
limit.input(June–August): firings at 229,862–242,813 — branch B, consistent with both reports here;input − reserved = 190,000plus normal turn overshoot. If the branch-ACOMPACTION_BUFFER = 20_000cap from fix(compaction): output-budget reservation capped at 20K for limit.input models, risking overflow #32656 were active here, the expected firing point would be ~220,000 (240000 − 20000).So on 1.18.31
reservedappears uncapped for us: either #32656 was fixed between 1.18.21 and 1.18.31, or its trigger conditions are narrower than described. Worth checking which, since the workaround recommendation in this thread depends on it.Your other two points both match our experience:
limit.inputworkaround did nothing until the server restarted;opencode debug configshowed the resolved values, which misled us into thinking the file-level fix had taken effect.thdxr commentedon Oct 11, 2026
Thanks for the report! OpenCode 2.0 is now out and development is focused on V2, so we're closing issues filed against OpenCode 1. Please upgrade (
opencode upgrade --major, details: https://anoma.ly/notes/opencode-2-0), and if this is still relevant on 2.0, reopen this issue or open a new one with your 2.0 version.— from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊