Skip to content

Commit 14f43e9

Browse files
claude-code-bestglm-5.2
andcommitted
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>
1 parent 2e29e36 commit 14f43e9

1 file changed

Lines changed: 26 additions & 12 deletions

File tree

src/query.ts

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -522,21 +522,35 @@ async function* queryLoop(
522522

523523
let messagesForQuery = getMessagesAfterCompactBoundary(messages)
524524

525-
// Release toolUseResult payloads from previous turns. By this point the
526-
// UI has already rendered those results and the next API call only needs
527-
// message.message.content (tool_result blocks), not the raw output object.
528-
// This prevents unbounded memory growth in long sessions before compact
529-
// triggers — a single FileRead of a 400KB file would otherwise stay in
530-
// mutableMessages forever.
531-
for (const msg of messagesForQuery) {
525+
// Release toolUseResult payloads from previous turns — the next API call
526+
// only needs message.message.content (tool_result blocks), not the raw
527+
// output object. This prevents unbounded memory growth in long sessions
528+
// before compact triggers (a single FileRead of a 400KB file would
529+
// otherwise stay in mutableMessages forever).
530+
//
531+
// IMPORTANT: shallow-copy rather than mutate. messagesForQuery elements
532+
// are references shared with mutableMessages (UI state); deleting
533+
// toolUseResult in place strips it from the live message while React may
534+
// still be rendering it. The next query can start within milliseconds of
535+
// tool_result creation (model immediately calls the next tool), before
536+
// the UI commit lands — UserToolSuccessMessage reads
537+
// message.toolUseResult to delegate to tool.renderToolResultMessage, so a
538+
// mutation race makes tool-result rows render blank. Map to a stripped
539+
// copy so mutableMessages keeps the original for the UI; downstream API
540+
// transformations (applyToolResultBudget, snip, microcompact) already
541+
// build new arrays via .map(), so they compose cleanly with this copy.
542+
messagesForQuery = messagesForQuery.map(msg => {
532543
if (
533-
msg.type === 'user' &&
534-
'toolUseResult' in msg &&
535-
msg.toolUseResult !== undefined
544+
msg.type !== 'user' ||
545+
!('toolUseResult' in msg) ||
546+
(msg as { toolUseResult?: unknown }).toolUseResult === undefined
536547
) {
537-
delete (msg as Message & { toolUseResult?: unknown }).toolUseResult
548+
return msg
538549
}
539-
}
550+
const copy: typeof msg = { ...msg }
551+
delete (copy as Message & { toolUseResult?: unknown }).toolUseResult
552+
return copy
553+
})
540554

541555
let tracking = autoCompactTracking
542556

0 commit comments

Comments
 (0)