Skip to content

fix(recommend): unify effective-gate units to real chars (CJK regression) - #81

Merged
ranxianglei merged 1 commit into
masterfrom
2026-08-16_effective-gate-chars
Aug 16, 2026
Merged

ranxianglei merged 1 commit into
masterfrom
2026-08-16_effective-gate-chars

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

Problem

Review of the last 48h of merged PRs (#70/#77) found a unit mismatch between the two minCompressRange gates:

Side Unit Location
apply (authoritative) real chars — msg.text.length src/compress.ts pre-check
recommend/nudge tokens * 4 proxy pendingByTier, mergeRangesToThreshold

tokens*4 == chars only holds for the default chars/4 estimator. The kernel explicitly encourages hosts to inject a real tokenizer (createCore({ countTokens })), and its own estimateTokensFast is CJK-aware (≈1 token/char). With such a tokenizer:

  • tokens*4 overestimates range size ~4×
  • a ~1300-char CJK range looks "effective" to nudge → injected recommendation
  • the model calls compress → apply side counts 1300 chars < 5000 → atomic rejection (Total compressible content too small)

This is exactly the "recommended-then-rejected" failure mode #70's effective gate was built to eliminate — it regressed for CJK sessions. Default hosts see no change (units coincide), which is why existing tests didn't catch it.

Fix

Unify on the apply side's unit (real chars):

  • CompressibleRange gains optional chars (sum of message text lengths), always set by buildCompressibleRanges / mergeBatch
  • mergeRangesToThreshold batches by accumulated chars (sub-threshold tail behavior unchanged)
  • pendingByTier T1 effective filter: (r.chars ?? r.tokens * 4) >= minCompressRange
  • Hand-built ranges without chars fall back to the historical tokens*4 estimate — no breaking type change, legacy fixtures behave identically

Tests

New tests/effective-gate-chars.test.ts (CJK identity tokenizer, minCompressRange: 5000):

  1. 3000-char CJK session at 95% usage → nudge suppressed (no tier has effective compressible content), and applyCompression of the same range rejects with "too small" — both gates now agree
  2. 6000-char control → nudge injects T1, applyCompression creates the block — both gates agree
  3. mergeRangesToThreshold: legacy fallback (tokens: 1250 w/o chars) still clears the 5000 threshold
ℹ tests 338   ℹ pass 338   ℹ fail 0

tsc clean, build clean.

The apply-side minCompressRange gate counts raw characters
(msg.text.length), but the recommend-side gates approximated chars with
tokens*4 — only equivalent under the default chars/4 estimator. Hosts
injecting a CJK-aware tokenizer (≈1 token/char, e.g. the kernel's own
estimateTokensFast) made tokens*4 a ~4x overestimate: nudge offered
ranges the kernel then atomically rejected with 'Total compressible
content too small' — exactly the recommend/apply disagreement the
effective gate exists to prevent.

- CompressibleRange gains optional 'chars' (sum of message text lengths),
  always set by buildCompressibleRanges / mergeBatch
- mergeRangesToThreshold batches by real chars (tail batch unchanged)
- pendingByTier T1 effective filter uses chars
- hand-built ranges without chars fall back to tokens*4 (backwards compat)

Regression tests: CJK tokenizer below/above threshold — nudge suppression
matches apply rejection; control case compresses successfully.
@ranxianglei
ranxianglei merged commit b3ffacf into master Aug 16, 2026
3 checks passed
@ranxianglei ranxianglei mentioned this pull request Aug 16, 2026
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