[Bug] auto_compress still falls back to the text heuristic for providers that report no usage (MiniMax-M3) — root cause + fix in #1845
Summary
Follow-up to #1764. That issue reported that the auto_compress trigger was fed by
estimateTokensWithPendingAssistant, a text-only heuristic that misses 70-85% of the real
prompt. #1761 fixed the primary half of it by preferring the exact API-reported number
from ContextUsageReporter.GetContextUsage().
This issue is the remaining half: for providers that report no usable usage at all, there is still no exact number, and every turn silently falls back to that same heuristic.
Root cause
1. The provider sends an empty usage block. Measured on MiniMax-M3: every stream-json
assistant event carries
"usage": {"input_tokens": 0, "output_tokens": 0,
"cache_creation_input_tokens": 0, "cache_read_input_tokens": 0}handleAssistant's if used > 0 guard then never writes lastUsage, so
GetContextUsage() returns nil for the entire lifetime of the session. The exact-usage
path introduced in #1761 is correct but has nothing to report, so every single turn
falls through to the heuristic. lastUsage is never written — not "written small".
2. The result event's aggregate cannot stand in for it. Its
cache_read_input_tokens sums every sub-call in the turn (measured: in+cc+cr walking
38377 → 38538 → 38671 within one turn), so it overstates the final prompt by a factor
that grows with the sub-call count — roughly 2x on a short turn, worse on long agentic
turns. Using it would trade an underestimate for an overestimate.
3. The heuristic itself is badly wrong. It counts only cc-connect's own history text,
ignoring tool_use/tool_result blocks and the fixed system-prompt + tools + skills
overhead. On our test instance it read 12,887 for a turn whose real prompt was
37,184 — 2.9x low, and the ratio moves with how much the agent read.
Where the exact number actually lives
The Claude Code JSONL transcript records one self-contained record per API call, and that
record's in + cache_creation + cache_read is exactly that call's prompt size. Unlike the
result aggregate it does not accumulate (verified live: cr 38802 → in+cr 38948 →
next cr 38948). It is the same number behind Claude Code's own ctx N% indicator.
Reading it is cheap as a bounded tail read rather than a scan. Measured across 11,061 real transcripts (2.5 GB total, largest 137 MB):
| percentile | last-line bytes |
|---|---|
| min | 82 |
| p50 | 478 |
| p90 | 668 |
| p99 | 1452 |
| max | 32510 |
So a 4 KiB initial window settles 99.7% of sessions, and doubling only when the record is out of reach covers the rest. A full rescan would be O(n²) — one of our transcripts is 136 MiB, and auto-compress consults this on every turn.
Reproduction
- Any project whose agent is
claudecodebehind an Anthropic-compatible gateway that reports zero usage — MiniMax-M3 is the one we measured. auto_compress.enabled = true, anymax_tokens.- Watch the logs:
GetContextUsage()staysnil, and the trigger value comes from the text-length heuristic no matter how large the real prompt gets. (Before the logging added in the PR, this was invisible — "the agent reported a small prompt" and "the agent reported nothing and we guessed from text" looked identical.)
Proposed fix — PR #1845
- Recover the exact per-call prompt size from the transcript tail: in
handleResultwhen the in-process snapshot is absent, and once at attach time so a--resumed process does not spend its first turn heuristic-blind. The result aggregate is used only for theEventResultbilling fields. - Coerce non-
float64numbers inparseClaudeUsage, so a CLI build emitting token counts as JSON strings cannot silently zero them out either. - Add
UsageSourceReporter+estimate_source/usage_sourceon every decision log, so this ambiguity is visible next time. - Add
auto_compress.allow_heuristic, default false: a turn with no exact usage makes no decision and leaveslastAutoCompressAtuntouched, so the next turn (which does have exact usage) decides on real data. The legacy fallback is preserved behind the opt-in.
Verified on an isolated instance against MiniMax-M3 over a multi-turn session: 9/9
decisions report estimate_source=exact with usage_source=transcript, zero heuristic
fallbacks, zero skips; lastUsage goes from never-written to written once per turn and
tracks real context growth (41656 → 41707 → … → jumps 42,597 → 58,489 when the turn loads
a large file). The one turn that crossed the threshold genuinely fired /compact on real
data, after which the next decision read 37,102.
Refs #1115
Source: chenhg5/cc-connect