Skip to content

fix(cli): trigger auto-compaction at the configured context threshold - #13340

Open
rakshith1928 wants to merge 10 commits into
Kilo-Org:mainfrom
rakshith1928:fix/preflight-compaction-threshold
Open

fix(cli): trigger auto-compaction at the configured context threshold#13340
rakshith1928 wants to merge 10 commits into
Kilo-Org:mainfrom
rakshith1928:fix/preflight-compaction-threshold

Conversation

@rakshith1928

@rakshith1928 rakshith1928 commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Issue

Fixes #13335

Context

Auto-compaction was firing on message send long before the configured context threshold — with threshold_percent: 80, compaction hit around 40% of the context bar, inconsistently. Two compounding causes in the preflight check:

  1. Wrong base for the percentage. It applied threshold_percent to model.limit.input || model.limit.context. For models with a separate input limit (GPT-5 family: 272k input of a 400k window), "80%" was really 54% of the displayed bar.
  2. Inflated estimate as the trigger value. It compared that limit against a char-based estimate of the whole payload (chars/4 × 1.3 + tool schemas), whose overshoot varies with content type — hence the randomness. Since the check defers during tool loops, it landed on the next user message: the reported "triggers on send" pattern.

Implementation

packages/opencode/src/kilocode/session/overflow.ts

  • limit() now applies threshold_percent to model.limit.context — the denominator the UI uses. Math.min(usable, cap) is unchanged, so the usable input limit remains the hard ceiling.
  • measure() additionally returns tail: the 1.3×-inflated estimate of messages added after the last assistant reply, i.e. the content the provider has not counted yet. Tool schemas stay out of tail since the baseline already includes them.
  • shouldCompact() projects the next request as reported + tail, where reported is the provider-reported count from the last finished turn. Without a baseline (first turn, post-compaction summary, zero-usage report) the full outgoing estimate decides alone. The safety factor stays everywhere — dense CJK/code still over-count rather than under-count, which matters since fix(cli): preserve output budget for encrypted reasoning #13349 normalizes encrypted reasoning out of local estimates, so neither the raw estimate nor the stale baseline alone sees reality.

On models whose input limit sits below their context window (GPT-5: 272k of 400k), the reserved input ceiling fires before a high threshold — 80% triggers around 63% of the displayed bar. That's the model's physical wall, not policy; lower thresholds stay predictable.

packages/opencode/src/session/llm.ts

Passes reported: input.reportedContextTokens and tail: usage.tail into the preflight decision — both values already existed on StreamInput/measure(), no new plumbing.

Tests (test/kilocode/session-overflow.test.ts) cover the false-negative regressions and the effective ceiling below.

Screenshots / Video

before after
Screenshot 2026-08-24 024910 Screenshot 2026-08-24 034022

Config:

  "compaction": {
    "auto": true,
    "threshold_percent": 10,
    "prune": true
  },

How to Test

Manual/local verification

  • bun test ./test/kilocode/session-overflow.test.ts from packages/opencode/: 36 pass, including new regressions for dense CJK payloads, a large new message on top of a high reported baseline with encrypted reasoning history, the input-limited effective ceiling, and zero-usage reports falling back to the estimate path. The false-negative cases were written failing before the fix .

Reviewer test steps

  1. Set compaction.threshold_percent: 80; use a model with an input limit below its context window (e.g. GPT-5).
  2. Chat until the bar reads 40-60%, send a message — no compaction.
  3. Continue past the effective limit — compaction fires on the next send (~80% where input equals context; reserved ceiling earlier on input-limited models).
  4. Optional: paste a large file at low usage — still compacts before the request goes out.

Blocked checks and substitute verification

Checklist

  • Issue linked above, or exception explained
  • Tests/verification described
  • Screenshots/video included for visual changes, or marked N/A
  • Changeset considered for user-facing changes
  • I personally reviewed the diff and can explain the changes, including any AI-assisted work.

Get in Touch

@TRAVIX26 discord

@kilo-code-bot

kilo-code-bot Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (1 files)
  • packages/opencode/src/kilocode/session/overflow.ts
Previous Review Summaries (6 snapshots, latest commit 030412e)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 030412e)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (3 files)
  • packages/opencode/src/kilocode/session/overflow.ts
  • packages/opencode/src/session/llm.ts
  • packages/opencode/test/kilocode/session-overflow.test.ts

