From f5c3ee5b5d1bdd05af27a4320221336afc0428ee Mon Sep 17 00:00:00 2001 From: claude-code-best Date: Sun, 26 Apr 2026 21:14:00 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E9=95=BF=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E8=BF=90=E8=A1=8C=E4=BC=9A=E8=AF=9D=E7=9A=84=E5=86=85?= =?UTF-8?q?=E5=AD=98=E6=B3=84=E6=BC=8F=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /clear 时释放 STATE 中保存的大块数据(API 请求/分类器请求/模型统计), 全屏模式增加 500 条消息上限防止无限增长,修复 progress 消息去重逻辑 避免交错消息导致重复累积(观察到 13k+ 条目/1GB+ 堆)。 Co-Authored-By: Claude Opus 4.7 --- src/commands/clear/conversation.ts | 12 ++++++++ src/screens/REPL.tsx | 46 ++++++++++++++++++++---------- 2 files changed, 43 insertions(+), 15 deletions(-) diff --git a/src/commands/clear/conversation.ts b/src/commands/clear/conversation.ts index 9db92cebe..72eb14613 100644 --- a/src/commands/clear/conversation.ts +++ b/src/commands/clear/conversation.ts @@ -10,6 +10,10 @@ import { getOriginalCwd, getSessionId, regenerateSessionId, + resetCostState, + setLastAPIRequest, + setLastAPIRequestMessages, + setLastClassifierRequests, } from '../../bootstrap/state.js' import type { SDKStatusMessage } from '../../entrypoints/sdk/coreTypes.js' import { @@ -144,6 +148,14 @@ export async function clearConversation({ // tracking) is retained so those agents keep functioning. clearSessionCaches(preservedAgentIds) + // Clear large STATE-held data that outlives the message array. + // lastAPIRequestMessages can hold the full post-compaction conversation + // (hundreds of KB–MB) for /share; resetCostState clears modelUsage. + setLastAPIRequest(null) + setLastAPIRequestMessages(null) + setLastClassifierRequests(null) + resetCostState() + setCwd(getOriginalCwd()) readFileState.clear() discoveredSkillNames?.clear() diff --git a/src/screens/REPL.tsx b/src/screens/REPL.tsx index 383adc7d4..28e4132d8 100644 --- a/src/screens/REPL.tsx +++ b/src/screens/REPL.tsx @@ -3051,12 +3051,22 @@ export function REPL({ // are O(n) per render, so drop everything before the previous // boundary to keep n bounded across multi-day sessions. if (isFullscreenEnvEnabled()) { - setMessages(old => [ - ...getMessagesAfterCompactBoundary(old, { + setMessages(old => { + const postBoundary = getMessagesAfterCompactBoundary(old, { includeSnipped: true, - }), - newMessage, - ]); + }) + // Hard cap: keep at most 500 messages in fullscreen scrollback + // to prevent unbounded memory growth in multi-day sessions. + // normalizeMessages/applyGrouping are O(n), and Ink fiber + // trees cost ~250KB RSS per message. Without this cap, + // scrollback after several compactions can reach thousands + // of messages (observed: 13k+, 1GB+ heap). + const MAX_FULLSCREEN_SCROLLBACK = 500 + const kept = postBoundary.length > MAX_FULLSCREEN_SCROLLBACK + ? postBoundary.slice(-MAX_FULLSCREEN_SCROLLBACK) + : postBoundary + return [...kept, newMessage] + }); } else { setMessages(() => [newMessage]); } @@ -3082,17 +3092,23 @@ export function REPL({ // history). Replacing those leaves the AgentTool UI stuck at // "Initializing…" because it renders the full progress trail. setMessages(oldMessages => { - const last = oldMessages.at(-1); - const lastData = last?.data as Record | undefined; const newData = newMessage.data as Record; - if ( - last?.type === 'progress' && - last.parentToolUseID === newMessage.parentToolUseID && - lastData?.type === newData.type - ) { - const copy = oldMessages.slice(); - copy[copy.length - 1] = newMessage; - return copy; + // Scan backwards to find the last ephemeral progress with matching + // parentToolUseID and type. Previously only checked the last message, + // so interleaved non-ephemeral messages caused duplicate progress + // entries to accumulate (observed 13k+ entries in sleep-heavy sessions). + for (let i = oldMessages.length - 1; i >= 0; i--) { + const m = oldMessages[i]! + if (m.type !== 'progress') break + const mData = m.data as Record | undefined + if ( + m.parentToolUseID === newMessage.parentToolUseID && + mData?.type === newData.type + ) { + const copy = oldMessages.slice(); + copy[i] = newMessage; + return copy; + } } return [...oldMessages, newMessage]; });