Summary
Auto-compaction can fire far below the configured trigger because estimateContextTokens falls back to input + output + cacheRead + cacheWrite when usage.totalTokens is falsy. When the usage object it receives holds turn-aggregated values (multiple API calls in one agent turn, each re-sending ~150k cached context), that sum counts the same cached tokens once per call — producing a context "estimate" several times larger than the real context.
Observed: compaction fired at ~164k real context on a session configured to trigger at 180k (300k window, reserveTokens: 120000), because the estimator computed 931,132.
Environment
- OpenClaw 2026.6.11, Linux container, Anthropic provider (
claude-opus-4-8), compaction.mode: safeguard
agents.defaults.contextTokens: 300000, compaction.reserveTokens: 120000
Token-exact evidence
The last successful assistant message before the compaction (persisted session JSONL) carried this usage record:
input=12 output=15,104 cacheRead=819,661 cacheWrite=93,130 totalTokens=163,978
Note cacheRead=819,661 — that's an aggregate across the ~12 API calls of one long tool-loop turn (each call re-reads ~150k of cache), while totalTokens=163,978 is the real final context.
- Fallback formula:
12 + 15,104 + 819,661 + 93,130 = 927,907
-
- trailing-message estimates after that usage record ≈
3,225
- = 931,132 — exactly the number the compaction banner reported ("931,132 → 28,936 tokens")
shouldCompact(931132, 300000, {reserveTokens: 120000}) → fires. With the correct 163,978 + 3,225 = 167,203 it would not have (< 180k).
Code path (dist bundle, 2026.6.11)
function calculateContextTokens(usage) {
return usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
}
// estimateContextTokens() → calculateContextTokens(lastAssistantUsage) + trailing estimates
// shouldCompact(contextTokens, contextWindow, settings) → contextTokens > contextWindow - reserveTokens
Since the persisted record does contain a correct totalTokens, the object handed to the estimator at trigger-evaluation time appears to be an in-memory turn accumulator whose totalTokens is unset/0 — so the fallback engages on aggregated fields. (The turn in question also contained several stopReason: "error" zero-usage entries from transient provider errors, in case that's relevant to which object is consulted.)
Impact
- Sessions doing long multi-call turns (agentic tool loops with prompt caching) compact far earlier than configured — in our case at 55% of the intended trigger, mid-turn-sequence, with the usual quality loss from an unplanned compaction.
- The compaction banner also displays the inflated number, which makes the event look like a context explosion and misleads debugging.
Suggested fix directions
- Prefer the last call's real context (
totalTokens, or input+cacheRead+cacheWrite of the final call only) over summed turn aggregates; and/or ensure the in-memory accumulator populates totalTokens before compaction evaluation.
- Skip zero-usage error entries when locating the "last assistant usage".
Summary
Auto-compaction can fire far below the configured trigger because
estimateContextTokensfalls back toinput + output + cacheRead + cacheWritewhenusage.totalTokensis falsy. When the usage object it receives holds turn-aggregated values (multiple API calls in one agent turn, each re-sending ~150k cached context), that sum counts the same cached tokens once per call — producing a context "estimate" several times larger than the real context.Observed: compaction fired at ~164k real context on a session configured to trigger at 180k (300k window,
reserveTokens: 120000), because the estimator computed 931,132.Environment
claude-opus-4-8),compaction.mode: safeguardagents.defaults.contextTokens: 300000,compaction.reserveTokens: 120000Token-exact evidence
The last successful assistant message before the compaction (persisted session JSONL) carried this usage record:
Note
cacheRead=819,661— that's an aggregate across the ~12 API calls of one long tool-loop turn (each call re-reads ~150k of cache), whiletotalTokens=163,978is the real final context.12 + 15,104 + 819,661 + 93,130 = 927,9073,225shouldCompact(931132, 300000, {reserveTokens: 120000})→ fires. With the correct163,978 + 3,225 = 167,203it would not have (< 180k).Code path (dist bundle, 2026.6.11)
Since the persisted record does contain a correct
totalTokens, the object handed to the estimator at trigger-evaluation time appears to be an in-memory turn accumulator whosetotalTokensis unset/0 — so the fallback engages on aggregated fields. (The turn in question also contained severalstopReason: "error"zero-usage entries from transient provider errors, in case that's relevant to which object is consulted.)Impact
Suggested fix directions
totalTokens, orinput+cacheRead+cacheWriteof the final call only) over summed turn aggregates; and/or ensure the in-memory accumulator populatestotalTokensbefore compaction evaluation.