Previous review (commit e28ec56)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (5 files)
  • .changeset/fix-autocompaction-threshold.md
  • packages/opencode/src/kilocode/session/overflow.ts
  • packages/opencode/src/session/llm.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/kilocode/session-overflow.test.ts

Previous review (commit 54d7675)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (4 files)
  • .changeset/fix-autocompaction-threshold.md
  • packages/opencode/src/kilocode/session/overflow.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/kilocode/session-overflow.test.ts

Previous review (commit f607bf0)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (4 files)
  • .changeset/fix-autocompaction-threshold.md
  • packages/opencode/src/kilocode/session/overflow.ts
  • packages/opencode/src/session/llm.ts
  • packages/opencode/test/kilocode/session-overflow.test.ts

Previous review (commit c5180fc)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (1 files)
  • packages/opencode/test/kilocode/session-overflow.test.ts

Previous review (commit c1689db)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (4 files)
  • .changeset/fix-autocompaction-threshold.md
  • packages/opencode/src/kilocode/session/overflow.ts
  • packages/opencode/src/session/llm.ts
  • packages/opencode/test/kilocode/session-overflow.test.ts

Reviewed by grok-4.6 · Input: 66.4K · Output: 3.7K · Cached: 170.4K

Review guidance: REVIEW.md from base branch main

@marius-kilocode

Copy link
Copy Markdown
Collaborator

Thanks for digging into this. The input-vs-context mismatch is real, but I do not think we should merge this approach yet. It fixes the repeated-x reproduction by weakening the preflight safety model, which moves this in the wrong direction.

The main issue is estimate / FACTOR. FACTOR exists because Token.estimate is only characters / 4 and undercounts provider tokenizers, especially for code, JSON, and non-English text. Removing it makes the repeated-x case look better, but creates large false negatives. For example, with the OpenAI o200k tokenizer, a 576k-character CJK payload is about 432k tokens. This PR treats it as about 144k tokens, so an 80% threshold on a 200k model does not compact even though the input is already more than twice the model context.

The provider-reported value also cannot replace the current outgoing estimate with Math.max(reported, estimate / FACTOR). reportedContextTokens is from the last completed turn. It does not include the new user message. This matters more after #13349, where encrypted reasoning is correctly excluded from the local normalized estimate. A concrete Luna-shaped case is:

last provider-reported context: 624,205
new user payload:              ~250,000 tokens
projected outgoing context:    ~874,205
80% of 1,050,000:               840,000
value checked by this PR:       624,205
result:                         no compaction

The provider baseline and local estimate are not measurements of the same thing once opaque media and encrypted reasoning are normalized out. We need to account for content added since the provider report, not only take the larger number.

There is also a product-semantics mismatch in the PR description. For a 400k context / 272k input model, usable is 252k after the reserve. The new context-based 80% threshold is 320k, but Math.min(usable, cap) still triggers at 252k, which is 63% on the displayed context bar. Keeping the hard input safety ceiling is reasonable, but we should not claim this now fires at the configured 80%.

Can we revise this so that we:

  • keep conservative accounting for client estimates instead of removing the safety factor;
  • treat the previous provider usage as a baseline and include newly added outgoing content;
  • preserve the encrypted-reasoning normalization now on main;
  • add regressions for code/JSON or multilingual input, reported usage plus a large new message, encrypted reasoning plus a new message, and the effective threshold when input < context;
  • update the changeset and test instructions to explain that the usable input ceiling can trigger before the configured context percentage.

I locally combined current main with this PR. It merges and the focused tests pass, but both false-negative cases above reproduce against the combined code, so the current green checks do not cover the unsafe paths.

The preflight threshold was applied to model.limit.input while the UI
displays usage against limit.context, and it compared an inflated
chars/4 x 1.3 estimate instead of provider-reported tokens. On models
with input < context (GPT-5 family), 80% effectively fired at ~40%
displayed usage.

Apply threshold_percent to the context window and anchor shouldCompact
to the reported token count from the last finished turn, keeping the
deflated estimate as a floor for huge pastes.

Fixes Kilo-Org#13335
…ontent

Address review on the first iteration: dividing the estimate by its
safety factor under-counted dense CJK/code payloads ~3x, and taking
max(reported, estimate) ignored content added since the provider
report once encrypted reasoning is normalized out of local estimates.

The preflight now projects the next request as the last finished
turn's provider-reported count plus the inflated estimate of messages
added after it, falling back to the whole-payload estimate when no
baseline exists (first turn, post-compaction, zero-usage reports).
threshold_percent applies to limit.context, matching the UI. The 1.3
factor stays everywhere; conservative accounting preserved.

