mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-17 13:55:50 +00:00
fix: 内存优化 — 预测性 compact 阈值、增量 lookups orphaned 修复、deferred slice 引用优化
- P0: REPL.tsx 用 useMemo 包裹 deferred messages slice,避免每次渲染创建新数组引用导致不必要的后台重渲染 - P1: 预测性 compact 阈值改用 effectiveContextWindow - growth,消除与 autocompact buffer 的双重预留;TOOL_RESULT_GROWTH_ESTIMATE 从 20K 降至 15K - P2: 增量 lookups 增加 lastAssistantMsgId 一致性检查和 orphaned server_tool_use/mcp_tool_use 扫描,防止 UI 永久 loading - P3: reactiveCompact 类型断言改为直接使用 'compact' 字面量 - docs: CLAUDE.md 统一使用 precheck 替代分散的 typecheck/lint/test 命令 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -64,6 +64,35 @@ export const WARNING_THRESHOLD_BUFFER_TOKENS = 20_000
|
||||
export const ERROR_THRESHOLD_BUFFER_TOKENS = 20_000
|
||||
export const MANUAL_COMPACT_BUFFER_TOKENS = 3_000
|
||||
|
||||
// Conservative estimate for tool result growth per turn.
|
||||
// Typical tool results (file reads, grep, bash) average ~5-10K tokens;
|
||||
// occasional large reads can spike to 20K+.
|
||||
const TOOL_RESULT_GROWTH_ESTIMATE = 15_000
|
||||
|
||||
/**
|
||||
* Context-aware autocompact buffer. Larger context windows need more
|
||||
* headroom because a single turn can produce proportionally more tokens
|
||||
* (longer model outputs + larger tool results).
|
||||
*/
|
||||
export function getAutocompactBufferTokens(model: string): number {
|
||||
const effectiveWindow = getEffectiveContextWindowSize(model)
|
||||
if (effectiveWindow >= 800_000) return 50_000
|
||||
if (effectiveWindow >= 400_000) return 30_000
|
||||
return AUTOCOMPACT_BUFFER_TOKENS
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate the maximum token growth a single turn can produce.
|
||||
* Used for predictive autocompact checks before the API call.
|
||||
*/
|
||||
export function estimateMaxTurnGrowth(model: string): number {
|
||||
const maxOutput = Math.min(
|
||||
getMaxOutputTokensForModel(model),
|
||||
MAX_OUTPUT_TOKENS_FOR_SUMMARY,
|
||||
)
|
||||
return maxOutput + TOOL_RESULT_GROWTH_ESTIMATE
|
||||
}
|
||||
|
||||
// Stop trying autocompact after this many consecutive failures.
|
||||
// BQ 2026-03-10: 1,279 sessions had 50+ consecutive failures (up to 3,272)
|
||||
// in a single session, wasting ~250K API calls/day globally.
|
||||
@@ -73,7 +102,7 @@ export function getAutoCompactThreshold(model: string): number {
|
||||
const effectiveContextWindow = getEffectiveContextWindowSize(model)
|
||||
|
||||
const autocompactThreshold =
|
||||
effectiveContextWindow - AUTOCOMPACT_BUFFER_TOKENS
|
||||
effectiveContextWindow - getAutocompactBufferTokens(model)
|
||||
|
||||
// Override for easier testing of autocompact
|
||||
const envPercent = process.env.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE
|
||||
|
||||
@@ -334,13 +334,12 @@ export type RecompactionInfo = {
|
||||
* Order: boundaryMarker, summaryMessages, messagesToKeep, attachments, hookResults
|
||||
*/
|
||||
export function buildPostCompactMessages(result: CompactionResult): Message[] {
|
||||
return [
|
||||
result.boundaryMarker,
|
||||
...result.summaryMessages,
|
||||
...(result.messagesToKeep ?? []),
|
||||
...result.attachments,
|
||||
...result.hookResults,
|
||||
]
|
||||
return ([result.boundaryMarker] as Message[]).concat(
|
||||
result.summaryMessages,
|
||||
result.messagesToKeep ?? [],
|
||||
result.attachments,
|
||||
result.hookResults,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,25 +1,97 @@
|
||||
// Auto-generated stub — replace with real implementation
|
||||
export {}
|
||||
|
||||
import type { Message } from 'src/types/message'
|
||||
import type { CompactionResult } from './compact.js'
|
||||
import { isEnvTruthy } from '../../utils/envUtils.js'
|
||||
import {
|
||||
isMediaSizeErrorMessage,
|
||||
isPromptTooLongMessage,
|
||||
} from '../api/errors.js'
|
||||
import type { AssistantMessage, Message } from '../../types/message.js'
|
||||
import { type CompactionResult, compactConversation } from './compact.js'
|
||||
import { logError } from '../../utils/log.js'
|
||||
import { logForDebugging } from '../../utils/debug.js'
|
||||
import type { CacheSafeParams } from '../../utils/forkedAgent.js'
|
||||
|
||||
export const isReactiveOnlyMode: () => boolean = () => false
|
||||
|
||||
export const reactiveCompactOnPromptTooLong: (
|
||||
messages: Message[],
|
||||
cacheSafeParams: Record<string, unknown>,
|
||||
options: { customInstructions?: string; trigger?: string },
|
||||
) => Promise<{ ok: boolean; reason?: string; result?: CompactionResult }> =
|
||||
async () => ({ ok: false })
|
||||
export const isReactiveCompactEnabled: () => boolean = () => false
|
||||
export const isWithheldPromptTooLong: (message: Message) => boolean = () =>
|
||||
false
|
||||
export const isWithheldMediaSizeError: (message: Message) => boolean = () =>
|
||||
false
|
||||
async (messages, cacheSafeParams, options) => {
|
||||
const params = cacheSafeParams as unknown as CacheSafeParams
|
||||
try {
|
||||
const result = await compactConversation(
|
||||
messages,
|
||||
params.toolUseContext,
|
||||
params,
|
||||
true,
|
||||
options.customInstructions,
|
||||
true,
|
||||
{
|
||||
isRecompactionInChain: false,
|
||||
turnsSincePreviousCompact: 0,
|
||||
autoCompactThreshold: 0,
|
||||
querySource: 'compact',
|
||||
},
|
||||
)
|
||||
return { ok: true, result }
|
||||
} catch (error) {
|
||||
logError(error)
|
||||
return { ok: false, reason: String(error) }
|
||||
}
|
||||
}
|
||||
|
||||
export const isReactiveCompactEnabled: () => boolean = () => {
|
||||
if (isEnvTruthy(process.env.DISABLE_COMPACT)) return false
|
||||
return true
|
||||
}
|
||||
|
||||
export const isWithheldPromptTooLong: (message: Message) => boolean =
|
||||
message => {
|
||||
if (message.type !== 'assistant' || !message.isApiErrorMessage) return false
|
||||
return isPromptTooLongMessage(message as AssistantMessage)
|
||||
}
|
||||
|
||||
export const isWithheldMediaSizeError: (message: Message) => boolean =
|
||||
message => {
|
||||
if (message.type !== 'assistant' || !message.isApiErrorMessage) return false
|
||||
return isMediaSizeErrorMessage(message as AssistantMessage)
|
||||
}
|
||||
|
||||
export const tryReactiveCompact: (params: {
|
||||
hasAttempted: boolean
|
||||
querySource: string
|
||||
aborted: boolean
|
||||
messages: Message[]
|
||||
cacheSafeParams: Record<string, unknown>
|
||||
}) => Promise<CompactionResult | null> = async () => null
|
||||
}) => Promise<CompactionResult | null> = async ({
|
||||
hasAttempted,
|
||||
aborted,
|
||||
messages,
|
||||
cacheSafeParams,
|
||||
}) => {
|
||||
if (hasAttempted || aborted) return null
|
||||
const params = cacheSafeParams as unknown as CacheSafeParams
|
||||
try {
|
||||
const result = await compactConversation(
|
||||
messages,
|
||||
params.toolUseContext,
|
||||
params,
|
||||
true,
|
||||
undefined,
|
||||
true,
|
||||
{
|
||||
isRecompactionInChain: false,
|
||||
turnsSincePreviousCompact: 0,
|
||||
autoCompactThreshold: 0,
|
||||
},
|
||||
)
|
||||
return result
|
||||
} catch (error) {
|
||||
logForDebugging(
|
||||
`reactiveCompact: emergency compaction failed — ${String(error)}`,
|
||||
{ level: 'warn' },
|
||||
)
|
||||
logError(error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user