mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-20 15:25:50 +00:00
fix(query): shallow-copy messages before stripping toolUseResult
Previously the per-query cleanup mutated messagesForQuery entries in place via `delete msg.toolUseResult`. Those entries are references shared with mutableMessages (UI state), so the delete stripped the field from the live message object. The next query can start within milliseconds of tool_result creation — before the React UI commit lands — so UserToolSuccessMessage's `!message.toolUseResult` check returned null and tool.renderToolResultMessage was never called, leaving tool-result rows blank. Map to a stripped copy instead so mutableMessages keeps the original for the UI. Downstream API transformations (applyToolResultBudget, snip, microcompact) already build new arrays via .map(), so they compose cleanly with this copy. Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>
This commit is contained in:
38
src/query.ts
38
src/query.ts
@@ -522,21 +522,35 @@ 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) {
|
||||
// Release toolUseResult payloads from previous turns — 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).
|
||||
//
|
||||
// IMPORTANT: shallow-copy rather than mutate. messagesForQuery elements
|
||||
// are references shared with mutableMessages (UI state); deleting
|
||||
// toolUseResult in place strips it from the live message while React may
|
||||
// still be rendering it. The next query can start within milliseconds of
|
||||
// tool_result creation (model immediately calls the next tool), before
|
||||
// the UI commit lands — UserToolSuccessMessage reads
|
||||
// message.toolUseResult to delegate to tool.renderToolResultMessage, so a
|
||||
// mutation race makes tool-result rows render blank. Map to a stripped
|
||||
// copy so mutableMessages keeps the original for the UI; downstream API
|
||||
// transformations (applyToolResultBudget, snip, microcompact) already
|
||||
// build new arrays via .map(), so they compose cleanly with this copy.
|
||||
messagesForQuery = messagesForQuery.map(msg => {
|
||||
if (
|
||||
msg.type === 'user' &&
|
||||
'toolUseResult' in msg &&
|
||||
msg.toolUseResult !== undefined
|
||||
msg.type !== 'user' ||
|
||||
!('toolUseResult' in msg) ||
|
||||
(msg as { toolUseResult?: unknown }).toolUseResult === undefined
|
||||
) {
|
||||
delete (msg as Message & { toolUseResult?: unknown }).toolUseResult
|
||||
return msg
|
||||
}
|
||||
}
|
||||
const copy: typeof msg = { ...msg }
|
||||
delete (copy as Message & { toolUseResult?: unknown }).toolUseResult
|
||||
return copy
|
||||
})
|
||||
|
||||
let tracking = autoCompactTracking
|
||||
|
||||
|
||||
Reference in New Issue
Block a user