Skip to content

Commit b28de71

Browse files
perf: 优化内存与遥测管理,启用 Vite minify
- 禁用 HISTORY_SNIP feature flag 并新增 proactiveTruncate 防止无 compact_boundary 时内存无限增长 - 跳过未启用 telemetry 时的 OTel 初始化,防止长会话 PerformanceMeasure 堆积 - OTel 导出遇 401/403 自动关闭 reader,防止 handle 泄漏 - Vite 构建启用 minify Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 5c1be19 commit b28de71

6 files changed

Lines changed: 135 additions & 3 deletions

File tree

scripts/defines.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export const DEFAULT_BUILD_FEATURES = [
4949
'DAEMON', // 守护进程模式,长驻 supervisor 管理后台 worker(非 GB 级主因)
5050
'ACP', // ACP 代理协议,支持外部 agent 接入
5151
'WORKFLOW_SCRIPTS', // 工作流脚本(.claude/workflows/ 中的 YAML/MD)
52-
'HISTORY_SNIP', // 历史消息裁剪,压缩上下文窗口
52+
// 'HISTORY_SNIP', // 历史消息裁剪,压缩上下文窗口
5353
// 'CONTEXT_COLLAPSE', // 已禁用:实现是空壳 stub,启用后会抑制 auto compact 导致上下文管理完全失效
5454
'MONITOR_TOOL', // Monitor 工具,流式监控后台进程输出
5555
// 'FORK_SUBAGENT', // 已禁用:显式 `fork: true` 参数触发 fork 路径(继承父级上下文和模型),不影响 forceAsync 和探索任务模型选择

src/QueryEngine.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1003,6 +1003,15 @@ export class QueryEngine {
10031003
uuid: msg.uuid,
10041004
}
10051005
}
1006+
// Proactive truncation: prevent unbounded growth when API doesn't
1007+
// return compact_boundary (e.g. third-party compat layers).
1008+
if (feature('HISTORY_SNIP') && snipModule) {
1009+
const truncated = snipModule.proactiveTruncate(this.mutableMessages)
1010+
if (truncated !== this.mutableMessages) {
1011+
this.mutableMessages.length = 0
1012+
this.mutableMessages.push(...truncated)
1013+
}
1014+
}
10061015
// Don't yield other system messages in headless mode
10071016
break
10081017
}

src/entrypoints/init.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,16 @@ async function doInitializeTelemetry(): Promise<void> {
320320
return
321321
}
322322

