mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-23 08:45:50 +00:00
feat: 添加 compact 缓存与上下文压缩增强
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
118
src/services/compact/__tests__/cachedMicrocompact.test.ts
Normal file
118
src/services/compact/__tests__/cachedMicrocompact.test.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import { describe, test, expect, beforeEach } from 'bun:test'
|
||||||
|
import {
|
||||||
|
createCachedMCState,
|
||||||
|
registerToolResult,
|
||||||
|
getToolResultsToDelete,
|
||||||
|
createCacheEditsBlock,
|
||||||
|
markToolsSentToAPI,
|
||||||
|
resetCachedMCState,
|
||||||
|
isCachedMicrocompactEnabled,
|
||||||
|
isModelSupportedForCacheEditing,
|
||||||
|
type CachedMCState,
|
||||||
|
} from '../cachedMicrocompact.js'
|
||||||
|
|
||||||
|
describe('cachedMicrocompact', () => {
|
||||||
|
let state: CachedMCState
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
state = createCachedMCState()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('createCachedMCState returns clean state', () => {
|
||||||
|
expect(state.registeredTools.size).toBe(0)
|
||||||
|
expect(state.toolOrder).toEqual([])
|
||||||
|
expect(state.deletedRefs.size).toBe(0)
|
||||||
|
expect(state.pinnedEdits).toEqual([])
|
||||||
|
expect(state.toolsSentToAPI).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('registerToolResult tracks tool IDs in order', () => {
|
||||||
|
registerToolResult(state, 'tool-1')
|
||||||
|
registerToolResult(state, 'tool-2')
|
||||||
|
registerToolResult(state, 'tool-3')
|
||||||
|
expect(state.registeredTools.size).toBe(3)
|
||||||
|
expect(state.toolOrder).toEqual(['tool-1', 'tool-2', 'tool-3'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('getToolResultsToDelete returns empty when below threshold', () => {
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
registerToolResult(state, `tool-${i}`)
|
||||||
|
}
|
||||||
|
const toDelete = getToolResultsToDelete(state)
|
||||||
|
expect(toDelete).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('getToolResultsToDelete returns oldest when above threshold', () => {
|
||||||
|
for (let i = 0; i < 12; i++) {
|
||||||
|
registerToolResult(state, `tool-${i}`)
|
||||||
|
}
|
||||||
|
const toDelete = getToolResultsToDelete(state)
|
||||||
|
// Should suggest deleting oldest, keeping recent
|
||||||
|
expect(toDelete.length).toBeGreaterThan(0)
|
||||||
|
// Should not include the most recent tools
|
||||||
|
expect(toDelete).not.toContain('tool-11')
|
||||||
|
expect(toDelete).not.toContain('tool-10')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('createCacheEditsBlock generates correct structure', () => {
|
||||||
|
for (let i = 0; i < 12; i++) {
|
||||||
|
registerToolResult(state, `tool-${i}`)
|
||||||
|
}
|
||||||
|
const toDelete = getToolResultsToDelete(state)
|
||||||
|
const block = createCacheEditsBlock(state, toDelete)
|
||||||
|
if (block) {
|
||||||
|
expect(block.type).toBe('cache_edits')
|
||||||
|
expect(block.edits.length).toBe(toDelete.length)
|
||||||
|
for (const edit of block.edits) {
|
||||||
|
expect(edit.type).toBe('delete_tool_result')
|
||||||
|
expect(typeof edit.tool_use_id).toBe('string')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('createCacheEditsBlock returns null for empty list', () => {
|
||||||
|
const block = createCacheEditsBlock(state, [])
|
||||||
|
expect(block).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('already deleted tools are not suggested again', () => {
|
||||||
|
for (let i = 0; i < 12; i++) {
|
||||||
|
registerToolResult(state, `tool-${i}`)
|
||||||
|
}
|
||||||
|
const first = getToolResultsToDelete(state)
|
||||||
|
// Simulate deletion
|
||||||
|
for (const id of first) {
|
||||||
|
state.deletedRefs.add(id)
|
||||||
|
}
|
||||||
|
const second = getToolResultsToDelete(state)
|
||||||
|
// Should not re-suggest already deleted
|
||||||
|
for (const id of first) {
|
||||||
|
expect(second).not.toContain(id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('markToolsSentToAPI sets flag', () => {
|
||||||
|
expect(state.toolsSentToAPI).toBe(false)
|
||||||
|
markToolsSentToAPI(state)
|
||||||
|
expect(state.toolsSentToAPI).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('resetCachedMCState clears everything', () => {
|
||||||
|
registerToolResult(state, 'tool-1')
|
||||||
|
markToolsSentToAPI(state)
|
||||||
|
resetCachedMCState(state)
|
||||||
|
expect(state.registeredTools.size).toBe(0)
|
||||||
|
expect(state.toolOrder).toEqual([])
|
||||||
|
expect(state.toolsSentToAPI).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('isModelSupportedForCacheEditing accepts Claude 4.x', () => {
|
||||||
|
expect(isModelSupportedForCacheEditing('claude-opus-4-6')).toBe(true)
|
||||||
|
expect(isModelSupportedForCacheEditing('claude-sonnet-4-6')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('isModelSupportedForCacheEditing rejects old models', () => {
|
||||||
|
expect(isModelSupportedForCacheEditing('claude-2')).toBe(false)
|
||||||
|
expect(isModelSupportedForCacheEditing('gpt-4')).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -86,27 +86,24 @@ export function getAPIContextManagement(options?: {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tool clearing strategies are ant-only
|
// Tool clearing: default enabled for all users (upstream gates on USER_TYPE=ant).
|
||||||
if (process.env.USER_TYPE !== 'ant') {
|
// Opt out via USE_API_CLEAR_TOOL_RESULTS=0 / USE_API_CLEAR_TOOL_USES=0.
|
||||||
return strategies.length > 0 ? { edits: strategies } : undefined
|
const useClearToolResults =
|
||||||
}
|
process.env.USE_API_CLEAR_TOOL_RESULTS !== undefined
|
||||||
|
? isEnvTruthy(process.env.USE_API_CLEAR_TOOL_RESULTS)
|
||||||
const useClearToolResults = isEnvTruthy(
|
: true
|
||||||
process.env.USE_API_CLEAR_TOOL_RESULTS,
|
|
||||||
)
|
|
||||||
const useClearToolUses = isEnvTruthy(process.env.USE_API_CLEAR_TOOL_USES)
|
const useClearToolUses = isEnvTruthy(process.env.USE_API_CLEAR_TOOL_USES)
|
||||||
|
|
||||||
// If no tool clearing strategy is enabled, return early
|
|
||||||
if (!useClearToolResults && !useClearToolUses) {
|
if (!useClearToolResults && !useClearToolUses) {
|
||||||
return strategies.length > 0 ? { edits: strategies } : undefined
|
return strategies.length > 0 ? { edits: strategies } : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useClearToolResults) {
|
if (useClearToolResults) {
|
||||||
const triggerThreshold = process.env.API_MAX_INPUT_TOKENS
|
const triggerThreshold = process.env.API_MAX_INPUT_TOKENS
|
||||||
? parseInt(process.env.API_MAX_INPUT_TOKENS)
|
? parseInt(process.env.API_MAX_INPUT_TOKENS, 10)
|
||||||
: DEFAULT_MAX_INPUT_TOKENS
|
: DEFAULT_MAX_INPUT_TOKENS
|
||||||
const keepTarget = process.env.API_TARGET_INPUT_TOKENS
|
const keepTarget = process.env.API_TARGET_INPUT_TOKENS
|
||||||
? parseInt(process.env.API_TARGET_INPUT_TOKENS)
|
? parseInt(process.env.API_TARGET_INPUT_TOKENS, 10)
|
||||||
: DEFAULT_TARGET_INPUT_TOKENS
|
: DEFAULT_TARGET_INPUT_TOKENS
|
||||||
|
|
||||||
const strategy: ContextEditStrategy = {
|
const strategy: ContextEditStrategy = {
|
||||||
@@ -127,10 +124,10 @@ export function getAPIContextManagement(options?: {
|
|||||||
|
|
||||||
if (useClearToolUses) {
|
if (useClearToolUses) {
|
||||||
const triggerThreshold = process.env.API_MAX_INPUT_TOKENS
|
const triggerThreshold = process.env.API_MAX_INPUT_TOKENS
|
||||||
? parseInt(process.env.API_MAX_INPUT_TOKENS)
|
? parseInt(process.env.API_MAX_INPUT_TOKENS, 10)
|
||||||
: DEFAULT_MAX_INPUT_TOKENS
|
: DEFAULT_MAX_INPUT_TOKENS
|
||||||
const keepTarget = process.env.API_TARGET_INPUT_TOKENS
|
const keepTarget = process.env.API_TARGET_INPUT_TOKENS
|
||||||
? parseInt(process.env.API_TARGET_INPUT_TOKENS)
|
? parseInt(process.env.API_TARGET_INPUT_TOKENS, 10)
|
||||||
: DEFAULT_TARGET_INPUT_TOKENS
|
: DEFAULT_TARGET_INPUT_TOKENS
|
||||||
|
|
||||||
const strategy: ContextEditStrategy = {
|
const strategy: ContextEditStrategy = {
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
// Auto-generated stub — replace with real implementation
|
|
||||||
export {};
|
|
||||||
|
|
||||||
export type CachedMCState = {
|
export type CachedMCState = {
|
||||||
registeredTools: Set<string>
|
registeredTools: Set<string>
|
||||||
toolOrder: string[]
|
toolOrder: string[]
|
||||||
@@ -19,19 +16,97 @@ export type PinnedCacheEdits = {
|
|||||||
block: CacheEditsBlock
|
block: CacheEditsBlock
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isCachedMicrocompactEnabled: () => boolean = () => false;
|
const TRIGGER_THRESHOLD = 10
|
||||||
export const isModelSupportedForCacheEditing: (model: string) => boolean = () => false;
|
const KEEP_RECENT = 5
|
||||||
export const getCachedMCConfig: () => { triggerThreshold: number; keepRecent: number } = () => ({ triggerThreshold: 0, keepRecent: 0 });
|
|
||||||
export const createCachedMCState: () => CachedMCState = () => ({
|
/**
|
||||||
|
* Returns true when the CLAUDE_CACHED_MICROCOMPACT env var is set to '1'
|
||||||
|
* or the feature is explicitly enabled.
|
||||||
|
*/
|
||||||
|
export function isCachedMicrocompactEnabled(): boolean {
|
||||||
|
return process.env.CLAUDE_CACHED_MICROCOMPACT === '1'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true for Claude 4.x models that support cache_edits.
|
||||||
|
*/
|
||||||
|
export function isModelSupportedForCacheEditing(model: string): boolean {
|
||||||
|
return /claude-[a-z]+-4[-\d]/.test(model)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCachedMCConfig(): {
|
||||||
|
triggerThreshold: number
|
||||||
|
keepRecent: number
|
||||||
|
} {
|
||||||
|
return { triggerThreshold: TRIGGER_THRESHOLD, keepRecent: KEEP_RECENT }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createCachedMCState(): CachedMCState {
|
||||||
|
return {
|
||||||
registeredTools: new Set(),
|
registeredTools: new Set(),
|
||||||
toolOrder: [],
|
toolOrder: [],
|
||||||
deletedRefs: new Set(),
|
deletedRefs: new Set(),
|
||||||
pinnedEdits: [],
|
pinnedEdits: [],
|
||||||
toolsSentToAPI: false,
|
toolsSentToAPI: false,
|
||||||
});
|
}
|
||||||
export const markToolsSentToAPI: (state: CachedMCState) => void = () => {};
|
}
|
||||||
export const resetCachedMCState: (state: CachedMCState) => void = () => {};
|
|
||||||
export const registerToolResult: (state: CachedMCState, toolId: string) => void = () => {};
|
export function markToolsSentToAPI(state: CachedMCState): void {
|
||||||
export const registerToolMessage: (state: CachedMCState, groupIds: string[]) => void = () => {};
|
state.toolsSentToAPI = true
|
||||||
export const getToolResultsToDelete: (state: CachedMCState) => string[] = () => [];
|
}
|
||||||
export const createCacheEditsBlock: (state: CachedMCState, toolIds: string[]) => CacheEditsBlock | null = () => null;
|
|
||||||
|
export function resetCachedMCState(state: CachedMCState): void {
|
||||||
|
state.registeredTools.clear()
|
||||||
|
state.toolOrder = []
|
||||||
|
state.deletedRefs.clear()
|
||||||
|
state.pinnedEdits = []
|
||||||
|
state.toolsSentToAPI = false
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerToolResult(state: CachedMCState, toolId: string): void {
|
||||||
|
if (!state.registeredTools.has(toolId)) {
|
||||||
|
state.registeredTools.add(toolId)
|
||||||
|
state.toolOrder.push(toolId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerToolMessage(
|
||||||
|
state: CachedMCState,
|
||||||
|
groupIds: string[],
|
||||||
|
): void {
|
||||||
|
for (const id of groupIds) {
|
||||||
|
registerToolResult(state, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the tool IDs that should be deleted (oldest first) to bring
|
||||||
|
* the count below the threshold, excluding already-deleted tools and
|
||||||
|
* the most recently seen ones.
|
||||||
|
*/
|
||||||
|
export function getToolResultsToDelete(state: CachedMCState): string[] {
|
||||||
|
const { triggerThreshold, keepRecent } = getCachedMCConfig()
|
||||||
|
const active = state.toolOrder.filter(id => !state.deletedRefs.has(id))
|
||||||
|
if (active.length <= triggerThreshold) return []
|
||||||
|
// Keep the last keepRecent tools
|
||||||
|
const toDelete = active.slice(0, active.length - keepRecent)
|
||||||
|
return toDelete
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a cache_edits block that deletes the given tool result IDs.
|
||||||
|
* Returns null if toolIds is empty.
|
||||||
|
*/
|
||||||
|
export function createCacheEditsBlock(
|
||||||
|
state: CachedMCState,
|
||||||
|
toolIds: string[],
|
||||||
|
): CacheEditsBlock | null {
|
||||||
|
if (toolIds.length === 0) return null
|
||||||
|
return {
|
||||||
|
type: 'cache_edits',
|
||||||
|
edits: toolIds.map(id => ({
|
||||||
|
type: 'delete_tool_result',
|
||||||
|
tool_use_id: id,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export interface DrainResult {
|
|||||||
messages: Message[]
|
messages: Message[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStats: () => ContextCollapseStats = (() => ({
|
export const getStats: () => ContextCollapseStats = () => ({
|
||||||
collapsedSpans: 0,
|
collapsedSpans: 0,
|
||||||
collapsedMessages: 0,
|
collapsedMessages: 0,
|
||||||
stagedSpans: 0,
|
stagedSpans: 0,
|
||||||
@@ -38,29 +38,38 @@ export const getStats: () => ContextCollapseStats = (() => ({
|
|||||||
emptySpawnWarningEmitted: false,
|
emptySpawnWarningEmitted: false,
|
||||||
totalEmptySpawns: 0,
|
totalEmptySpawns: 0,
|
||||||
},
|
},
|
||||||
}));
|
})
|
||||||
|
|
||||||
export const isContextCollapseEnabled: () => boolean = (() => false);
|
let _contextCollapseEnabled = false
|
||||||
|
|
||||||
export const subscribe: (callback: () => void) => () => void = ((_callback: () => void) => () => {});
|
export function isContextCollapseEnabled(): boolean {
|
||||||
|
return _contextCollapseEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
export const subscribe: (callback: () => void) => () => void =
|
||||||
|
(_callback: () => void) => () => {}
|
||||||
|
|
||||||
export const applyCollapsesIfNeeded: (
|
export const applyCollapsesIfNeeded: (
|
||||||
messages: Message[],
|
messages: Message[],
|
||||||
toolUseContext: ToolUseContext,
|
toolUseContext: ToolUseContext,
|
||||||
querySource: QuerySource,
|
querySource: QuerySource,
|
||||||
) => Promise<CollapseResult> = (async (messages: Message[]) => ({ messages }));
|
) => Promise<CollapseResult> = async (messages: Message[]) => ({ messages })
|
||||||
|
|
||||||
export const isWithheldPromptTooLong: (
|
export const isWithheldPromptTooLong: (
|
||||||
message: Message,
|
message: Message,
|
||||||
isPromptTooLongMessage: (msg: Message) => boolean,
|
isPromptTooLongMessage: (msg: Message) => boolean,
|
||||||
querySource: QuerySource,
|
querySource: QuerySource,
|
||||||
) => boolean = (() => false);
|
) => boolean = () => false
|
||||||
|
|
||||||
export const recoverFromOverflow: (
|
export const recoverFromOverflow: (
|
||||||
messages: Message[],
|
messages: Message[],
|
||||||
querySource: QuerySource,
|
querySource: QuerySource,
|
||||||
) => DrainResult = ((messages: Message[]) => ({ committed: 0, messages }));
|
) => DrainResult = (messages: Message[]) => ({ committed: 0, messages })
|
||||||
|
|
||||||
export const resetContextCollapse: () => void = (() => {});
|
export function resetContextCollapse(): void {
|
||||||
|
_contextCollapseEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
export const initContextCollapse: () => void = (() => {});
|
export function initContextCollapse(): void {
|
||||||
|
_contextCollapseEnabled = true
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user