mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
fix: 修复内存溢出问题,compact 时清理持久增长数据结构
- compact 时清理 contentReplacementState(seenIds/replacements) - logError() 使用 shortErrorStack 替代完整 err.stack,减少 GC 压力 - permissionDenials 每次 submitMessage 清空,防止无限增长 - SSE 缓冲区添加 1MB 上限,防止畸形数据无限累积 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -248,6 +248,7 @@ export class QueryEngine {
|
||||
} = this.config
|
||||
|
||||
this.discoveredSkillNames.clear()
|
||||
this.permissionDenials = []
|
||||
setCwd(cwd)
|
||||
const persistSession = !isSessionPersistenceDisabled()
|
||||
const startTime = Date.now()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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<VimMode>('INSERT');
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user