@@ -48,6 +48,14 @@ import { codebaseSearchTool } from "../tools/CodebaseSearchTool"
4848import { formatResponse } from "../prompts/responses"
4949import { sanitizeToolUseId } from "../../utils/tool-id"
5050
51+ /**
52+ * Module-scoped interceptor singleton. A single shared instance keeps the
53+ * per-task WeakMap alive across content blocks within the same Task, so
54+ * occurrence counters and circuit breakers persist between tool blocks.
55+ * Recreating one per block would reset all per-task counters to empty.
56+ */
57+ const toolErrorInterceptor = createToolErrorInterceptor ( )
58+
5159/**
5260 * Processes and presents assistant message content to the user interface.
5361 *
@@ -114,7 +122,7 @@ export async function presentAssistantMessage(cline: Task) {
114122 // These are converted to the same execution path as use_mcp_tool but preserve
115123 // their original name in API history
116124 const mcpBlock = block as McpToolUse
117- const interceptor = createToolErrorInterceptor ( )
125+ const interceptor = toolErrorInterceptor
118126
119127 if ( cline . didRejectTool ) {
120128 // For native protocol, we must send a tool_result for every tool_use to avoid API errors
@@ -124,10 +132,13 @@ export async function presentAssistantMessage(cline: Task) {
124132 : `MCP tool ${ mcpBlock . name } was interrupted and not executed due to user rejecting a previous tool.`
125133
126134 if ( toolCallId ) {
135+ // Consume any pending native protocol guide so it cannot leak
136+ // into later turns when this early tool_result path is taken.
137+ const rejectedMcpGuide = getTaskErrorState ( cline ) . consumePendingNativeProtocolGuide ( )
127138 cline . pushToolResultToUserContent ( {
128139 type : "tool_result" ,
129140 tool_use_id : sanitizeToolUseId ( toolCallId ) ,
130- content : errorMessage ,
141+ content : rejectedMcpGuide ? ` ${ errorMessage } \n\n ${ rejectedMcpGuide } ` : errorMessage ,
131142 is_error : true ,
132143 } )
133144 }
@@ -175,6 +186,12 @@ export async function presentAssistantMessage(cline: Task) {
175186 }
176187
177188 if ( toolCallId ) {
189+ // Merge any pending XML_NATIVE_DUAL_PROTOCOL guide into this
190+ // tool_result and clear it so it cannot leak into later turns.
191+ const mcpPendingGuide = getTaskErrorState ( cline ) . consumePendingNativeProtocolGuide ( )
192+ if ( mcpPendingGuide ) {
193+ resultContent = `${ resultContent } \n\n${ mcpPendingGuide } `
194+ }
178195 cline . pushToolResultToUserContent ( {
179196 type : "tool_result" ,
180197 tool_use_id : sanitizeToolUseId ( toolCallId ) ,
@@ -346,9 +363,10 @@ export async function presentAssistantMessage(cline: Task) {
346363 "INVALID_TOOL_PROTOCOL" ,
347364 "INVALID_TOOL_PROTOCOL|XML_NATIVE_DUAL_PROTOCOL|text-block" ,
348365 )
349- ; ( cline as any ) . pendingNativeProtocolGuide =
366+ taskErrorState . setPendingNativeProtocolGuide (
350367 `[XML_NATIVE_DUAL_PROTOCOL occurrence=${ occurrence } ] XML tool calls are not supported. ` +
351- `Use native tool_use only. The XML markup was removed from the visible text; only the native tool call was executed.`
368+ `Use native tool_use only. The XML markup was removed from the visible text; only the native tool call was executed.` ,
369+ )
352370 }
353371 }
354372 }
@@ -359,7 +377,7 @@ export async function presentAssistantMessage(cline: Task) {
359377 case "tool_use" : {
360378 // Native tool calling is the only supported tool calling mechanism.
361379 // A tool_use block without an id is invalid and cannot be executed.
362- const interceptor = createToolErrorInterceptor ( )
380+ const interceptor = toolErrorInterceptor
363381 const toolCallId = ( block as any ) . id as string | undefined
364382 if ( ! toolCallId ) {
365383 const errorMessage =
@@ -464,10 +482,13 @@ export async function presentAssistantMessage(cline: Task) {
464482 ? `Skipping tool ${ toolDescription ( ) } due to user rejecting a previous tool.`
465483 : `Tool ${ toolDescription ( ) } was interrupted and not executed due to user rejecting a previous tool.`
466484
485+ // Consume any pending native protocol guide so it cannot leak
486+ // into later turns when this early tool_result path is taken.
487+ const rejectedGuide = getTaskErrorState ( cline ) . consumePendingNativeProtocolGuide ( )
467488 cline . pushToolResultToUserContent ( {
468489 type : "tool_result" ,
469490 tool_use_id : sanitizeToolUseId ( toolCallId ) ,
470- content : errorMessage ,
491+ content : rejectedGuide ? ` ${ errorMessage } \n\n ${ rejectedGuide } ` : errorMessage ,
471492 is_error : true ,
472493 } )
473494
@@ -511,13 +532,15 @@ export async function presentAssistantMessage(cline: Task) {
511532
512533 // Push tool_result directly without setting didAlreadyUseTool so streaming can
513534 // continue gracefully.
535+ const missingArgsGuide = getTaskErrorState ( cline ) . consumePendingNativeProtocolGuide ( )
536+ const missingArgsBase = guided ?? formatResponse . toolError ( errorMessage )
514537 cline . pushToolResultToUserContent ( {
515538 type : "tool_result" ,
516539 tool_use_id : sanitizeToolUseId ( toolCallId ) ,
517- content : guided ?? formatResponse . toolError ( errorMessage ) ,
540+ content : missingArgsGuide ? ` ${ missingArgsBase } \n\n ${ missingArgsGuide } ` : missingArgsBase ,
518541 is_error : true ,
519542 } )
520-
543+
521544 break
522545 }
523546 }
@@ -550,10 +573,9 @@ export async function presentAssistantMessage(cline: Task) {
550573 // Merge any pending XML_NATIVE_DUAL_PROTOCOL guide into this native
551574 // tool's result. The native result remains primary; the warning is
552575 // appended once and then cleared so it cannot leak into later turns.
553- const pendingGuide = ( cline as any ) . pendingNativeProtocolGuide as string | undefined
576+ const pendingGuide = getTaskErrorState ( cline ) . consumePendingNativeProtocolGuide ( )
554577 if ( pendingGuide ) {
555578 resultContent = `${ resultContent } \n\n${ pendingGuide } `
556- ; ( cline as any ) . pendingNativeProtocolGuide = undefined
557579 }
558580
559581 // Merge approval feedback into tool result (GitHub #10465)
@@ -679,14 +701,22 @@ export async function presentAssistantMessage(cline: Task) {
679701 if ( ! block . partial && block . nativeArgs ) {
680702 const taskErrorState = getTaskErrorState ( cline )
681703 const structuralSignals = [
682- validateCwdParameter ( block . nativeArgs as Record < string , unknown > , String ( block . name ) ) ,
704+ ...( block . name === "execute_command"
705+ ? [ validateCwdParameter ( block . nativeArgs as Record < string , unknown > , String ( block . name ) ) ]
706+ : [ ] ) ,
683707 validateNestedParams ( block . nativeArgs as Record < string , unknown > , String ( block . name ) ) ,
684708 ] . filter ( ( s ) : s is NonNullable < typeof s > => s != null )
685709
686710 if ( structuralSignals . length > 0 ) {
687711 const signal = structuralSignals [ 0 ]
688712 const variant = ( signal . metadata ?. variant as string | undefined ) ?? "STRUCTURAL_MISUSE"
689713 const fingerprint = `PARAM_TYPE_MISMATCH|${ variant } |${ String ( block . name ) } |${ ( signal . metadata ?. parameter as string | undefined ) ?? "" } `
714+ // If the structural failure shape changed (different tool, variant, or
715+ // parameter), reset the circuit so the new shape gets fresh guidance
716+ // instead of inheriting MODEL_STUCK_LOOP from an unrelated pattern.
717+ if ( taskErrorState . getFingerprint ( "PARAM_TYPE_MISMATCH" ) !== fingerprint ) {
718+ taskErrorState . reset ( "PARAM_TYPE_MISMATCH" )
719+ }
690720 taskErrorState . setFingerprint ( "PARAM_TYPE_MISMATCH" , fingerprint )
691721 const occurrence = taskErrorState . incrementOccurrence ( "PARAM_TYPE_MISMATCH" )
692722 const circuitOpen = taskErrorState . isOpen ( "PARAM_TYPE_MISMATCH" )
@@ -716,10 +746,12 @@ export async function presentAssistantMessage(cline: Task) {
716746 metadata : { ...signal . metadata , structuralPreflight : true , occurrence, circuitOpen } ,
717747 } )
718748
749+ const structuralGuide = taskErrorState . consumePendingNativeProtocolGuide ( )
750+ const structuralBase = guided ?? formatResponse . toolError ( errorMessage )
719751 cline . pushToolResultToUserContent ( {
720752 type : "tool_result" ,
721753 tool_use_id : sanitizeToolUseId ( toolCallId ) ,
722- content : guided ?? formatResponse . toolError ( errorMessage ) ,
754+ content : structuralGuide ? ` ${ structuralBase } \n\n ${ structuralGuide } ` : structuralBase ,
723755 is_error : true ,
724756 } )
725757
@@ -789,13 +821,15 @@ export async function presentAssistantMessage(cline: Task) {
789821 metadata : validationMetadata ,
790822 } )
791823 // Push tool_result directly without setting didAlreadyUseTool
824+ const validationGuide = getTaskErrorState ( cline ) . consumePendingNativeProtocolGuide ( )
825+ const validationBase = guided ? `${ guided } \n\n${ errorMessage } ` : errorMessage
792826 cline . pushToolResultToUserContent ( {
793827 type : "tool_result" ,
794828 tool_use_id : sanitizeToolUseId ( toolCallId ) ,
795- content : guided ?? errorMessage ,
829+ content : validationGuide ? ` ${ validationBase } \n\n ${ validationGuide } ` : validationBase ,
796830 is_error : true ,
797831 } )
798-
832+
799833 break
800834 }
801835 }
@@ -1119,10 +1153,12 @@ export async function presentAssistantMessage(cline: Task) {
11191153 toolName : block . name ,
11201154 metadata : { typeMismatch : true } ,
11211155 } )
1156+ const unknownToolGuide = getTaskErrorState ( cline ) . consumePendingNativeProtocolGuide ( )
1157+ const unknownToolBase = guided ?? formatResponse . toolError ( errorMessage )
11221158 cline . pushToolResultToUserContent ( {
11231159 type : "tool_result" ,
11241160 tool_use_id : sanitizeToolUseId ( toolCallId ) ,
1125- content : guided ?? formatResponse . toolError ( errorMessage ) ,
1161+ content : unknownToolGuide ? ` ${ unknownToolBase } \n\n ${ unknownToolGuide } ` : unknownToolBase ,
11261162 is_error : true ,
11271163 } )
11281164 break
0 commit comments