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);