From cf2bf29dcd712a70e05d15c5ad47633515547b4f Mon Sep 17 00:00:00 2001 From: claude-code-best Date: Tue, 5 May 2026 12:41:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=B0=9D=E8=AF=95=E6=B7=B1=E5=BA=A6?= =?UTF-8?q?=E6=8B=B7=E8=B4=9D=E6=95=B0=E6=8D=AE=E4=BB=A5=E5=88=86=E7=A6=BB?= =?UTF-8?q?=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/query.ts | 16 ++++++++++++++++ src/services/compact/compact.ts | 21 ++++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/query.ts b/src/query.ts index 106c88fa5..a53d238cb 100644 --- a/src/query.ts +++ b/src/query.ts @@ -479,6 +479,22 @@ async function* queryLoop( let messagesForQuery = getMessagesAfterCompactBoundary(messages) + // Release toolUseResult payloads from previous turns. By this point the + // UI has already rendered those results and the next API call only needs + // message.message.content (tool_result blocks), not the raw output object. + // This prevents unbounded memory growth in long sessions before compact + // triggers — a single FileRead of a 400KB file would otherwise stay in + // mutableMessages forever. + for (const msg of messagesForQuery) { + if ( + msg.type === 'user' && + 'toolUseResult' in msg && + msg.toolUseResult !== undefined + ) { + delete (msg as Message & { toolUseResult?: unknown }).toolUseResult + } + } + let tracking = autoCompactTracking // Enforce per-message budget on aggregate tool result size. Runs BEFORE diff --git a/src/services/compact/compact.ts b/src/services/compact/compact.ts index cc34a1154..40bb76a04 100644 --- a/src/services/compact/compact.ts +++ b/src/services/compact/compact.ts @@ -336,12 +336,31 @@ export type RecompactionInfo = { export function buildPostCompactMessages(result: CompactionResult): Message[] { return ([result.boundaryMarker] as Message[]).concat( result.summaryMessages, - result.messagesToKeep ?? [], + stripToolUseResults(result.messagesToKeep), result.attachments, result.hookResults, ) } +/** Release large tool result payloads from kept messages after compaction. + * toolUseResult is only used for UI rendering, not API calls. */ +function stripToolUseResults(messages: Message[] | undefined): Message[] { + if (!messages) return [] + return messages.map(msg => { + if ( + msg.type === 'user' && + 'toolUseResult' in msg && + msg.toolUseResult !== undefined + ) { + const { toolUseResult, ...rest } = msg as Message & { + toolUseResult: unknown + } + return rest as Message + } + return msg + }) +} + /** * Annotate a compact boundary with relink metadata for messagesToKeep. * Preserved messages keep their original parentUuids on disk (dedup-skipped);