mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 21:05:51 +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:
@@ -1397,6 +1397,172 @@ export function buildMessageLookups(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Incrementally update lookups by processing only newly appended messages.
|
||||
* Returns the same lookups object (mutated in place) if update succeeds,
|
||||
* or null if a full rebuild is needed (e.g., messages were removed).
|
||||
*/
|
||||
export function updateMessageLookupsIncremental(
|
||||
existing: MessageLookups,
|
||||
previousNormalizedCount: number,
|
||||
previousMessageCount: number,
|
||||
normalizedMessages: NormalizedMessage[],
|
||||
messages: Message[],
|
||||
): MessageLookups | null {
|
||||
// Safety check: only handle append-only case
|
||||
if (
|
||||
normalizedMessages.length < previousNormalizedCount ||
|
||||
messages.length < previousMessageCount
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
// No new messages — nothing to do
|
||||
if (
|
||||
normalizedMessages.length === previousNormalizedCount &&
|
||||
messages.length === previousMessageCount
|
||||
) {
|
||||
return existing
|
||||
}
|
||||
|
||||
// Process new messages entries (pass 1: assistant tool_use blocks)
|
||||
const newMessageStart = previousMessageCount
|
||||
for (let i = newMessageStart; i < messages.length; i++) {
|
||||
const msg = messages[i]!
|
||||
if (msg.type === 'assistant') {
|
||||
const aMsg = msg as AssistantMessage
|
||||
const id = aMsg.message.id!
|
||||
if (Array.isArray(aMsg.message.content)) {
|
||||
const newToolUseIDs: string[] = []
|
||||
for (const content of aMsg.message.content) {
|
||||
if (typeof content !== 'string' && content.type === 'tool_use') {
|
||||
const toolUseContent = content as ToolUseBlock
|
||||
newToolUseIDs.push(toolUseContent.id)
|
||||
existing.toolUseByToolUseID.set(
|
||||
toolUseContent.id,
|
||||
content as ToolUseBlockParam,
|
||||
)
|
||||
}
|
||||
}
|
||||
// Update sibling lookup: all tool_use IDs in this message share siblings
|
||||
const allSiblings = new Set(newToolUseIDs)
|
||||
for (const toolUseID of newToolUseIDs) {
|
||||
existing.siblingToolUseIDs.set(toolUseID, allSiblings)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process new normalizedMessages entries (pass 2: progress, hooks, tool results)
|
||||
const newNormalizedStart = previousNormalizedCount
|
||||
for (let i = newNormalizedStart; i < normalizedMessages.length; i++) {
|
||||
const msg = normalizedMessages[i]!
|
||||
|
||||
if (msg.type === 'progress') {
|
||||
const toolUseID = msg.parentToolUseID as string
|
||||
const existing2 = existing.progressMessagesByToolUseID.get(toolUseID)
|
||||
if (existing2) {
|
||||
existing2.push(msg as ProgressMessage)
|
||||
} else {
|
||||
existing.progressMessagesByToolUseID.set(toolUseID, [
|
||||
msg as ProgressMessage,
|
||||
])
|
||||
}
|
||||
|
||||
const progressData = msg.data as { type: string; hookEvent: HookEvent }
|
||||
if (progressData.type === 'hook_progress') {
|
||||
const hookEvent = progressData.hookEvent
|
||||
let byHookEvent = existing.inProgressHookCounts.get(toolUseID)
|
||||
if (!byHookEvent) {
|
||||
byHookEvent = new Map()
|
||||
existing.inProgressHookCounts.set(toolUseID, byHookEvent)
|
||||
}
|
||||
byHookEvent.set(hookEvent, (byHookEvent.get(hookEvent) ?? 0) + 1)
|
||||
}
|
||||
}
|
||||
|
||||
if (msg.type === 'user' && Array.isArray(msg.message?.content)) {
|
||||
for (const content of msg.message?.content ?? []) {
|
||||
if (typeof content !== 'string' && content.type === 'tool_result') {
|
||||
const tr = content as ToolResultBlockParam
|
||||
existing.toolResultByToolUseID.set(tr.tool_use_id, msg)
|
||||
existing.resolvedToolUseIDs.add(tr.tool_use_id)
|
||||
if (tr.is_error) {
|
||||
existing.erroredToolUseIDs.add(tr.tool_use_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (msg.type === 'assistant' && Array.isArray(msg.message?.content)) {
|
||||
for (const content of msg.message?.content ?? []) {
|
||||
if (typeof content === 'string') continue
|
||||
if (
|
||||
'tool_use_id' in content &&
|
||||
typeof (content as { tool_use_id: string }).tool_use_id === 'string'
|
||||
) {
|
||||
existing.resolvedToolUseIDs.add(
|
||||
(content as { tool_use_id: string }).tool_use_id,
|
||||
)
|
||||
}
|
||||
if ((content.type as string) === 'advisor_tool_result') {
|
||||
const result = content as {
|
||||
tool_use_id: string
|
||||
content: { type: string }
|
||||
}
|
||||
if (result.content.type === 'advisor_tool_result_error') {
|
||||
existing.erroredToolUseIDs.add(result.tool_use_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isHookAttachmentMessage(msg)) {
|
||||
const toolUseID = msg.attachment.toolUseID
|
||||
const hookEvent = msg.attachment.hookEvent
|
||||
const hookName = (msg.attachment as HookAttachmentWithName).hookName
|
||||
if (hookName !== undefined) {
|
||||
let byHookEvent = existing.resolvedHookCounts.get(toolUseID)
|
||||
if (!byHookEvent) {
|
||||
byHookEvent = new Map()
|
||||
existing.resolvedHookCounts.set(toolUseID, byHookEvent)
|
||||
}
|
||||
byHookEvent.set(hookEvent, (byHookEvent.get(hookEvent) ?? 0) + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
existing.normalizedMessageCount = normalizedMessages.length
|
||||
|
||||
// Mark orphaned server_tool_use / mcp_tool_use blocks as errored.
|
||||
// Only scan the new normalizedMessages since the previous count —
|
||||
// existing entries were already checked by a prior full build.
|
||||
const lastMsg = messages.at(-1)
|
||||
const lastAssistantMsgId =
|
||||
lastMsg?.type === 'assistant' ? lastMsg.message?.id : undefined
|
||||
for (let i = newNormalizedStart; i < normalizedMessages.length; i++) {
|
||||
const msg = normalizedMessages[i]!
|
||||
if (msg.type !== 'assistant') continue
|
||||
const aMsg = msg as AssistantMessage
|
||||
if (aMsg.message.id === lastAssistantMsgId) continue
|
||||
if (!Array.isArray(aMsg.message.content)) continue
|
||||
for (const content of aMsg.message.content) {
|
||||
if (
|
||||
typeof content !== 'string' &&
|
||||
((content.type as string) === 'server_tool_use' ||
|
||||
(content.type as string) === 'mcp_tool_use') &&
|
||||
!existing.resolvedToolUseIDs.has((content as { id: string }).id)
|
||||
) {
|
||||
const id = (content as { id: string }).id
|
||||
existing.resolvedToolUseIDs.add(id)
|
||||
existing.erroredToolUseIDs.add(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return existing
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute a lightweight structural fingerprint for buildMessageLookups caching.
|
||||
* Only captures information that affects lookup results (types, IDs, counts),
|
||||
|
||||
@@ -101,6 +101,20 @@ export async function readFileInRange(
|
||||
throw new FileTooLargeError(stats.size, maxBytes)
|
||||
}
|
||||
|
||||
// For targeted reads of moderately large files, prefer streaming to
|
||||
// avoid loading the full file into memory when only a slice is needed.
|
||||
const isTargetedRead = offset > 0 || maxLines !== undefined
|
||||
if (isTargetedRead && stats.size > FAST_PATH_MAX_SIZE / 4) {
|
||||
return readFileInRangeStreaming(
|
||||
filePath,
|
||||
offset,
|
||||
maxLines,
|
||||
maxBytes,
|
||||
truncateOnByteLimit,
|
||||
signal,
|
||||
)
|
||||
}
|
||||
|
||||
const text = await readFile(filePath, { encoding: 'utf8', signal })
|
||||
return readFileInRangeFast(
|
||||
text,
|
||||
|
||||
Reference in New Issue
Block a user