fix(agent): use ground-truth prompt_tokens in the compaction trigger - #49
Conversation
The auto-compaction trigger sized the context with a bytes/4 heuristic (`estimate_context_tokens`). That under-counts the code/JSON/non-English payloads this agent generates, so a context that has really blown past the model's window can read as "under threshold" and the next request overflows — an unrecoverable failure once the provider rejects it. The provider already returns the exact input size in `usage.prompt_tokens`, but it was only emitted as telemetry and discarded. Track the most recent value per turn and feed it into the trigger via `effective_context_tokens(estimate, last)` = `max(estimate, last_prompt_tokens)`. At the end-of-turn compaction check, `last_prompt_tokens` covers nearly the whole current context (only the just- produced final assistant message is newer), so the max corrects the heuristic's under-count and compaction fires when it should. Self-correcting by construction: after any compaction the next request re-counts a smaller context (overflow recovery `continue`s the loop; the threshold path ends the turn), and `last_prompt_tokens` resets to `None` per inbound — so a stale large value can't cause a spurious re-trigger. Out of scope (follow-up): an OpenAI context-window override for `provider.context_window_tokens()` so the window-aware threshold tightens there too. Tests: `effective_context_tokens_prefers_ground_truth` (fallback, ground-truth wins, estimate wins, zero never lowers).
Independent code review — APPROVE (upstream-ready)Rust review of the ground-truth-token compaction trigger. No CRITICAL/HIGH.
|
There was a problem hiding this comment.
Code Review
This pull request improves the context-size estimation for the compaction trigger by using the maximum of the heuristic estimate and the actual usage.prompt_tokens from the last LLM call. While this helps prevent context window overflows on code/JSON-heavy payloads, a review comment identifies an issue where last_prompt_tokens is not reset to None after a successful emergency compaction. This could lead to redundant compactions if subsequent LLM calls do not return usage statistics, as the stale, pre-compaction token count would persist.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| // server-counted). The bytes/4 heuristic under-counts code/JSON/non-English — exactly what | ||
| // this agent generates — so the compaction trigger uses `max(estimate, last_prompt_tokens)` | ||
| // to avoid silently overflowing the context window. Updated after each provider response. | ||
| let mut last_prompt_tokens: Option<u32> = None; |
There was a problem hiding this comment.
When emergency compaction succeeds in the ContextOverflow recovery path, the context is compacted and becomes much smaller. However, last_prompt_tokens is not reset to None. If the subsequent LLM call does not return usage statistics (e.g., due to a mock provider, local model, or temporary provider issue), last_prompt_tokens will retain the stale, pre-compaction huge value. This will cause the end-of-turn compaction check to immediately trigger a redundant compaction right after we just compacted. To prevent this, we should reset last_prompt_tokens to None when emergency compaction succeeds.
Addresses review feedback: when emergency compaction succeeds in the ContextOverflow recovery path, the context shrinks but last_prompt_tokens still held the pre-compaction value. If the retried call then returned no usage stats (mock/local provider, transient gap), the end-of-turn check would read that stale huge value through effective_context_tokens and fire a redundant compaction immediately after this one. Reset it to None on successful emergency compaction so the check falls back to the fresh post-compaction estimate.
|
Good catch — fixed. On a successful emergency compaction in the |
Summary
The auto-compaction trigger sized the context with a bytes/4 heuristic (
estimate_context_tokens). That under-counts the code/JSON/non-English payloads this agent generates, so a context that has really blown past the model's window can read as "under threshold" — and the next request overflows, which is unrecoverable once the provider rejects it.The provider already returns the exact input size in
usage.prompt_tokens, but it was only emitted as telemetry and discarded. This tracks the most recent value per turn and feeds it into the trigger:At the end-of-turn compaction check,
last_prompt_tokenscovers nearly the entire current context (only the just-produced final assistant message is newer, and the estimate side of themaxincludes that), so the max corrects the heuristic's under-count and compaction fires when it should.Correctness / why it's safe
last_prompt_tokensis a localOption<u32>reset toNoneeachrun_reasoning_loopcall — no stale cross-turn value.last_prompt_tokensin the same iteration. The mid-turn overflow-recovery pathcontinues before the usage update, so the next iteration re-counts the smaller post-compaction context and overwriteslast_prompt_tokens— the pre-compaction value is never observed by the trigger.last_prompt_tokens = None→ falls back to bytes/4. A zero usage block is guarded (if usage.prompt_tokens > 0) so it can't clobber a valid value.maxcan only raise the count toward the ground truth (a request that actually fit), so the worst case is compacting slightly earlier — the intended conservative behavior.Testing
cargo check --all-targets,cargo clippy --release --all-targets(no new warnings),cargo test --lib→ 0 failed.effective_context_tokens_prefers_ground_truth: fallback (None), ground-truth-larger, estimate-larger, andSome(0)never lowers.Out of scope (follow-up)
An OpenAI context-window override for
provider.context_window_tokens()so the window-aware threshold (effective_compaction_threshold) also tightens for OpenAI, not just providers that report a window.Branch merges cleanly into current
main; independently reviewed before upstreaming (see review comment).