From ab0bbbc4b5265323c6e10bdd9926a709b0875ad1 Mon Sep 17 00:00:00 2001 From: claude-code-best Date: Fri, 1 May 2026 22:24:18 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=86=85=E5=AD=98?= =?UTF-8?q?=E6=BA=A2=E5=87=BA=E9=97=AE=E9=A2=98=EF=BC=8Ccompact=20?= =?UTF-8?q?=E6=97=B6=E6=B8=85=E7=90=86=E6=8C=81=E4=B9=85=E5=A2=9E=E9=95=BF?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - compact 时清理 contentReplacementState(seenIds/replacements) - logError() 使用 shortErrorStack 替代完整 err.stack,减少 GC 压力 - permissionDenials 每次 submitMessage 清空,防止无限增长 - SSE 缓冲区添加 1MB 上限,防止畸形数据无限累积 Co-Authored-By: Claude Opus 4.7 --- src/QueryEngine.ts | 1 + src/cli/transports/SSETransport.ts | 9 +++++++++ src/screens/REPL.tsx | 6 +++++- src/services/compact/postCompactCleanup.ts | 18 ++++++++++++++++++ src/utils/log.ts | 4 ++-- 5 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/QueryEngine.ts b/src/QueryEngine.ts index c9697c768..8e48ed757 100644 --- a/src/QueryEngine.ts +++ b/src/QueryEngine.ts @@ -248,6 +248,7 @@ export class QueryEngine { } = this.config this.discoveredSkillNames.clear() + this.permissionDenials = [] setCwd(cwd) const persistSession = !isSessionPersistenceDisabled() const startTime = Date.now() diff --git a/src/cli/transports/SSETransport.ts b/src/cli/transports/SSETransport.ts index 42b90afbd..110f71957 100644 --- a/src/cli/transports/SSETransport.ts +++ b/src/cli/transports/SSETransport.ts @@ -350,6 +350,7 @@ export class SSETransport implements Transport { const reader = body.getReader() const decoder = new TextDecoder() let buffer = '' + const MAX_BUFFER_BYTES = 1024 * 1024 // 1MB — SSE frames include event/data/id prefixes try { while (true) { @@ -357,6 +358,14 @@ export class SSETransport implements Transport { if (done) break buffer += decoder.decode(value, STREAM_DECODE_OPTS) + if (buffer.length > MAX_BUFFER_BYTES) { + logForDebugging( + `SSETransport: Buffer exceeded ${MAX_BUFFER_BYTES} bytes — dropping connection`, + { level: 'error' }, + ) + logForDiagnosticsNoPII('error', 'cli_sse_buffer_overflow') + break + } const { frames, remaining } = parseSSEFrames(buffer) buffer = remaining diff --git a/src/screens/REPL.tsx b/src/screens/REPL.tsx index fe26e38cf..0cd46d38c 100644 --- a/src/screens/REPL.tsx +++ b/src/screens/REPL.tsx @@ -305,8 +305,9 @@ import { import { deserializeMessages } from '../utils/conversationRecovery.js'; import { extractReadFilesFromMessages, extractBashToolsFromMessages } from '../utils/queryHelpers.js'; import { resetMicrocompactState } from '../services/compact/microCompact.js'; -import { runPostCompactCleanup } from '../services/compact/postCompactCleanup.js'; +import { runPostCompactCleanup, registerCompactCleanup } from '../services/compact/postCompactCleanup.js'; import { + createContentReplacementState, provisionContentReplacementState, reconstructContentReplacementState, type ContentReplacementRecord, @@ -1778,6 +1779,9 @@ export function REPL({ const [contentReplacementStateRef] = useState(() => ({ current: provisionContentReplacementState(initialMessages, initialContentReplacements), })); + registerCompactCleanup(() => { + contentReplacementStateRef.current = createContentReplacementState(); + }); const [haveShownCostDialog, setHaveShownCostDialog] = useState(getGlobalConfig().hasAcknowledgedCostThreshold); const [vimMode, setVimMode] = useState('INSERT'); diff --git a/src/services/compact/postCompactCleanup.ts b/src/services/compact/postCompactCleanup.ts index b89e3a0be..b52730217 100644 --- a/src/services/compact/postCompactCleanup.ts +++ b/src/services/compact/postCompactCleanup.ts @@ -10,6 +10,17 @@ import { clearSessionMessagesCache } from '../../utils/sessionStorage.js' import { clearBetaTracingState } from '../../utils/telemetry/betaSessionTracing.js' import { resetMicrocompactState } from './microCompact.js' +/** + * Compact-scoped cleanup callbacks registered by REPL or other long-lived + * components. Called during runPostCompactCleanup() so instance-scoped state + * (e.g. contentReplacementState) is freed alongside module-level caches. + */ +const compactCleanupCallbacks: Array<() => void> = [] + +export function registerCompactCleanup(callback: () => void): void { + compactCleanupCallbacks.push(callback) +} + /** * Run cleanup of caches and tracking state after compaction. * Call this after both auto-compact and manual /compact to free memory @@ -88,4 +99,11 @@ export function runPostCompactCleanup(querySource?: QuerySource): void { }) } clearSessionMessagesCache() + for (const cb of compactCleanupCallbacks) { + try { + cb() + } catch (error) { + logError(error) + } + } } diff --git a/src/utils/log.ts b/src/utils/log.ts index 7d15b2fb3..7b85e4cf7 100644 --- a/src/utils/log.ts +++ b/src/utils/log.ts @@ -17,7 +17,7 @@ import { import { CACHE_PATHS } from './cachePaths.js' import { stripDisplayTags, stripDisplayTagsAllowEmpty } from './displayTags.js' import { isEnvTruthy } from './envUtils.js' -import { toError } from './errors.js' +import { toError, shortErrorStack } from './errors.js' import { isEssentialTrafficOnly } from './privacyLevel.js' import { jsonParse } from './slowOperations.js' @@ -175,7 +175,7 @@ export function logError(error: unknown): void { return } - const errorStr = err.stack || err.message + const errorStr = shortErrorStack(err) const errorInfo = { error: errorStr,