mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-27 15:13:37 +08:00
Implement 6 cache optimization improvements inspired by Reasonix: 1. **CCH attribution header**: Disabled by default to prevent per-turn cache invalidation. The `x-anthropic-billing-header` was embedded in `system[0]` of every prompt, and its xxHash value changed per-turn, causing full cache prefix misses. Users can re-enable via env var `CLAUDE_CODE_ATTRIBUTION_HEADER` or GrowthBook flag. 2. **Multi-level percentage compaction thresholds**: Supplement the existing fixed 13K buffer with percentage-based levels (75%/78%/80%/ 90%) that work correctly across all context window sizes from 200K to 1M+. The fixed buffer remains as "final defense" at 93-98%. 3. **Turn-start token pre-estimation**: Feature-flagged checkpoint (`TURN_START_PRE_ESTIMATION`) before API calls to detect when context approaches 90% capacity before the passive check catches it. 4. **Cache-aligned compaction**: Already implemented (CacheSafeParams in forkedAgent.ts). No changes needed — verified. 5. **Token-based tool result truncation**: `truncateToolResultByTokens()` replaces char-based counting with rough token estimation for CJK- aware truncation at clean line boundaries. 6. **Minimum savings check**: `isCompactionWorthwhile()` skips compaction when the head portion is less than 30% of the context window, preventing waste when the summary costs nearly as much as it saves. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
152 lines
5.9 KiB
TypeScript
152 lines
5.9 KiB
TypeScript
import { expect, test } from 'bun:test'
|
|
import {
|
|
COMPACT_NORMAL_FOLD_RATIO,
|
|
COMPACT_AGGRESSIVE_FOLD_RATIO,
|
|
COMPACT_FORCE_SUMMARY_RATIO,
|
|
COMPACT_PRECHECK_FOLD_RATIO,
|
|
COMPACT_NORMAL_FOLD_TAIL_RATIO,
|
|
COMPACT_AGGRESSIVE_FOLD_TAIL_RATIO,
|
|
MIN_COMPACTION_SAVINGS_RATIO,
|
|
isCompactionWorthwhile,
|
|
} from '../src/services/compact/autoCompact.js'
|
|
import { truncateToolResultByTokens } from '../src/utils/toolResultStorage.js'
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Constant validation — ensure thresholds stay at their expected values
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test('percentage thresholds are correctly ordered', () => {
|
|
expect(COMPACT_NORMAL_FOLD_RATIO).toBe(0.75)
|
|
expect(COMPACT_AGGRESSIVE_FOLD_RATIO).toBe(0.78)
|
|
expect(COMPACT_FORCE_SUMMARY_RATIO).toBe(0.80)
|
|
expect(COMPACT_PRECHECK_FOLD_RATIO).toBe(0.90)
|
|
|
|
// Thresholds must be monotonically increasing
|
|
expect(COMPACT_NORMAL_FOLD_RATIO).toBeLessThan(COMPACT_AGGRESSIVE_FOLD_RATIO)
|
|
expect(COMPACT_AGGRESSIVE_FOLD_RATIO).toBeLessThan(
|
|
COMPACT_FORCE_SUMMARY_RATIO,
|
|
)
|
|
expect(COMPACT_FORCE_SUMMARY_RATIO).toBeLessThan(COMPACT_PRECHECK_FOLD_RATIO)
|
|
})
|
|
|
|
test('tail budget ratios are correctly ordered', () => {
|
|
expect(COMPACT_NORMAL_FOLD_TAIL_RATIO).toBe(0.20)
|
|
expect(COMPACT_AGGRESSIVE_FOLD_TAIL_RATIO).toBe(0.10)
|
|
|
|
// Normal fold should preserve more tail than aggressive fold
|
|
expect(COMPACT_AGGRESSIVE_FOLD_TAIL_RATIO).toBeLessThan(
|
|
COMPACT_NORMAL_FOLD_TAIL_RATIO,
|
|
)
|
|
})
|
|
|
|
test('minimum savings ratio is a reasonable value', () => {
|
|
expect(MIN_COMPACTION_SAVINGS_RATIO).toBe(0.30)
|
|
expect(MIN_COMPACTION_SAVINGS_RATIO).toBeGreaterThan(0)
|
|
expect(MIN_COMPACTION_SAVINGS_RATIO).toBeLessThan(1)
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// isCompactionWorthwhile
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test('isCompactionWorthwhile returns true when most of context is occupied', () => {
|
|
// 90K tokens in 100K window → 90% occupied → worthwhile
|
|
expect(isCompactionWorthwhile(90_000, 100_000)).toBe(true)
|
|
})
|
|
|
|
test('isCompactionWorthwhile returns true at the boundary (30%)', () => {
|
|
// 30K tokens in 100K window → exactly 30% → still worthwhile
|
|
expect(isCompactionWorthwhile(30_000, 100_000)).toBe(true)
|
|
})
|
|
|
|
test('isCompactionWorthwhile returns false when below threshold', () => {
|
|
// 20K tokens in 100K window → 20% → not worthwhile
|
|
expect(isCompactionWorthwhile(20_000, 100_000)).toBe(false)
|
|
})
|
|
|
|
test('isCompactionWorthwhile returns true when tokens exceed window (emergency)', () => {
|
|
// Emergency: tokens exceed the context window — always worthwhile
|
|
expect(isCompactionWorthwhile(105_000, 100_000)).toBe(true)
|
|
})
|
|
|
|
test('isCompactionWorthwhile handles large 1M context window', () => {
|
|
// 400K tokens in 1M window → 40% → worthwhile
|
|
expect(isCompactionWorthwhile(400_000, 1_000_000)).toBe(true)
|
|
|
|
// 200K tokens in 1M window → 20% → not worthwhile
|
|
expect(isCompactionWorthwhile(200_000, 1_000_000)).toBe(false)
|
|
|
|
// 205K tokens in 200K window → >100% → emergency → worthwhile
|
|
expect(isCompactionWorthwhile(205_000, 200_000)).toBe(true)
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// truncateToolResultByTokens
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test('truncateToolResultByTokens returns content unchanged when under limit', () => {
|
|
const content = 'short content'
|
|
const result = truncateToolResultByTokens(content, 100)
|
|
expect(result.wasTruncated).toBe(false)
|
|
expect(result.truncated).toBe(content)
|
|
})
|
|
|
|
test('truncateToolResultByTokens returns content unchanged when exactly at limit', () => {
|
|
// 400 bytes → ~100 tokens at 4 bytes/token
|
|
const content = 'A'.repeat(400)
|
|
const result = truncateToolResultByTokens(content, 100)
|
|
// May or may not truncate depending on rough estimate — but marker
|
|
// should not appear when content is small enough
|
|
if (!result.wasTruncated) {
|
|
expect(result.truncated).toBe(content)
|
|
}
|
|
})
|
|
|
|
test('truncateToolResultByTokens truncates when well above limit', () => {
|
|
// ~50K chars → ~12,500 tokens at 4 bytes/token
|
|
const content = 'A'.repeat(50_000)
|
|
const result = truncateToolResultByTokens(content, 100)
|
|
expect(result.wasTruncated).toBe(true)
|
|
expect(result.truncated.length).toBeLessThan(content.length)
|
|
expect(result.truncated).toContain('Content truncated')
|
|
})
|
|
|
|
test('truncateToolResultByTokens includes marker in truncated content', () => {
|
|
const content = 'B'.repeat(10_000)
|
|
const result = truncateToolResultByTokens(content, 100)
|
|
expect(result.wasTruncated).toBe(true)
|
|
expect(result.truncated).toContain(
|
|
'Content truncated',
|
|
)
|
|
})
|
|
|
|
test('truncateToolResultByTokens handles CJK content', () => {
|
|
// CJK characters are ~1-3 tokens each, so char/4 underestimates tokens.
|
|
// The function should still gracefully handle and truncate CJK content.
|
|
const content = '中文测试内容'.repeat(5_000)
|
|
const result = truncateToolResultByTokens(content, 500)
|
|
expect(result.wasTruncated).toBe(true)
|
|
expect(result.truncated.length).toBeLessThan(content.length)
|
|
})
|
|
|
|
test('truncateToolResultByTokens preserves content integrity', () => {
|
|
const content = 'Hello World\nThis is a test\n'.repeat(200)
|
|
const result = truncateToolResultByTokens(content, 100)
|
|
expect(result.wasTruncated).toBe(true)
|
|
// Should not start with partial line when possible
|
|
// (if a newline was found within 70% of the budget)
|
|
const truncatedPart = result.truncated.replace(
|
|
/\n\n\[Content truncated.*\]$/s,
|
|
'',
|
|
)
|
|
// Content before the marker should be a prefix of the original
|
|
expect(content.startsWith(truncatedPart)).toBe(true)
|
|
})
|
|
|
|
test('truncateToolResultByTokens handles empty content', () => {
|
|
const result = truncateToolResultByTokens('', 100)
|
|
expect(result.wasTruncated).toBe(false)
|
|
expect(result.truncated).toBe('')
|
|
expect(result.estimatedTokens).toBe(0)
|
|
})
|