Files
claude-code/src/services/compact/postCompactCleanup.ts
claude-code-best ab0bbbc4b5 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>
2026-05-01 22:24:18 +08:00

110 lines
5.0 KiB
TypeScript

import { feature } from 'bun:bundle'
import type { QuerySource } from '../../constants/querySource.js'
import { clearSystemPromptSections } from '../../constants/systemPromptSections.js'
import { getUserContext } from '../../context.js'
import { clearSpeculativeChecks } from '@claude-code-best/builtin-tools/tools/BashTool/bashPermissions.js'
import { clearClassifierApprovals } from '../../utils/classifierApprovals.js'
import { resetGetMemoryFilesCache } from '../../utils/claudemd.js'
import { logError } from '../../utils/log.js'
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
* held by tracking structures that are invalidated by compaction.
*
* Note: We intentionally do NOT clear invoked skill content here.
* Skill content must survive across multiple compactions so that
* createSkillAttachmentIfNeeded() can include the full skill text
* in subsequent compaction attachments.
*
* querySource: pass the compacting query's source so we can skip
* resets that would clobber main-thread module-level state. Subagents
* (agent:*) run in the same process and share module-level state
* (context-collapse store, getMemoryFiles one-shot hook flag,
* getUserContext cache); resetting those when a SUBAGENT compacts
* would corrupt the MAIN thread's state. All compaction callers should
* pass querySource — undefined is only safe for callers that are
* genuinely main-thread-only (/compact, /clear).
*/
export function runPostCompactCleanup(querySource?: QuerySource): void {
// Subagents (agent:*) run in the same process and share module-level
// state with the main thread. Only reset main-thread module-level state
// (context-collapse, memory file cache) for main-thread compacts.
// Same startsWith pattern as isMainThread (index.ts:188).
const isMainThreadCompact =
querySource === undefined ||
querySource.startsWith('repl_main_thread') ||
querySource === 'sdk'
resetMicrocompactState()
if (feature('CONTEXT_COLLAPSE')) {
if (isMainThreadCompact) {
/* eslint-disable @typescript-eslint/no-require-imports */
;(
require('../contextCollapse/index.js') as typeof import('../contextCollapse/index.js')
).resetContextCollapse()
/* eslint-enable @typescript-eslint/no-require-imports */
}
}
if (isMainThreadCompact) {
// getUserContext is a memoized outer layer wrapping getClaudeMds() →
// getMemoryFiles(). If only the inner getMemoryFiles cache is cleared,
// the next turn hits the getUserContext cache and never reaches
// getMemoryFiles(), so the armed InstructionsLoaded hook never fires.
// Manual /compact already clears this explicitly at its call sites;
// auto-compact and reactive-compact did not — this centralizes the
// clear so all compaction paths behave consistently.
getUserContext.cache.clear?.()
resetGetMemoryFilesCache('compact')
}
clearSystemPromptSections()
clearClassifierApprovals()
clearSpeculativeChecks()
// Intentionally NOT calling resetSentSkillNames(): re-injecting the full
// skill_listing (~4K tokens) post-compact is pure cache_creation. The
// model still has SkillTool in schema, invoked_skills preserves used
// skills, and dynamic additions are handled by skillChangeDetector /
// cacheUtils resets. See compactConversation() for full rationale.
clearBetaTracingState()
if (feature('COMMIT_ATTRIBUTION')) {
// Intentionally fire-and-forget: the file-content cache sweep is a
// best-effort memory release whose completion no caller depends on.
// Keeping `runPostCompactCleanup` synchronous lets compaction call sites
// (REPL post-compact handler, /compact command, autoCompact) finish their
// own state transitions without an extra microtask round-trip — the sweep
// catches up on the next event-loop tick.
//
// The .catch is required even though the current attributionHooks.ts is a
// no-op stub: without it, a future restored sweepFileContentCache that
// throws would surface as an unhandled promise rejection from a function
// whose synchronous signature gives callers no way to observe it.
void import('../../utils/attributionHooks.js')
.then(m => m.sweepFileContentCache())
.catch(error => {
logError(error)
})
}
clearSessionMessagesCache()
for (const cb of compactCleanupCallbacks) {
try {
cb()
} catch (error) {
logError(error)
}
}
}