From b28de717ddf76bf47d7da09431cfc19e54b4b454 Mon Sep 17 00:00:00 2001 From: claude-code-best Date: Mon, 4 May 2026 23:23:25 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=E5=86=85=E5=AD=98?= =?UTF-8?q?=E4=B8=8E=E9=81=A5=E6=B5=8B=E7=AE=A1=E7=90=86=EF=BC=8C=E5=90=AF?= =?UTF-8?q?=E7=94=A8=20Vite=20minify?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 禁用 HISTORY_SNIP feature flag 并新增 proactiveTruncate 防止无 compact_boundary 时内存无限增长 - 跳过未启用 telemetry 时的 OTel 初始化,防止长会话 PerformanceMeasure 堆积 - OTel 导出遇 401/403 自动关闭 reader,防止 handle 泄漏 - Vite 构建启用 minify Co-Authored-By: Claude Opus 4.7 --- scripts/defines.ts | 2 +- src/QueryEngine.ts | 9 ++++ src/entrypoints/init.ts | 10 ++++ src/services/compact/snipCompact.ts | 74 ++++++++++++++++++++++++++ src/utils/telemetry/instrumentation.ts | 41 +++++++++++++- vite.config.ts | 2 +- 6 files changed, 135 insertions(+), 3 deletions(-) diff --git a/scripts/defines.ts b/scripts/defines.ts index 0ae46ab15..6098d1733 100644 --- a/scripts/defines.ts +++ b/scripts/defines.ts @@ -49,7 +49,7 @@ export const DEFAULT_BUILD_FEATURES = [ 'DAEMON', // 守护进程模式,长驻 supervisor 管理后台 worker(非 GB 级主因) 'ACP', // ACP 代理协议,支持外部 agent 接入 'WORKFLOW_SCRIPTS', // 工作流脚本(.claude/workflows/ 中的 YAML/MD) - 'HISTORY_SNIP', // 历史消息裁剪,压缩上下文窗口 + // 'HISTORY_SNIP', // 历史消息裁剪,压缩上下文窗口 // 'CONTEXT_COLLAPSE', // 已禁用:实现是空壳 stub,启用后会抑制 auto compact 导致上下文管理完全失效 'MONITOR_TOOL', // Monitor 工具,流式监控后台进程输出 // 'FORK_SUBAGENT', // 已禁用:显式 `fork: true` 参数触发 fork 路径(继承父级上下文和模型),不影响 forceAsync 和探索任务模型选择 diff --git a/src/QueryEngine.ts b/src/QueryEngine.ts index 8e48ed757..8ffa5e9d0 100644 --- a/src/QueryEngine.ts +++ b/src/QueryEngine.ts @@ -1003,6 +1003,15 @@ export class QueryEngine { uuid: msg.uuid, } } + // Proactive truncation: prevent unbounded growth when API doesn't + // return compact_boundary (e.g. third-party compat layers). + if (feature('HISTORY_SNIP') && snipModule) { + const truncated = snipModule.proactiveTruncate(this.mutableMessages) + if (truncated !== this.mutableMessages) { + this.mutableMessages.length = 0 + this.mutableMessages.push(...truncated) + } + } // Don't yield other system messages in headless mode break } diff --git a/src/entrypoints/init.ts b/src/entrypoints/init.ts index a610c1c25..b1301056d 100644 --- a/src/entrypoints/init.ts +++ b/src/entrypoints/init.ts @@ -320,6 +320,16 @@ async function doInitializeTelemetry(): Promise { return } + // Skip entire OTel initialization when telemetry is not enabled. + // Prevents PerformanceMeasure accumulation in long-running sessions. + if (!isEnvTruthy(process.env.CLAUDE_CODE_ENABLE_TELEMETRY)) { + telemetryInitialized = true + logForDebugging( + '[3P telemetry] Skipped — CLAUDE_CODE_ENABLE_TELEMETRY not set', + ) + return + } + // Set flag before init to prevent double initialization telemetryInitialized = true try { diff --git a/src/services/compact/snipCompact.ts b/src/services/compact/snipCompact.ts index 4e4a0d9fc..3be2ef624 100644 --- a/src/services/compact/snipCompact.ts +++ b/src/services/compact/snipCompact.ts @@ -163,3 +163,77 @@ export function isSnipRuntimeEnabled(): boolean { export function shouldNudgeForSnips(messages: Message[]): boolean { return messages.length >= SNIP_NUDGE_THRESHOLD } + +/** + * Maximum total character length of message content before proactive + * truncation kicks in. ~150 MB of string data corresponds to roughly + * 1.5x the default 200k-token context window at 4 chars/token — well + * beyond what any model can actually use in a single request. + */ +const PROACTIVE_TRUNCATE_CHARS = 150_000_000 + +/** + * Minimum number of messages to keep when falling back to tail-only + * retention (i.e. when no compact_boundary exists in the array). + */ +const PROACTIVE_TRUNCATE_MIN_TAIL = 50 + +/** + * Proactively truncate old messages when the in-memory store grows too + * large. Unlike `snipCompactIfNeeded` (which waits for a snip_boundary + * from the API), this runs client-side after every push — ensuring + * unbounded growth cannot happen even when the API never returns a + * compact_boundary (e.g. third-party compat layers). + * + * Strategy: + * 1. If a `compact_boundary` exists, keep it and everything after it. + * 2. Otherwise, keep only the last `PROACTIVE_TRUNCATE_MIN_TAIL` messages. + * + * Returns the same array reference when no truncation is needed. + */ +export function proactiveTruncate(messages: Message[]): Message[] { + if (messages.length < PROACTIVE_TRUNCATE_MIN_TAIL) return messages + + let totalChars = 0 + for (const msg of messages) { + const content = msg.message?.content + if (typeof content === 'string') { + totalChars += content.length + } else if (Array.isArray(content)) { + for (const block of content) { + if (typeof block === 'string') { + totalChars += (block as string).length + } else if (block && typeof block === 'object') { + const obj = block as unknown as Record + const text = obj.text ?? obj.content + if (typeof text === 'string') { + totalChars += text.length + } + } + } + } + } + + if (totalChars < PROACTIVE_TRUNCATE_CHARS) return messages + + // Find last compact_boundary — the standard anchor point + let boundaryIdx = -1 + for (let i = messages.length - 1; i >= 0; i--) { + const msg = messages[i]! + if ( + msg.type === 'system' && + (msg as Record).subtype === 'compact_boundary' + ) { + boundaryIdx = i + break + } + } + + const keepFrom = + boundaryIdx >= 0 + ? boundaryIdx + : Math.max(0, messages.length - PROACTIVE_TRUNCATE_MIN_TAIL) + if (keepFrom === 0) return messages + + return messages.slice(keepFrom) +} diff --git a/src/utils/telemetry/instrumentation.ts b/src/utils/telemetry/instrumentation.ts index fffe84def..d5bcf7ffb 100644 --- a/src/utils/telemetry/instrumentation.ts +++ b/src/utils/telemetry/instrumentation.ts @@ -206,10 +206,49 @@ async function getOtlpReaders() { return exporters.map(exporter => { if ('export' in exporter) { - return new PeriodicExportingMetricReader({ + const reader = new PeriodicExportingMetricReader({ exporter, exportIntervalMillis: exportInterval, }) + // Wrap the export callback to auto-shutdown the reader on auth + // failures (401/403). Without this the PeriodicExportingMetricReader's + // internal setInterval keeps retrying forever, leaking handles. + const originalExport = ( + exporter as unknown as { + export: ( + metrics: unknown, + callback: (result: { error?: Error }) => void, + ) => unknown + } + ).export.bind(exporter) + ;( + exporter as unknown as { + export: ( + metrics: unknown, + callback: (result: { error?: Error }) => void, + ) => unknown + } + ).export = (metrics, callback) => { + return originalExport(metrics, result => { + if (result.error) { + const msg = result.error.message || '' + if ( + msg.includes('401') || + msg.includes('403') || + msg.includes('Unauthorized') || + msg.includes('authentication') + ) { + logForDebugging( + `[3P telemetry] Auth error detected, shutting down metric reader`, + { level: 'error' }, + ) + void reader.shutdown() + } + } + callback(result) + }) + } + return reader } return exporter }) diff --git a/vite.config.ts b/vite.config.ts index 115b2e42a..7a3a1d350 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -83,7 +83,7 @@ export default defineConfig({ target: 'es2020', copyPublicDir: false, sourcemap: false, - minify: false, + minify: true, // SSR build mode — uses Rollup with Node.js target ssr: true,