Skip to content

Commit b2f6d3e

Browse files
author
Zoo (VP)
committed
fix(error-interception): add logging to silent error paths
1 parent 41c5c5e commit b2f6d3e

2 files changed

Lines changed: 42 additions & 31 deletions

File tree

src/core/assistant-message/presentAssistantMessage.ts

Lines changed: 37 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ export async function presentAssistantMessage(cline: Task) {
124124
// their original name in API history
125125
const mcpBlock = block as McpToolUse
126126
const interceptor = toolErrorInterceptor
127-
127+
128128
if (cline.didRejectTool) {
129129
// For native protocol, we must send a tool_result for every tool_use to avoid API errors
130130
const toolCallId = mcpBlock.id
@@ -258,7 +258,7 @@ export async function presentAssistantMessage(cline: Task) {
258258
)
259259
rawPushToolResult(formatResponse.toolError(errorString))
260260
}
261-
261+
262262
const { decoratedHandleError: handleError, decoratedPushToolResult: pushToolResult } =
263263
interceptor.createInterceptor(
264264
cline,
@@ -391,8 +391,11 @@ export async function presentAssistantMessage(cline: Task) {
391391
) {
392392
;(cline as any).recordToolError((block as any).name as ToolName, errorMessage)
393393
}
394-
} catch {
395-
// Best-effort only
394+
} catch (recordErr) {
395+
console.warn(
396+
"[ErrorInterception] Failed to record tool error:",
397+
recordErr instanceof Error ? recordErr.message : recordErr,
398+
)
396399
}
397400
cline.consecutiveMistakeCount++
398401
await cline.say("error", errorMessage)
@@ -516,20 +519,23 @@ export async function presentAssistantMessage(cline: Task) {
516519
// which is a distinct failure pattern from missing args.
517520
const parseErrorMessage = NativeToolCallParser.consumeParseError(toolCallId)
518521
const isInvalidJson = parseErrorMessage !== undefined
519-
522+
520523
const errorMessage = isInvalidJson
521524
? `Invalid tool call for '${block.name}': arguments could not be parsed as JSON. ` +
522525
`This usually means multiple JSON objects were concatenated into a single arguments string.`
523526
: `Invalid tool call for '${block.name}': missing nativeArgs. ` +
524527
`This usually means the model streamed invalid or incomplete arguments and the call could not be finalized.`
525-
528+
526529
cline.consecutiveMistakeCount++
527530
try {
528531
cline.recordToolError(block.name as ToolName, errorMessage)
529-
} catch {
530-
// Best-effort only
532+
} catch (recordErr) {
533+
console.warn(
534+
"[ErrorInterception] Failed to record tool error:",
535+
recordErr instanceof Error ? recordErr.message : recordErr,
536+
)
531537
}
532-
538+
533539
// Convert missing nativeArgs into a structured guided payload.
534540
// When a JSON.parse error was recorded, route to the
535541
// INVALID_JSON_ARGUMENTS pattern; otherwise fall back to
@@ -540,11 +546,9 @@ export async function presentAssistantMessage(cline: Task) {
540546
taskId: cline.taskId,
541547
toolCallId,
542548
toolName: block.name,
543-
metadata: isInvalidJson
544-
? { invalidJsonArguments: true }
545-
: { missingNativeArgs: true },
549+
metadata: isInvalidJson ? { invalidJsonArguments: true } : { missingNativeArgs: true },
546550
})
547-
551+
548552
// Push tool_result directly without setting didAlreadyUseTool so streaming can
549553
// continue gracefully.
550554
const missingArgsGuide = getTaskErrorState(cline).consumePendingNativeProtocolGuide()
@@ -555,7 +559,7 @@ export async function presentAssistantMessage(cline: Task) {
555559
content: missingArgsGuide ? `${missingArgsBase}\n\n${missingArgsGuide}` : missingArgsBase,
556560
is_error: true,
557561
})
558-
562+
559563
break
560564
}
561565
}
@@ -592,7 +596,7 @@ export async function presentAssistantMessage(cline: Task) {
592596
if (pendingGuide) {
593597
resultContent = `${resultContent}\n\n${pendingGuide}`
594598
}
595-
599+
596600
// Merge approval feedback into tool result (GitHub #10465)
597601
if (approvalFeedback) {
598602
const feedbackText = formatResponse.toolApprovedWithFeedback(approvalFeedback.text)
@@ -677,7 +681,7 @@ export async function presentAssistantMessage(cline: Task) {
677681

678682
rawPushToolResult(formatResponse.toolError(errorString))
679683
}
680-
684+
681685
const { decoratedHandleError: handleError, decoratedPushToolResult: pushToolResult } =
682686
interceptor.createInterceptor(
683687
cline,
@@ -708,7 +712,7 @@ export async function presentAssistantMessage(cline: Task) {
708712
})
709713
}
710714
}
711-
715+
712716
// Structural preflight: detect malformed native tool arguments before
713717
// approval or execution. CWD_OBJECT_MISUSE and NESTED_PARAM_OVERFLOW
714718
// signals block the malformed call and return exactly one guided
@@ -721,7 +725,7 @@ export async function presentAssistantMessage(cline: Task) {
721725
: []),
722726
validateNestedParams(block.nativeArgs as Record<string, unknown>, String(block.name)),
723727
].filter((s): s is NonNullable<typeof s> => s != null)
724-
728+
725729
if (structuralSignals.length > 0) {
726730
const signal = structuralSignals[0]
727731
const variant = (signal.metadata?.variant as string | undefined) ?? "STRUCTURAL_MISUSE"
@@ -735,7 +739,7 @@ export async function presentAssistantMessage(cline: Task) {
735739
taskErrorState.setFingerprint("PARAM_TYPE_MISMATCH", fingerprint)
736740
const occurrence = taskErrorState.incrementOccurrence("PARAM_TYPE_MISMATCH")
737741
const circuitOpen = taskErrorState.isOpen("PARAM_TYPE_MISMATCH")
738-
742+
739743
const parameter = (signal.metadata?.parameter as string | undefined) ?? "unknown"
740744
const errorMessage = circuitOpen
741745
? `[MODEL_STUCK_LOOP] The malformed '${String(block.name)}' call has failed ${occurrence} times with the same structural pattern (${variant} on parameter '${parameter}'). Stop retrying this invocation shape. Continue with a different tool or strategy.`
@@ -744,14 +748,17 @@ export async function presentAssistantMessage(cline: Task) {
744748
: variant === "CWD_OBJECT_MISUSE"
745749
? `[CWD_OBJECT_MISUSE] execute_command.cwd must be a single directory string (or omitted). A non-string value (type: ${String(signal.metadata?.actualType)}) was provided, likely because another tool-call object was nested inside it. Submit exactly one native execute_command with 'command' at the top level and 'cwd' as a workspace path string or omitted.`
746750
: `[NESTED_PARAM_OVERFLOW] The '${String(block.name)}' parameter '${parameter}' contains a nested tool invocation object (${String(signal.metadata?.signature ?? "unknown-signature")}). Issue each intended tool as a separate native tool call with only its own top-level parameters.`
747-
751+
748752
cline.consecutiveMistakeCount++
749753
try {
750754
cline.recordToolError(String(block.name) as ToolName, errorMessage)
751-
} catch {
752-
// Best-effort only
755+
} catch (recordErr) {
756+
console.warn(
757+
"[ErrorInterception] Failed to record tool error:",
758+
recordErr instanceof Error ? recordErr.message : recordErr,
759+
)
753760
}
754-
761+
755762
const guided = interceptor.transformError(cline, {
756763
source: "validation",
757764
stage: "preflight",
@@ -760,7 +767,7 @@ export async function presentAssistantMessage(cline: Task) {
760767
toolName: String(block.name),
761768
metadata: { ...signal.metadata, structuralPreflight: true, occurrence, circuitOpen },
762769
})
763-
770+
764771
const structuralGuide = taskErrorState.consumePendingNativeProtocolGuide()
765772
const structuralBase = guided ?? formatResponse.toolError(errorMessage)
766773
cline.pushToolResultToUserContent({
@@ -769,11 +776,11 @@ export async function presentAssistantMessage(cline: Task) {
769776
content: structuralGuide ? `${structuralBase}\n\n${structuralGuide}` : structuralBase,
770777
is_error: true,
771778
})
772-
779+
773780
break
774781
}
775782
}
776-
783+
777784
// Validate tool use before execution - ONLY for complete (non-partial) blocks.
778785
// Validating partial blocks would cause validation errors to be thrown repeatedly
779786
// during streaming, pushing multiple tool_results for the same tool_use_id and
@@ -844,7 +851,7 @@ export async function presentAssistantMessage(cline: Task) {
844851
content: validationGuide ? `${validationBase}\n\n${validationGuide}` : validationBase,
845852
is_error: true,
846853
})
847-
854+
848855
break
849856
}
850857
}
@@ -865,7 +872,7 @@ export async function presentAssistantMessage(cline: Task) {
865872
signalMetadata.repetitionCount = blockDetails.consecutiveCount
866873
signalMetadata.fingerprint = blockDetails.fingerprint
867874
}
868-
875+
869876
const guided = interceptor.transformError(cline, {
870877
source: "repetition",
871878
stage: "result",
@@ -874,11 +881,11 @@ export async function presentAssistantMessage(cline: Task) {
874881
toolName: block.name,
875882
metadata: signalMetadata,
876883
})
877-
884+
878885
if (guided) {
879886
pushToolResult(guided)
880887
}
881-
888+
882889
// Handle repetition similar to mistake_limit_reached pattern.
883890
const { response, text, images } = await cline.ask(
884891
repetitionCheck.askUser.messageKey as ClineAsk,

src/core/tools/error-interception/ToolErrorInterceptor.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,9 @@ export class ToolErrorInterceptor {
251251
): ToolResponse | undefined {
252252
const classification = classifyError(signal)
253253
if (classification.category === "UNCLASSIFIED" || classification.patternId === "EI/UNCLASSIFIED/001") {
254+
console.warn(
255+
`[ErrorInterceptor] Unclassified error pattern — passing through without guidance. tool=${signal.toolName ?? "unknown"} patternId=${classification.patternId}`,
256+
)
254257
return undefined
255258
}
256259

@@ -294,7 +297,8 @@ export class ToolErrorInterceptor {
294297
if (trimmed.startsWith("<error_details>")) return true
295298
if (trimmed.startsWith("File does not exist")) return true
296299
if (trimmed.startsWith("cannot find path") || trimmed.startsWith("Path not found")) return true
297-
if (trimmed.startsWith("apply_diff failed") || trimmed.includes("no sufficiently similar match")) return true
300+
if (trimmed.startsWith("apply_diff failed") || trimmed.includes("no sufficiently similar match"))
301+
return true
298302
return false
299303
}
300304

0 commit comments

Comments
 (0)