On models whose input limit sits below their context window, the
reserved input ceiling still triggers before a high configured
percentage.
@rakshith1928
rakshith1928 force-pushed the fix/preflight-compaction-threshold branch from c5180fc to f607bf0 Compare August 24, 2026 18:08
@rakshith1928

rakshith1928 commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

Revision

Pushed revision [f607bf0e01] covering all six review points:

  • Kept the safety factor in every path. Added a CJK regression using 576k characters that fails if the safety factor is removed.

  • Trigger now uses reportedContextTokens + tail, where tail estimates only messages added after the last assistant reply. The full outgoing estimate remains the fallback on the first turn or after compaction.

  • Rebased onto fix(cli): preserve output budget for encrypted reasoning #13349 with normalization unchanged. Added a regression ensuring opaque encrypted reasoning history is not re-inflated through tail.

  • Refactored message-size calculation into a reusable size() helper in overflow.ts, so full-context and newly added (tail) content use the same media and encrypted-reasoning normalization.

  • Added regressions for dense multilingual input, reported usage plus a large new message, encrypted reasoning plus a new message, and the input < context boundary at the exact usable ceiling.

  • Updated the changeset and test instructions to document the reserved input ceiling behavior (~63% displayed usage for an 80% threshold on GPT-5-style models).

  • Found one more while re-reviewing my own change: zero-usage reports collapsed the projection to tail alone, so non-positive counts now count as no baseline.

@rakshith1928

rakshith1928 commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author
Screenshot 2026-08-27 183714

When Input limit not smaller than context window case, the percentage decided and it compacted at the threshold(a bit above due to new reading files).

@marius-kilocode

Copy link
Copy Markdown
Collaborator

Thanks @rakshith1928 , can you have another look at those comments?

Interrupted responses hide uncounted tool output.
packages/opencode/src/kilocode/session/overflow.ts:47-50 starts the tail after the last serialized assistant, while packages/opencode/src/session/prompt.ts:1786-1789 selects usage from the last finished assistant.
After a tool produced 51,001 characters, I cancelled the next response after visible text. “Continue” sent a normal request: full estimate 163,475, projected count 150,109, threshold 160,000. The intervening tool output was omitted from the projection.

New system content and tool schemas bypass the threshold.
packages/opencode/src/kilocode/session/overflow.ts:90 excludes these components from tail; lines 118-119 then discard the full estimate when a baseline exists.
A new per-message system prompt produced a 240,407-token estimate, but the projection was only 15,117. The CLI sent it without compaction. A separate tool-schema probe also returned false where current main returned true.

Output-only usage is treated as complete context usage.
packages/opencode/src/kilocode/session/overflow.ts:118 accepts any positive count. Missing input usage becomes zero at packages/opencode/src/session/session.ts:427-428, but output tokens make the baseline positive.
With a provider returning only completion_tokens: 100, three growing prompts produced 230,516 estimated tokens, yet preflight projected 71,703 and never compacted.

Thanks in advance

A cancelled or errored assistant trails the last finished one without a
finish marker. The payload then replays tool output and partial text that
the provider report never covered, and the tail estimate starts after the
last serialized assistant so it cannot count them either. The stale
baseline plus tail-only projection under-counted and skipped compaction
past the threshold.

When the last assistant is not the finished one, reportedContextTokens is
now undefined and the full conservative estimate decides alone.
The provider-reported baseline covered only the previous request's
messages, so new system prompts and grown tool schemas bypassed the
threshold. The projection now adds the current system + tool estimate on
top, and a report without prompt-side usage (e.g. completion tokens
only) no longer anchors the baseline.
@rakshith1928

Copy link
Copy Markdown
Contributor Author

All three are addressed on the branch:

  • Interrupted responses — the reported baseline is now only trusted when the last assistant is also the last finished one. After a cancel, the full estimate decides on its own, so the tool output in between gets counted. Added a regression test replaying your 51k-char tool result scenario.
  • System content and tool schemas — the projection now adds the current system + tool-schema estimate on top of reported + tail, so a fresh per-message system prompt or grown schemas no longer slip past it. Both of your probes compact now.
  • Output-only usage — a report without input/cache usage no longer anchors the baseline; the full estimate decides instead. Your 230k-token probe now compacts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Regression: Auto-compaction triggers randomly on message send long before the configured context threshold

2 participants