Skip to content

fix: compress count_tokens so Claude Code never trips autocompact - #72

Merged
ranxianglei merged 2 commits into
masterfrom
2026-08-09_cc-count-tokens
Aug 9, 2026
Merged

ranxianglei merged 2 commits into
masterfrom
2026-08-09_cc-count-tokens

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

Problem

Claude Code (CC) through the bili proxy suffers context explosion + task drift (upstream issue #68). Root cause (full analysis in dog/billion-context-pi#12):

  • CC sizes its autocompact window off a separate /v1/messages/count_tokens API call.
  • The proxy forwarded count_tokens unchanged (unrecognized path) → relay returned the token count of CC's full uncompressed message list.
  • CC saw a large count → tripped its own autocompact mid-task → summarized away in-progress context → task drift (was doing task A, ended up on task B).
  • Meanwhile the proxy already compresses every /v1/messages turn and reports the compressed usage.input_tokens back to CC. So count_tokens was the one remaining leak — the only signal CC had that still reflected the uncompressed size.

Fix

Route /v1/messages/count_tokens through the same compression pipeline as /v1/messages, so the relay returns the post-compression token count. CC then sizes its window off the small compressed count and never trips autocompact.

  • New prepareCountTokens() runs the read-only processTurn (identical prune/inject/tag path as prepareAnthropic) but discards the returned state — count_tokens is a query and must not mutate session state, create blocks, or nudge. Verified processTurn does not mutate the input state object.
  • compressInjected: false + empty processedMessages → forward() does plain pipeThrough of the relay's non-streaming {input_tokens} JSON response.
  • Escape hatch: ACP_COUNT_TOKENS_PASSTHROUGH=1 restores the old unchanged-forward behavior.

Why this is correct

CC's autocompact is redundant in the proxy architecture: the proxy already compresses for the upstream model. CC's own compaction is pure double-compression — information loss (task drift) with zero model benefit. Making count_tokens report the compressed count aligns it with the /v1/messages usage CC already sees, so CC never believes it is near the limit.

Trade-off (explicit): CC's local context now grows unbounded (it never autocompacts; the upstream never returns prompt-too-long since it sees the compressed body). Memory grows at MB scale for long sessions — acceptable and is the intended behavior.

Tests

tests/count-tokens.test.ts (4 new):

  1. With coverage: pruned output < input, summary injected, log delta present.
  2. Read-only: session state JSON byte-identical before/after; block count + coverage unchanged.
  3. No blocks: output == input.
  4. Prune failure (bogus state): logs fallback warning, forwards original body.

Full suite: 173 pass, 0 fail. npm run typecheck clean. npm run build OK.

Note on Codex

Codex does not need an equivalent fix — it triggers compaction off response.completed.usage.input_tokens (no separate count_tokens call), which the proxy already returns compressed (verified stream-responses.ts:227 passes the completed event through verbatim).

Route Anthropic /v1/messages/count_tokens through the proxy compression
pipeline (read-only processTurn, state discarded) so the relay returns the
POST-compression token count. CC sizes its autocompact window off this count;
reporting the compressed count keeps CC below its threshold indefinitely,
eliminating the autocompact-induced task drift reported in #68.

prepareCountTokens mirrors exactly what the next /v1/messages would forward
(covered messages pruned, summaries injected, refs tagged) but does NOT mutate
session.state, create blocks, or nudge. forward() does plain pipeThrough for
the non-streaming JSON response (compressInjected=false).

ACP_COUNT_TOKENS_PASSTHROUGH=1 restores the old unchanged-forward behavior.
- Extract isCountTokensRequest() (pure) so the routing-detection + escape
  hatch are unit-testable without spinning up an HTTP server (repo tests are
  unit-level). Covers path-variant detection, non-count_tokens rejection, and
  ACP_COUNT_TOKENS_PASSTHROUGH=1.
- Deepen the read-only test to a full JSON snapshot of session.state + stats,
  catching nested mutations (block.survivedCount/generation, nudge baselines)
  that the previous field-by-field check missed.
- Document the cross-repo read-only invariant (relies on acp-kernel syncBlocks
  deep-cloning) and the load-bearing compressInjected:false on the Prepared
  return (prevents the usage-capture tail from poisoning lastInputTokens).
- NIT: drop redundant endsWith clause, drop unnecessary `as Prepared` casts.

176 tests pass (173 + 3 new detection/escape-hatch).
@ranxianglei
ranxianglei merged commit 6481b4b into master Aug 9, 2026
3 checks passed
ranxianglei added a commit that referenced this pull request Aug 9, 2026
Ported master's reasoning-through-compression (#71) and count_tokens (#72)
features onto PR#73's layout-based patching architecture:

responses.ts:
- Removed 'reasoning' from OPAQUE_ITEM_TYPES (master: reasoning is
  conversation history, routed through compression, not preserved verbatim)
- Added droppedReasoning counter + shouldDropAllReasoning() escape hatch
  (ACP_REASONING_KEEP=none)
- Added 'reasoning' case to responsesToCore switch: creates a tracked
  BiliMessage (contentType 'reasoning', rawResponsesItem carried) so the
  kernel hides it once its turn is summarized; dropped reasoning uses
  'continue' to skip layout.push so it is fully removed
- Added reasoning re-emit to coreToResponses (assistant contentType
  'reasoning' -> push raw verbatim)
- patchOriginalItem needs no change: reasoning items are preserved via
  slot.original when their turn is live, dropped (nextById miss) when
  compressed -- correct by construction

server.ts:
- Added countTokens detection (isCountTokensRequest) + prepareCountTokens
  (read-only processTurn mirror so count_tokens reports post-compression
  count; ACP_COUNT_TOKENS_PASSTHROUGH=1 escape hatch)
- Merged prepare ternary: countTokens -> anthropic -> openai ->
  responsesCompact -> responses

Validation: typecheck clean, 216 tests pass (incl. 9 reasoning + 3
count_tokens + custom_tool_call fix), build ok.
@ranxianglei
ranxianglei deleted the 2026-08-09_cc-count-tokens branch October 15, 2026 05:31
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.

1 participant