fix: convert orphaned tool_results to text blocks after condensing (#10927)

* fix: convert orphaned tool_results to text blocks after condensing

When condensing occurs after assistant sends tool_uses but before user responds,
the tool_use blocks get condensed away. User messages containing tool_results that
reference condensed tool_use_ids become orphaned and get filtered out by
getEffectiveApiHistory, causing user feedback to be lost.

This fix enhances the existing check in addToApiConversationHistory to detect when
the previous effective message is not an assistant and converts any tool_result
blocks to text blocks, preventing them from being filtered as orphans.

The conversion happens at the latest possible moment (message insertion) because:
- Tool results are created before we know if condensing will occur
- We need actual effective history state to make the decision
- This is the last checkpoint before orphan filtering happens

* Only include environment details in summary for automatic condensing

For automatic condensing (during attemptApiRequest), environment details
are included in the summary because the API request is already in progress
and the next user message won't have fresh environment details injected.

For manual condensing (via condenseContext button), environment details
are NOT included because fresh details will be injected on the very next
turn via getEnvironmentDetails() in recursivelyMakeClineRequests().

This uses the existing isAutomaticTrigger flag to differentiate behavior.

---------

Co-authored-by: Hannes Rudolph <hrudolph@gmail.com>
This commit is contained in:
Daniel 2026-01-23 19:04:36 -05:00 committed by GitHub
parent 526488e5b6
commit 339f5aad48
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 34 additions and 5 deletions

View file

@ -131,7 +131,14 @@ export type SummarizeResponse = {
* - Post-condense, the model sees only the summary (true fresh start)
* - All messages are still stored but tagged with condenseParent
* - <command> blocks from the original task are preserved across condensings
* - <environment_details> is included to provide current workspace context
*
* Environment details handling:
* - For AUTOMATIC condensing (isAutomaticTrigger=true): Environment details are included
* in the summary because the API request is already in progress and the next user
* message won't have fresh environment details injected.
* - For MANUAL condensing (isAutomaticTrigger=false): Environment details are NOT included
* because fresh environment details will be injected on the very next turn via
* getEnvironmentDetails() in recursivelyMakeClineRequests().
*
* @param {ApiMessage[]} messages - The conversation messages
* @param {ApiHandler} apiHandler - The API handler to use for summarization and token counting
@ -140,7 +147,7 @@ export type SummarizeResponse = {
* @param {boolean} isAutomaticTrigger - Whether the summarization is triggered automatically
* @param {string} customCondensingPrompt - Optional custom prompt to use for condensing
* @param {ApiHandlerCreateMessageMetadata} metadata - Optional metadata to pass to createMessage (tools, taskId, etc.)
* @param {string} environmentDetails - Optional environment details string to include in the summary
* @param {string} environmentDetails - Optional environment details string to include in the summary (only used when isAutomaticTrigger=true)
* @returns {SummarizeResponse} - The result of the summarization operation (see above)
*/
export async function summarizeConversation(
@ -294,8 +301,10 @@ ${commandBlocks}
})
}
// Add environment details as a separate text block if provided
if (environmentDetails?.trim()) {
// Add environment details as a separate text block if provided AND this is an automatic trigger.
// For manual condensing, fresh environment details will be injected on the next turn.
// For automatic condensing, the API request is already in progress so we need them in the summary.
if (isAutomaticTrigger && environmentDetails?.trim()) {
summaryContent.push({
type: "text",
text: environmentDetails,

View file

@ -1003,7 +1003,27 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
const effectiveHistoryForValidation = getEffectiveApiHistory(this.apiConversationHistory)
const lastEffective = effectiveHistoryForValidation[effectiveHistoryForValidation.length - 1]
const historyForValidation = lastEffective?.role === "assistant" ? effectiveHistoryForValidation : []
const validatedMessage = validateAndFixToolResultIds(message, historyForValidation)
// If the previous effective message is NOT an assistant, convert tool_result blocks to text blocks.
// This prevents orphaned tool_results from being filtered out by getEffectiveApiHistory.
// This can happen when condensing occurs after the assistant sends tool_uses but before
// the user responds - the tool_use blocks get condensed away, leaving orphaned tool_results.
let messageToAdd = message
if (lastEffective?.role !== "assistant" && Array.isArray(message.content)) {
messageToAdd = {
...message,
content: message.content.map((block) =>
block.type === "tool_result"
? {
type: "text" as const,
text: `Tool result:\n${typeof block.content === "string" ? block.content : JSON.stringify(block.content)}`,
}
: block,
),
}
}
const validatedMessage = validateAndFixToolResultIds(messageToAdd, historyForValidation)
const messageWithTs = { ...validatedMessage, ts: Date.now() }
this.apiConversationHistory.push(messageWithTs)
}