323+
// Skip entire OTel initialization when telemetry is not enabled.
324+
// Prevents PerformanceMeasure accumulation in long-running sessions.
325+
if (!isEnvTruthy(process.env.CLAUDE_CODE_ENABLE_TELEMETRY)) {
326+
telemetryInitialized = true
327+
logForDebugging(
328+
'[3P telemetry] Skipped — CLAUDE_CODE_ENABLE_TELEMETRY not set',
329+
)
330+
return
331+
}
332+
323333
// Set flag before init to prevent double initialization
324334
telemetryInitialized = true
325335
try {

src/services/compact/snipCompact.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,3 +163,77 @@ export function isSnipRuntimeEnabled(): boolean {
163163
export function shouldNudgeForSnips(messages: Message[]): boolean {
164164
return messages.length >= SNIP_NUDGE_THRESHOLD
165165
}
166+
167+
/**
168+
* Maximum total character length of message content before proactive
169+
* truncation kicks in. ~150 MB of string data corresponds to roughly
170+
* 1.5x the default 200k-token context window at 4 chars/token — well
171+
* beyond what any model can actually use in a single request.
172+
*/
173+
const PROACTIVE_TRUNCATE_CHARS = 150_000_000
174+
175+
/**
176+
* Minimum number of messages to keep when falling back to tail-only
177+
* retention (i.e. when no compact_boundary exists in the array).
178+
*/
179+
const PROACTIVE_TRUNCATE_MIN_TAIL = 50
180+
181+
/**
182+
* Proactively truncate old messages when the in-memory store grows too
183+
* large. Unlike `snipCompactIfNeeded` (which waits for a snip_boundary
184+
* from the API), this runs client-side after every push — ensuring
185+
* unbounded growth cannot happen even when the API never returns a
186+
* compact_boundary (e.g. third-party compat layers).
187+
*
188+
* Strategy:
189+
* 1. If a `compact_boundary` exists, keep it and everything after it.
190+
* 2. Otherwise, keep only the last `PROACTIVE_TRUNCATE_MIN_TAIL` messages.
191+
*
192+
* Returns the same array reference when no truncation is needed.
193+
*/
194+
export function proactiveTruncate(messages: Message[]): Message[] {
195+
if (messages.length < PROACTIVE_TRUNCATE_MIN_TAIL) return messages
196+
197+
let totalChars = 0
198+
for (const msg of messages) {
199+
const content = msg.message?.content
200+
if (typeof content === 'string') {
201+
totalChars += content.length
202+
} else if (Array.isArray(content)) {
203+
for (const block of content) {
204+
if (typeof block === 'string') {
205+
totalChars += (block as string).length
206+
} else if (block && typeof block === 'object') {
207+
const obj = block as unknown as Record<string, unknown>
208+
const text = obj.text ?? obj.content
209+
if (typeof text === 'string') {
210+
totalChars += text.length
211+
}
212+
}
213+
}
214+
}
215+
}
216+
217+
if (totalChars < PROACTIVE_TRUNCATE_CHARS) return messages
218+
219+
// Find last compact_boundary — the standard anchor point
220+
let boundaryIdx = -1
221+
for (let i = messages.length - 1; i >= 0; i--) {
222+
const msg = messages[i]!
223+
if (
224+
msg.type === 'system' &&
225+
(msg as Record<string, unknown>).subtype === 'compact_boundary'
226+
) {
227+
boundaryIdx = i
228+
break
229+
}
230+
}
231+
232+
const keepFrom =
233+
boundaryIdx >= 0
234+
? boundaryIdx
235+
: Math.max(0, messages.length - PROACTIVE_TRUNCATE_MIN_TAIL)
236+
if (keepFrom === 0) return messages
237+
238+
return messages.slice(keepFrom)
239+
}

src/utils/telemetry/instrumentation.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,10 +206,49 @@ async function getOtlpReaders() {
206206

207207
return exporters.map(exporter => {
208208
if ('export' in exporter) {
209-
return new PeriodicExportingMetricReader({
209+
const reader = new PeriodicExportingMetricReader({
210210
exporter,
211211
exportIntervalMillis: exportInterval,
212212
})
213+
// Wrap the export callback to auto-shutdown the reader on auth
214+
// failures (401/403). Without this the PeriodicExportingMetricReader's
215+
// internal setInterval keeps retrying forever, leaking handles.
216+
const originalExport = (
217+
exporter as unknown as {
218+
export: (
219+
metrics: unknown,
220+
callback: (result: { error?: Error }) => void,
221+
) => unknown
222+
}
223+
).export.bind(exporter)
224+
;(
225+
exporter as unknown as {
226+
export: (
227+
metrics: unknown,
228+
callback: (result: { error?: Error }) => void,
229+
) => unknown
230+
}
231+
).export = (metrics, callback) => {
232+
return originalExport(metrics, result => {
233+
if (result.error) {
234+
const msg = result.error.message || ''
235+
if (
236+
msg.includes('401') ||
237+
msg.includes('403') ||
238+
msg.includes('Unauthorized') ||
239+
msg.includes('authentication')
240+
) {
241+
logForDebugging(
242+
`[3P telemetry] Auth error detected, shutting down metric reader`,
243+
{ level: 'error' },
244+
)
245+
void reader.shutdown()
246+
}
247+
}
248+
callback(result)
249+
})
250+
}
251+
return reader
213252
}
214253
return exporter
215254
})

vite.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export default defineConfig({
8383
target: 'es2020',
8484
copyPublicDir: false,
8585
sourcemap: false,
86-
minify: false,
86+
minify: true,
8787

8888
// SSR build mode — uses Rollup with Node.js target
8989
ssr: true,

0 commit comments

Comments
 (0)