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.config
|
||||||
|
|
||||||
this.discoveredSkillNames.clear()
|
this.discoveredSkillNames.clear()
|
||||||
|
this.permissionDenials = []
|
||||||
setCwd(cwd)
|
setCwd(cwd)
|
||||||
const persistSession = !isSessionPersistenceDisabled()
|
const persistSession = !isSessionPersistenceDisabled()
|
||||||
const startTime = Date.now()
|
const startTime = Date.now()
|
||||||
|
|||||||
@@ -350,6 +350,7 @@ export class SSETransport implements Transport {
|
|||||||
const reader = body.getReader()
|
const reader = body.getReader()
|
||||||
const decoder = new TextDecoder()
|
const decoder = new TextDecoder()
|
||||||
let buffer = ''
|
let buffer = ''
|
||||||
|
const MAX_BUFFER_BYTES = 1024 * 1024 // 1MB — SSE frames include event/data/id prefixes
|
||||||
|
|
||||||
try {
|
try {
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -357,6 +358,14 @@ export class SSETransport implements Transport {
|
|||||||
if (done) break
|
if (done) break
|
||||||
|
|
||||||
buffer += decoder.decode(value, STREAM_DECODE_OPTS)
|
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)
|
const { frames, remaining } = parseSSEFrames(buffer)
|
||||||
buffer = remaining
|
buffer = remaining
|
||||||
|
|
||||||
|
|||||||
@@ -305,8 +305,9 @@ import {
|
|||||||
import { deserializeMessages } from '../utils/conversationRecovery.js';
|
import { deserializeMessages } from '../utils/conversationRecovery.js';
|
||||||
import { extractReadFilesFromMessages, extractBashToolsFromMessages } from '../utils/queryHelpers.js';
|
import { extractReadFilesFromMessages, extractBashToolsFromMessages } from '../utils/queryHelpers.js';
|
||||||
import { resetMicrocompactState } from '../services/compact/microCompact.js';
|
import { resetMicrocompactState } from '../services/compact/microCompact.js';
|
||||||
import { runPostCompactCleanup } from '../services/compact/postCompactCleanup.js';
|
import { runPostCompactCleanup, registerCompactCleanup } from '../services/compact/postCompactCleanup.js';
|
||||||
import {
|
import {
|
||||||
|
createContentReplacementState,
|
||||||
provisionContentReplacementState,
|
provisionContentReplacementState,
|
||||||
reconstructContentReplacementState,
|
reconstructContentReplacementState,
|
||||||
type ContentReplacementRecord,
|
type ContentReplacementRecord,
|
||||||
@@ -1778,6 +1779,9 @@ export function REPL({
|
|||||||
const [contentReplacementStateRef] = useState(() => ({
|
const [contentReplacementStateRef] = useState(() => ({
|
||||||
current: provisionContentReplacementState(initialMessages, initialContentReplacements),
|
current: provisionContentReplacementState(initialMessages, initialContentReplacements),
|
||||||
}));
|
}));
|
||||||
|
registerCompactCleanup(() => {
|
||||||
|
contentReplacementStateRef.current = createContentReplacementState();
|
||||||
|
});
|
||||||
|
|
||||||
const [haveShownCostDialog, setHaveShownCostDialog] = useState(getGlobalConfig().hasAcknowledgedCostThreshold);
|
const [haveShownCostDialog, setHaveShownCostDialog] = useState(getGlobalConfig().hasAcknowledgedCostThreshold);
|
||||||
const [vimMode, setVimMode] = useState<VimMode>('INSERT');
|
const [vimMode, setVimMode] = useState<VimMode>('INSERT');
|
||||||
|
|||||||
@@ -10,6 +10,17 @@ import { clearSessionMessagesCache } from '../../utils/sessionStorage.js'
|
|||||||
import { clearBetaTracingState } from '../../utils/telemetry/betaSessionTracing.js'
|
import { clearBetaTracingState } from '../../utils/telemetry/betaSessionTracing.js'
|
||||||
import { resetMicrocompactState } from './microCompact.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.
|
* Run cleanup of caches and tracking state after compaction.
|
||||||
* Call this after both auto-compact and manual /compact to free memory
|
* Call this after both auto-compact and manual /compact to free memory
|
||||||
@@ -88,4 +99,11 @@ export function runPostCompactCleanup(querySource?: QuerySource): void {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
clearSessionMessagesCache()
|
clearSessionMessagesCache()
|
||||||
|
for (const cb of compactCleanupCallbacks) {
|
||||||
|
try {
|
||||||
|
cb()
|
||||||
|
} catch (error) {
|
||||||
|
logError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
import { CACHE_PATHS } from './cachePaths.js'
|
import { CACHE_PATHS } from './cachePaths.js'
|
||||||
import { stripDisplayTags, stripDisplayTagsAllowEmpty } from './displayTags.js'
|
import { stripDisplayTags, stripDisplayTagsAllowEmpty } from './displayTags.js'
|
||||||
import { isEnvTruthy } from './envUtils.js'
|
import { isEnvTruthy } from './envUtils.js'
|
||||||
import { toError } from './errors.js'
|
import { toError, shortErrorStack } from './errors.js'
|
||||||
import { isEssentialTrafficOnly } from './privacyLevel.js'
|
import { isEssentialTrafficOnly } from './privacyLevel.js'
|
||||||
import { jsonParse } from './slowOperations.js'
|
import { jsonParse } from './slowOperations.js'
|
||||||
|
|
||||||
@@ -175,7 +175,7 @@ export function logError(error: unknown): void {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const errorStr = err.stack || err.message
|
const errorStr = shortErrorStack(err)
|
||||||
|
|
||||||
const errorInfo = {
|
const errorInfo = {
|
||||||
error: errorStr,
|
error: errorStr,
|
||||||
|
|||||||
Reference in New Issue
Block a user