Skip to content

feat: Add Error Interception Middleware for guided AI self-correcting tool errors#1009

Open
myk1yt wants to merge 3 commits into
Zoo-Code-Org:mainfrom
myk1yt:feat/error-interception-middleware
Open

feat: Add Error Interception Middleware for guided AI self-correcting tool errors#1009
myk1yt wants to merge 3 commits into
Zoo-Code-Org:mainfrom
myk1yt:feat/error-interception-middleware

Conversation

@myk1yt

@myk1yt myk1yt commented Jul 24, 2026

Copy link
Copy Markdown

Summary

Implements a deterministic error interception middleware that transforms raw LLM tool execution errors into structured WHAT/WHY/NEXT guidance messages, enabling AI models to self-correct without user intervention.

Closes #1000

Architecture

Model Provider
  → presentAssistantMessage dispatch
    → StructuralValidator.validateToolUse()  ← CWD + NESTED preflight
    → XML dual protocol detection            ← text block XML detection
    → Tool.execute()
    → ToolErrorInterceptor.transformError()  ← error classification + guidance
    → Exactly one tool_result
  → Model Provider (next turn)

New Modules

Module Role
errorPatterns.ts 11-category pattern DB with variant entries
ErrorClassifier.ts Two-pass deterministic classifier (priority-ordered)
MessageTransformer.ts Versioned JSON guidance serializer (1024 byte limit)
ToolErrorInterceptor.ts Interceptor facade with per-task state
TaskErrorState.ts WeakMap-based task-scoped persistent state
StructuralValidator.ts CWD + nested parameter preflight validator

Covered Error Categories (11)

Category Pattern ID retryPolicy
DUPLICATE_CALL EI/DUPLICATE_CALL/001 do-not-retry
PARAM_MISSING EI/PARAM_MISSING/001 correct-and-retry
PARAM_TYPE_MISMATCH EI/PARAM_TYPE_MISMATCH/001-003 correct-and-retry
FILE_NOT_FOUND EI/FILE_NOT_FOUND/001 alternate-tool
SHELL_INTEGRATION EI/SHELL_INTEGRATION/001 alternate-tool
DIFF_MATCH_FAILED EI/DIFF_MATCH_FAILED/001 correct-and-retry
MCP_TOOL_MISSING EI/MCP_TOOL_MISSING/001 alternate-tool
INVALID_TOOL_PROTOCOL EI/INVALID_TOOL_PROTOCOL/001-002 do-not-retry
CONTEXT_OVERFLOW EI/CONTEXT_OVERFLOW/001 auto-recover
UNCLASSIFIED EI/UNCLASSIFIED/001 do-not-retry (fail-open)

Design Principles

  • Fail-Open: If the middleware fails, the original error passes through unmodified
  • Exactly-Once: One tool_result per tool_use_id, guaranteed
  • Defense-in-Depth: Middleware complements existing guards (ToolRepetitionDetector, validateToolUse, isToolAllowedForMode)
  • Task-Scoped State: Error counters and circuit breaker bound to Task lifecycle via WeakMap

Test Coverage

  • 113+ error-interception tests passing
  • 7016 total project tests passing (0 failures)
  • Zero TypeScript errors (tsc --noEmit)
  • pnpm build successful

PR Scope

This PR includes error-interception middleware only. The Task.ts MODEL_STUCK_LOOP circuit breaker extension is deferred to a follow-up PR due to cross-branch dependencies with the stats feature.

Summary by CodeRabbit

  • New Features
    • Added structured, guided guidance for tool failures with actionable “what/why/next” and bounded, safe payload formatting.
    • Enhanced validation for malformed native tool arguments (e.g., invalid cwd and nested tool-shaped inputs) and improved handling when legacy and native tool markup conflict.
  • Bug Fixes
    • Added per-task circuit-breaker behavior to prevent repeated failures from getting stuck, including consistent exactly-once guided output and safer non-text handling.
  • Tests
    • Updated and expanded coverage for guided tool-error output, structural validation, message formatting/truncation, and interceptor behavior.

…ware

Implements a middleware layer that intercepts, classifies, and transforms
tool execution errors into structured WHAT/WHY/NEXT guidance messages,
enabling AI models to self-correct without user intervention.

New modules:
- errorPatterns.ts: 11-category pattern DB with variant entries
- ErrorClassifier.ts: Two-pass deterministic classifier
- MessageTransformer.ts: Versioned JSON guidance serializer
- ToolErrorInterceptor.ts: Interceptor facade with per-task state
- TaskErrorState.ts: WeakMap-based task-scoped persistent state
- StructuralValidator.ts: CWD + nested parameter preflight validator
- DIFF_MATCH_FAILED pattern for apply_diff near-miss errors
- Improved parallel tool call guidance for CWD_OBJECT_MISUSE and NESTED_PARAM_OVERFLOW

Closes Zoo-Code-Org#1000
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Guided error interception

Layer / File(s) Summary
Error contracts, classification, and guidance payloads
src/core/tools/error-interception/types.ts, src/core/tools/error-interception/errorPatterns.ts, src/core/tools/error-interception/ErrorClassifier.ts, src/core/tools/error-interception/MessageTransformer.ts, src/core/tools/error-interception/index.ts, src/core/tools/error-interception/__tests__/*
Adds typed interception signals, prioritized error patterns, sanitized classification, bounded UTF-8 guidance serialization, public exports, and classifier/transformer tests.
Stateful callback interception
src/core/tools/error-interception/TaskErrorState.ts, src/core/tools/error-interception/ToolErrorInterceptor.ts, src/core/tools/error-interception/__tests__/*
Adds task-scoped occurrence tracking, shell circuit handling, decorated tool callbacks, fail-open behavior, guided result transformation, reset operations, and interceptor tests.
Structural preflight and assistant-message integration
src/core/tools/error-interception/StructuralValidator.ts, src/core/tools/error-interception/__tests__/StructuralValidator.spec.ts, src/core/assistant-message/presentAssistantMessage.ts, src/core/assistant-message/__tests__/*
Adds bounded structural validation for malformed native arguments and routes XML/native conflicts, parse failures, validation failures, repetition blocks, custom-tool failures, and unknown tools through guided results. Updates assistant-message assertions for guided_tool_error payloads.
Repository support updates
apps/vscode-e2e/src/fixtures/apply-diff.ts, ci-fix-commit.ps1, commit-and-push.ps1, commit-message.txt
Updates the apply-diff fixture to expect structured mismatch guidance and adds commit/push helper scripts with the related commit message.

Estimated code review effort: 5 (Critical) | ~120 minutes

Suggested labels: awaiting-review

Suggested reviewers: taltas

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR defers the required MODEL_STUCK_LOOP breaker, so it does not fully satisfy issue #1000's linked requirements. Implement the three-strike MODEL_STUCK_LOOP breaker, reset the mistake count, change strategy guidance, and suppress Continue prompts.
Out of Scope Changes check ⚠️ Warning The added PowerShell commit helpers and commit-message file are unrelated to error interception and appear out of scope. Remove the commit automation scripts or move them to a separate PR if they are only for local workflow support.
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed Clear, concise title that matches the main change: guided error interception middleware.
Description check ✅ Passed Mostly matches the template with summary, implementation details, and testing info, but it lacks a dedicated Test Procedure section.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

🧹 Nitpick comments (10)
src/core/assistant-message/__tests__/presentAssistantMessage-images.spec.ts (1)

182-182: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Guided-payload assertions were weakened to a shared substring across both assistant-message specs. guided_tool_error is the payload type for every non-api_request category, so all three assertions pass even if the protocol violation degrades to UNCLASSIFIED — the interception wiring is no longer pinned to a classification.

  • src/core/assistant-message/__tests__/presentAssistantMessage-images.spec.ts#L182-L182: parse the text block as JSON and assert category === "INVALID_TOOL_PROTOCOL" (and optionally pattern_id).
  • src/core/assistant-message/__tests__/presentAssistantMessage-images.spec.ts#L286-L286: apply the same parsed-category assertion for the rejected-subsequent-call case.
  • src/core/assistant-message/__tests__/presentAssistantMessage-unknown-tool.spec.ts#L130-L130: apply the same parsed-category assertion for the missing-id legacy tool call.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/assistant-message/__tests__/presentAssistantMessage-images.spec.ts`
at line 182, Replace the shared substring checks with JSON parsing and assert
category === "INVALID_TOOL_PROTOCOL" in presentAssistantMessage-images.spec.ts
at lines 182-182 and 286-286, and presentAssistantMessage-unknown-tool.spec.ts
at line 130; optionally also assert pattern_id, while preserving each test’s
existing scenario and ensuring classification cannot degrade to UNCLASSIFIED.

Source: Path instructions

src/core/tools/error-interception/ErrorClassifier.ts (1)

118-125: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Resolve the catch-all by category rather than by array position.

ERROR_PATTERNS[length - 1] silently changes meaning if a pattern is ever appended after EI/UNCLASSIFIED/001, and it types as possibly-undefined under noUncheckedIndexedAccess.

♻️ Proposed refactor
-	const fallback = ERROR_PATTERNS[ERROR_PATTERNS.length - 1]
+	const fallback = ERROR_PATTERNS.find((pattern) => pattern.category === "UNCLASSIFIED")
+	if (!fallback) {
+		throw new Error("errorPatterns: missing UNCLASSIFIED catch-all pattern")
+	}
 	return {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/tools/error-interception/ErrorClassifier.ts` around lines 118 - 125,
Update the fallback selection in ErrorClassifier to locate the catch-all pattern
by its stable category or identifier, rather than relying on ERROR_PATTERNS
array position. Ensure the lookup is narrowed or validated so it cannot produce
an undefined fallback under noUncheckedIndexedAccess, while preserving the
existing fallback response fields and heuristic behavior.
src/core/tools/error-interception/__tests__/ErrorClassifier.spec.ts (1)

96-178: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the three new pattern variants.

EI/PARAM_TYPE_MISMATCH/002 (CWD_OBJECT_MISUSE), EI/PARAM_TYPE_MISMATCH/003 (NESTED_PARAM_OVERFLOW), and EI/INVALID_TOOL_PROTOCOL/002 (XML_NATIVE_DUAL_PROTOCOL) are the PR's headline classifications but have no direct assertions — including the priority ordering that must keep 002/003 ahead of the generic EI/PARAM_TYPE_MISMATCH/001, and the errorMessageIncludes text fallbacks that couple to StructuralValidator wording.

As per path instructions, "Use package-local unit tests for pure logic, parsing, state transitions, validation, serialization, request construction, retry decisions, and error handling".

💚 Suggested tests
+		it("classifies CWD_OBJECT_MISUSE ahead of the generic type mismatch", () => {
+			const signal = baseSignal({
+				source: "validation",
+				stage: "preflight",
+				toolName: "execute_command",
+				metadata: { variant: "CWD_OBJECT_MISUSE", typeMismatch: true },
+			})
+			const result = classifyError(signal)
+			expect(result.patternId).toBe("EI/PARAM_TYPE_MISMATCH/002")
+		})
+
+		it("classifies NESTED_PARAM_OVERFLOW from the error message fallback", () => {
+			const signal = baseSignal({
+				source: "validation",
+				stage: "preflight",
+				toolName: "read_file",
+				error: { message: "Detected nested tool input object in parameter" },
+				metadata: {},
+			})
+			const result = classifyError(signal)
+			expect(result.patternId).toBe("EI/PARAM_TYPE_MISMATCH/003")
+		})
+
+		it("classifies XML markup alongside a native call as XML_NATIVE_DUAL_PROTOCOL", () => {
+			const signal = baseSignal({
+				source: "parser",
+				stage: "parse",
+				metadata: { xmlNativeDualProtocol: true, xmlToolCall: true },
+			})
+			const result = classifyError(signal)
+			expect(result.patternId).toBe("EI/INVALID_TOOL_PROTOCOL/002")
+		})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/tools/error-interception/__tests__/ErrorClassifier.spec.ts` around
lines 96 - 178, Extend the classifyError tests around baseSignal and
classifyError with direct coverage for CWD_OBJECT_MISUSE
(EI/PARAM_TYPE_MISMATCH/002), NESTED_PARAM_OVERFLOW (003), and
XML_NATIVE_DUAL_PROTOCOL (EI/INVALID_TOOL_PROTOCOL/002). Assert their categories
and pattern IDs, including cases where errorMessageIncludes text triggers each
classification. Add precedence cases proving 002 and 003 win over generic
PARAM_TYPE_MISMATCH/001, and verify the StructuralValidator wording fallbacks.

Source: Path instructions

src/core/tools/error-interception/types.ts (1)

60-71: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Rename the local ToolResponse to avoid shadowing the existing public tool contract.

The module re-exports this local type from index.ts, so importing ToolResponse here produces a structurally incompatible type compared with the shared contract (string | Array<Anthropic.TextBlockParam | Anthropic.ImageBlockParam>), regardless of whether it is currently consumed outside this module.

♻️ Suggested rename
-export interface ToolResponse {
+export interface StructuredToolResult {
 	type?: string
 	status?: string
 	error?: unknown
 	text?: string
 	toolUseId?: string
 	[key: string]: unknown
 }

Update InterceptionSignal.result?: ToolResponse, the errorPatterns.ts / ErrorClassifier.ts references, and src/core/tools/error-interception/index.ts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/tools/error-interception/types.ts` around lines 60 - 71, Rename the
local ToolResponse interface to a distinct interception-specific type, then
update InterceptionSignal.result, all references in errorPatterns.ts and
ErrorClassifier.ts, and the re-export in error-interception/index.ts to use the
new name. Keep the shared public ToolResponse contract unshadowed and unchanged.
src/core/tools/error-interception/__tests__/StructuralValidator.spec.ts (2)

150-168: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

These bound tests pass even with the bounds removed.

The deep fixture bottoms out at { leaf: 1 } and the wide fixture uses { child: i } — neither is a tool signature, so detection would return null regardless of NESTED_DETECTION_MAX_DEPTH / NESTED_DETECTION_MAX_NODES. Plant a detectable signature beyond the bound so the assertion actually pins the limit.

💚 Make the fixtures detectable-but-out-of-bounds
 	it("bounds recursion to NESTED_DETECTION_MAX_DEPTH", () => {
-		let deep: Record<string, unknown> = { leaf: 1 }
+		// Tool-shaped leaf placed deeper than the bound: must NOT be detected.
+		let deep: Record<string, unknown> = { command: "x" }
 		for (let i = 0; i < NESTED_DETECTION_MAX_DEPTH + 3; i += 1) {
 			deep = { wrap: deep }
 		}
@@
 	it("bounds total visited nodes to NESTED_DETECTION_MAX_NODES", () => {
 		const wide: Record<string, unknown> = {}
 		for (let i = 0; i < NESTED_DETECTION_MAX_NODES + 10; i += 1) {
-			wide[`k${i}`] = { child: i }
+			wide[`k${i}`] = { child: i }
 		}
+		// Tool-shaped node beyond the visit budget: must NOT be detected.
+		wide[`k${NESTED_DETECTION_MAX_NODES + 10}`] = { command: "x" }
 		expect(NESTED_DETECTION_MAX_NODES).toBeGreaterThan(0)

As per coding guidelines, "Use package-local unit tests for pure logic, parsing, state transitions, validation, serialization, request construction, retry decisions, and error handling."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/tools/error-interception/__tests__/StructuralValidator.spec.ts`
around lines 150 - 168, Update the deep and wide fixtures in the tests named
“bounds recursion to NESTED_DETECTION_MAX_DEPTH” and “bounds total visited nodes
to NESTED_DETECTION_MAX_NODES” so each contains a recognizable tool-signature
pattern only beyond its respective depth or node limit. Keep the existing
validateNestedParams assertions expecting null, ensuring the tests would fail if
either bound were removed.

Source: Coding guidelines


101-104: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

This case exercises the explicit signature set, not the multi-known-keys heuristic.

{ path, regex } already matches TOOL_SIGNATURE_KEY_SETS. Use two keys that only appear in KNOWN_PARAMETER_KEYS (e.g. { mode: "x", message: "y" }) and assert structuralReason is nested-tool-input:multi-known-keys.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/tools/error-interception/__tests__/StructuralValidator.spec.ts`
around lines 101 - 104, Update the test case around validateNestedParams to use
two keys that are only in KNOWN_PARAMETER_KEYS, such as mode and message, rather
than the explicit signature pair path and regex. Assert that the returned
signal’s structuralReason equals nested-tool-input:multi-known-keys.
src/core/tools/error-interception/MessageTransformer.ts (1)

10-27: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Hand-rolled UTF-8 counting duplicates the TextEncoder helper below and mis-sizes unpaired surrogates.

Line 18 assumes every high surrogate is followed by a low surrogate, counting 4 bytes and skipping the next code unit; for an unpaired high surrogate followed by a multi-byte char the count is short ("\ud800€" → 4 vs 6 actual), which undermines the "byte length <= byteLimit" guarantee documented at Line 155. The module already has a correct implementation in getPayloadByteLength (Line 172).

♻️ Use the encoder for both call sites
-function countUtf8Bytes(text: string): number {
-	let count = 0
-	for (let i = 0; i < text.length; i++) {
-		const code = text.charCodeAt(i)
-		if (code <= 0x7f) {
-			count += 1
-		} else if (code <= 0x7ff) {
-			count += 2
-		} else if (code >= 0xd800 && code <= 0xdbff) {
-			// Surrogate pair: count 4 bytes for the pair.
-			count += 4
-			i++
-		} else {
-			count += 3
-		}
-	}
-	return count
-}
+const utf8Encoder = new TextEncoder()
+
+function countUtf8Bytes(text: string): number {
+	return utf8Encoder.encode(text).length
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/tools/error-interception/MessageTransformer.ts` around lines 10 -
27, Replace the hand-rolled countUtf8Bytes implementation with the module’s
existing TextEncoder-based getPayloadByteLength logic, and update both call
sites to reuse that implementation. Remove the surrogate-specific counting so
unpaired surrogates are encoded correctly and the documented byteLimit guarantee
is preserved.
src/core/tools/error-interception/__tests__/ToolErrorInterceptor.spec.ts (1)

56-77: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the push/handle ordering this test claims, and cover array error content.

The implementation pushes the guided payload before awaiting rawHandleError (ToolErrorInterceptor.ts Lines 153-173), so the title is inverted and the ordering — which the callers' exactly-once guard depends on — is unverified. mock.invocationCallOrder pins it. Also worth adding a case where the error result is [text, image], since that path currently forwards only the transformed string.

💚 Suggested assertion
-		it("pushes a transformed result after the raw error", async () => {
+		it("pushes a transformed result before forwarding the raw error", async () => {
@@
 			expect(pushToolResult).toHaveBeenCalledTimes(1)
+			expect(pushToolResult.mock.invocationCallOrder[0]).toBeLessThan(handleError.mock.invocationCallOrder[0])
 			const result = JSON.parse((pushToolResult.mock.calls[0] as [string])[0])

As per coding guidelines, "Use package-local unit tests for pure logic, parsing, state transitions, validation, serialization, request construction, retry decisions, and error handling."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/tools/error-interception/__tests__/ToolErrorInterceptor.spec.ts`
around lines 56 - 77, Update the test around createInterceptor and
decoratedHandleError to assert the claimed ordering with
mock.invocationCallOrder, verifying raw handleError completes before
pushToolResult is invoked; rename the test if needed to reflect that order. Add
a focused case covering an error result containing [text, image] and assert the
pushed payload preserves both elements rather than only the transformed string.

Source: Coding guidelines

src/core/tools/error-interception/ToolErrorInterceptor.ts (2)

63-78: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Type the circuit-open literal as GuidancePayload so it can't drift from the contract.

The literal duplicates GUIDANCE_VERSION and isn't checked against GuidancePayload (types.ts:110-121); it also reports occurrence: 1 while the real count is available at Lines 258-262, which hides how many shell failures actually occurred.

♻️ Proposed change
-/** Circuit-open payload used when the shell integration breaker trips. */
-const CIRCUIT_OPEN_MESSAGE = {
-	version: 1,
+/** Circuit-open payload used when the shell integration breaker trips. */
+const CIRCUIT_OPEN_MESSAGE: GuidancePayload = {
+	version: GUIDANCE_VERSION,
 	status: "error",

Add the imports:

-import type { ErrorCategory, ErrorClassification, ErrorSource, ErrorStage, InterceptionSignal } from "./types"
+import { GUIDANCE_VERSION } from "./errorPatterns"
+import type {
+	ErrorCategory,
+	ErrorClassification,
+	ErrorSource,
+	ErrorStage,
+	GuidancePayload,
+	InterceptionSignal,
+} from "./types"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/tools/error-interception/ToolErrorInterceptor.ts` around lines 63 -
78, Type CIRCUIT_OPEN_MESSAGE as GuidancePayload and reuse GUIDANCE_VERSION
instead of duplicating the version literal. Update its occurrence field at the
construction or emission site to use the actual shell-failure count available in
the circuit-breaker flow around lines 258–262, rather than the fixed value 1.

153-174: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Make the exactly-once contract part of the interceptor API.

decoratedHandleError emits one rawPushToolResult(transformed(...)), then delegates to rawHandleError, whose current callers also call rawPushToolResult(formatResponse.toolError(...)). Existing call sites avoid double tool_result by managing their own hasToolResult flag elsewhere, but that contract is only local; new callers or guard removals can emit the UI/raw error payload twice. Explicit this requirement in DecoratedCallbacks/createInterceptor, change the wrapper to swallow the raw push after a guided payload, or move deduplication into the interceptor layer.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/tools/error-interception/ToolErrorInterceptor.ts` around lines 153 -
174, Make the exactly-once tool-result contract explicit in
DecoratedCallbacks/createInterceptor and enforce it in decoratedHandleError.
When transformSignal produces a guided payload and rawPushToolResult is called,
prevent the subsequent rawHandleError path from emitting another tool result;
preserve the raw error emission when no transformed payload exists. Update
callback wiring or deduplication at the interceptor layer so callers no longer
need separate hasToolResult guards.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/core/assistant-message/__tests__/presentAssistantMessage-unknown-tool.spec.ts`:
- Around line 96-97: Update the unknown-tool error path exercised by
presentAssistantMessage so the guided payload preserves the offending native
tool name and the instruction pointing to available tools. Add these facts to
the emitted signal or prefix the guided payload, while retaining the existing
guided_tool_error structure and metadata.

In `@src/core/assistant-message/presentAssistantMessage.ts`:
- Line 362: Use one module-scoped ToolErrorInterceptor in
presentAssistantMessage.ts so its perTask state persists across content blocks.
Replace the per-block createToolErrorInterceptor() at lines 362-362 and reuse
the same instance at lines 117-118 for the mcp_tool_use case, allowing both tool
paths to share task counters and circuit state.
- Around line 341-354: Move pendingNativeProtocolGuide from the undeclared cline
property onto the task-scoped state returned by getTaskErrorState(cline), adding
a declared Task/state field. Consume and clear the guide whenever this turn
emits a tool_result, including direct pushToolResultToUserContent paths and the
no-tool early-break path, while preserving the existing rawPushToolResult
behavior so it cannot leak into a later turn.
- Around line 679-684: In the structural signal construction within
presentAssistantMessage, invoke validateCwdParameter only when block.name
identifies the execute_command tool; continue running validateNestedParams for
all tools and preserve the existing filtering and error handling.
- Around line 770-795: Replace the validation-message substring checks in the
preflight error handling around validateToolUse with a typed error or stable
error code exposed by validateToolUse, mapping that code to validationMetadata
without relying on wording. When constructing the tool_result in
presentAssistantMessage, preserve the original errorMessage alongside any
interceptor-guided payload so the model retains the specific tool and
restriction details.
- Around line 689-701: The PARAM_TYPE_MISMATCH path in presentAssistantMessage
must reset its persisted error state when the structural fingerprint changes, so
different tools or parameters receive fresh guidance instead of inheriting an
open circuit. Update the logic around taskErrorState.setFingerprint,
incrementOccurrence, and isOpen to compare the prior fingerprint and reset the
PARAM_TYPE_MISMATCH state before incrementing when it differs; preserve existing
same-fingerprint circuit behavior.

In `@src/core/tools/error-interception/__tests__/MessageTransformer.spec.ts`:
- Around line 143-153: Update the test around transformErrorToMessage to provide
non-ASCII content through the classification/template input and set a byteLimit
that forces fitPayloadWithinByteLimit to truncate it. Assert the resulting
message remains valid JSON and preserves correct handling of surrogate pairs
after truncation, rather than relying on the ASCII EI/PARAM_MISSING/001 template
without a byte limit.

In `@src/core/tools/error-interception/ErrorClassifier.ts`:
- Around line 87-115: Enforce pattern.requiresToolContext in both matching
passes of classifyError: skip any pattern requiring tool context when the signal
lacks toolName and toolCallId. Preserve exact matching, heuristic fallback, and
UNCLASSIFIED exclusions for eligible patterns, and ensure bare signals cannot
select tool-bound classifications.

In `@src/core/tools/error-interception/errorPatterns.ts`:
- Around line 163-179: Update the invalid-argument pattern in errorPatterns.ts
to match JSON-RPC code -32602 as a number using the appropriate typed code
matcher instead of errorCodeIs. In ErrorClassifier.spec.ts, change the
corresponding fixture error to use numeric code -32602 with message "Invalid
params"; preserve the existing classification expectations.

In `@src/core/tools/error-interception/MessageTransformer.ts`:
- Around line 10-27: Update countUtf8Bytes to use the module’s shared
TextEncoder-based byte-counting helper instead of the manual UTF-8 loop. In the
surrogate-cleaning helper at
src/core/tools/error-interception/MessageTransformer.ts lines 29-42, remove only
orphaned high and low surrogates, preserving valid surrogate pairs during
clamping.
- Around line 137-149: The minimal fallback in MessageTransformer must still
honor the caller-supplied byteLimit. Update the fallback path that builds the
minimal GuidancePayload and calls serializePayload to ensure its serialized
result is within byteLimit, clamping degradable fields as needed while
preserving category and pattern_id; alternatively, explicitly document and
enforce a minimum supported byteLimit.

In `@src/core/tools/error-interception/StructuralValidator.ts`:
- Around line 35-82: The global KNOWN_PARAMETER_KEYS two-key heuristic
over-classifies legitimate nested schema objects as tool invocations. Update
validateNestedParams to validate nested objects only against the current tool’s
declared schema and explicitly allowed object parameters, removing or narrowing
the shared-key scan while preserving genuine nested invocation detection.
- Around line 194-237: Update visitNested so state.seen tracks only objects on
the current recursion path: add a value when entering it and remove it before
every return after its children are processed. Preserve cycleDetected for
back-edges while allowing shared non-cyclic references in sibling branches to be
traversed without reporting a cycle.

In `@src/core/tools/error-interception/ToolErrorInterceptor.ts`:
- Around line 207-224: Update the array-result handling around transformSignal
so transformed error results retain all non-text content blocks, including
images, alongside the guided text. Build the transformed response from the
existing text and the original content items rather than forwarding only the
string, while preserving the current fail-open behavior and metadata handling.

---

Nitpick comments:
In `@src/core/assistant-message/__tests__/presentAssistantMessage-images.spec.ts`:
- Line 182: Replace the shared substring checks with JSON parsing and assert
category === "INVALID_TOOL_PROTOCOL" in presentAssistantMessage-images.spec.ts
at lines 182-182 and 286-286, and presentAssistantMessage-unknown-tool.spec.ts
at line 130; optionally also assert pattern_id, while preserving each test’s
existing scenario and ensuring classification cannot degrade to UNCLASSIFIED.

In `@src/core/tools/error-interception/__tests__/ErrorClassifier.spec.ts`:
- Around line 96-178: Extend the classifyError tests around baseSignal and
classifyError with direct coverage for CWD_OBJECT_MISUSE
(EI/PARAM_TYPE_MISMATCH/002), NESTED_PARAM_OVERFLOW (003), and
XML_NATIVE_DUAL_PROTOCOL (EI/INVALID_TOOL_PROTOCOL/002). Assert their categories
and pattern IDs, including cases where errorMessageIncludes text triggers each
classification. Add precedence cases proving 002 and 003 win over generic
PARAM_TYPE_MISMATCH/001, and verify the StructuralValidator wording fallbacks.

In `@src/core/tools/error-interception/__tests__/StructuralValidator.spec.ts`:
- Around line 150-168: Update the deep and wide fixtures in the tests named
“bounds recursion to NESTED_DETECTION_MAX_DEPTH” and “bounds total visited nodes
to NESTED_DETECTION_MAX_NODES” so each contains a recognizable tool-signature
pattern only beyond its respective depth or node limit. Keep the existing
validateNestedParams assertions expecting null, ensuring the tests would fail if
either bound were removed.
- Around line 101-104: Update the test case around validateNestedParams to use
two keys that are only in KNOWN_PARAMETER_KEYS, such as mode and message, rather
than the explicit signature pair path and regex. Assert that the returned
signal’s structuralReason equals nested-tool-input:multi-known-keys.

In `@src/core/tools/error-interception/__tests__/ToolErrorInterceptor.spec.ts`:
- Around line 56-77: Update the test around createInterceptor and
decoratedHandleError to assert the claimed ordering with
mock.invocationCallOrder, verifying raw handleError completes before
pushToolResult is invoked; rename the test if needed to reflect that order. Add
a focused case covering an error result containing [text, image] and assert the
pushed payload preserves both elements rather than only the transformed string.

In `@src/core/tools/error-interception/ErrorClassifier.ts`:
- Around line 118-125: Update the fallback selection in ErrorClassifier to
locate the catch-all pattern by its stable category or identifier, rather than
relying on ERROR_PATTERNS array position. Ensure the lookup is narrowed or
validated so it cannot produce an undefined fallback under
noUncheckedIndexedAccess, while preserving the existing fallback response fields
and heuristic behavior.

In `@src/core/tools/error-interception/MessageTransformer.ts`:
- Around line 10-27: Replace the hand-rolled countUtf8Bytes implementation with
the module’s existing TextEncoder-based getPayloadByteLength logic, and update
both call sites to reuse that implementation. Remove the surrogate-specific
counting so unpaired surrogates are encoded correctly and the documented
byteLimit guarantee is preserved.

In `@src/core/tools/error-interception/ToolErrorInterceptor.ts`:
- Around line 63-78: Type CIRCUIT_OPEN_MESSAGE as GuidancePayload and reuse
GUIDANCE_VERSION instead of duplicating the version literal. Update its
occurrence field at the construction or emission site to use the actual
shell-failure count available in the circuit-breaker flow around lines 258–262,
rather than the fixed value 1.
- Around line 153-174: Make the exactly-once tool-result contract explicit in
DecoratedCallbacks/createInterceptor and enforce it in decoratedHandleError.
When transformSignal produces a guided payload and rawPushToolResult is called,
prevent the subsequent rawHandleError path from emitting another tool result;
preserve the raw error emission when no transformed payload exists. Update
callback wiring or deduplication at the interceptor layer so callers no longer
need separate hasToolResult guards.

In `@src/core/tools/error-interception/types.ts`:
- Around line 60-71: Rename the local ToolResponse interface to a distinct
interception-specific type, then update InterceptionSignal.result, all
references in errorPatterns.ts and ErrorClassifier.ts, and the re-export in
error-interception/index.ts to use the new name. Keep the shared public
ToolResponse contract unshadowed and unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 591abfc9-8f7f-474a-b10d-3e3c23ea80c8

📥 Commits

Reviewing files that changed from the base of the PR and between 2634d5c and 66e4d56.

📒 Files selected for processing (16)
  • src/core/assistant-message/__tests__/presentAssistantMessage-images.spec.ts
  • src/core/assistant-message/__tests__/presentAssistantMessage-unknown-tool.spec.ts
  • src/core/assistant-message/presentAssistantMessage.ts
  • src/core/tools/error-interception/ErrorClassifier.ts
  • src/core/tools/error-interception/MessageTransformer.ts
  • src/core/tools/error-interception/StructuralValidator.ts
  • src/core/tools/error-interception/TaskErrorState.ts
  • src/core/tools/error-interception/ToolErrorInterceptor.ts
  • src/core/tools/error-interception/__tests__/ErrorClassifier.spec.ts
  • src/core/tools/error-interception/__tests__/MessageTransformer.spec.ts
  • src/core/tools/error-interception/__tests__/StructuralValidator.spec.ts
  • src/core/tools/error-interception/__tests__/TaskErrorState.spec.ts
  • src/core/tools/error-interception/__tests__/ToolErrorInterceptor.spec.ts
  • src/core/tools/error-interception/errorPatterns.ts
  • src/core/tools/error-interception/index.ts
  • src/core/tools/error-interception/types.ts

Comment thread src/core/assistant-message/presentAssistantMessage.ts
Comment thread src/core/assistant-message/presentAssistantMessage.ts Outdated
Comment thread src/core/assistant-message/presentAssistantMessage.ts
Comment thread src/core/assistant-message/presentAssistantMessage.ts
Comment thread src/core/tools/error-interception/MessageTransformer.ts
Comment on lines +137 to +149
const minimal: GuidancePayload = {
version: GUIDANCE_VERSION,
status: "error",
type,
category: payload.category,
what: "Error.",
why: "Error.",
next: [],
retryable: payload.retryable,
occurrence: payload.occurrence,
pattern_id: payload.pattern_id,
}
return serializePayload(minimal)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Minimal fallback is returned without re-checking byteLimit.

byteLimit is caller-supplied (TransformOptions.byteLimit), and category + pattern_id are not degradable, so for small limits this return value can exceed the bound the docblock at Line 155 guarantees. Either clamp the remaining fields or document that byteLimit has a floor.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/tools/error-interception/MessageTransformer.ts` around lines 137 -
149, The minimal fallback in MessageTransformer must still honor the
caller-supplied byteLimit. Update the fallback path that builds the minimal
GuidancePayload and calls serializePayload to ensure its serialized result is
within byteLimit, clamping degradable fields as needed while preserving category
and pattern_id; alternatively, explicitly document and enforce a minimum
supported byteLimit.

Comment on lines +35 to +82
const OBJECT_ALLOWED_PARAMETERS: Readonly<Record<string, ReadonlySet<string>>> = {
read_file: new Set(["indentation"]),
use_mcp_tool: new Set(["arguments"]),
}

/**
* Known tool-shaped signatures. A nested object is treated as a tool input
* only when it contains at least one of these key sets. Matching requires
* all listed keys to be present in the same object.
*/
const TOOL_SIGNATURE_KEY_SETS: ReadonlyArray<ReadonlyArray<string>> = [
["command"],
["path", "regex"],
["query", "path"],
["server_name", "tool_name"],
["path", "content"],
["pattern", "file_pattern"],
]

/**
* Recognized parameter keys used for the "multiple known keys from a
* different invocation" heuristic. Two or more of these keys appearing
* together inside a nested object is treated as a tool input signature.
*/
const KNOWN_PARAMETER_KEYS: ReadonlySet<string> = new Set([
"command",
"cwd",
"path",
"regex",
"file_pattern",
"query",
"content",
"diff",
"pattern",
"server_name",
"tool_name",
"arguments",
"uri",
"line_number",
"offset",
"limit",
"mode",
"prompt",
"slug",
"name",
"message",
"todos",
])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect native argument schemas to see which tools legitimately nest objects/arrays of objects.
fd -t f -e ts . packages/types/src | head -30
rg -nP 'apply_diff|write_to_file|read_file|update_todo_list' -g 'packages/types/**/*.ts' -C6 | head -160
# Native arg shapes consumed by the tools themselves
rg -nP '\bnativeArgs\b' -g '!**/__tests__/**' -C4 src/core/tools | head -160

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 1453


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== structural validator outline =="
ast-grep outline src/core/tools/error-interception/StructuralValidator.ts --view expanded || true

echo "== structural validator relevant lines =="
sed -n '1,240p' src/core/tools/error-interception/StructuralValidator.ts | nl -ba

echo "== presentAssistantMessage relevant lines =="
rg -n "consecutiveMistakeCount|tool_result|StructuralValidator" src/core -C 5 | head -220

echo "== tool schemas mentioning apply_diff/write_to_file/update_todo_list =="
rg -n "apply_diff|write_to_file|update_todo_list|read_file|use_mcp_tool|command.*path|path.*content|diff|content" packages/types/src -C 8 | head -260

echo "== files mentioning StructuralValidator =="
rg -n "StructuralValidator|interceptError|interceptTool" src/core packages/types -C 3 | head -260

echo "== package tool files =="
fd -t f . src/core/tools | sort | sed -n '1,200p'

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 1329


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== structural validator relevant lines =="
sed -n '1,260p' src/core/tools/error-interception/StructuralValidator.ts | cat -n

echo "== presentAssistantMessage / tool_result / StructuralValidator usages =="
rg -n "consecutiveMistakeCount|tool_result|StructuralValidator|validateNestedParams|validateCwdParameter|InterceptionSignal" src/core packages/types -C 5 | head -260

echo "== tool schemas mentioning native argument keys =="
rg -n "apply_diff|write_to_file|update_todo_list|read_file|use_mcp_tool|command.*path|path.*content|arguments|diff|content|todos" packages/types/src src/core -C 8 | head -340

echo "== files mentioning StructuralValidator =="
rg -n "StructuralValidator|interceptError|interceptTool" src/core packages/types -C 3 | head -260

echo "== repository TypeScript files listing (sample) =="
git ls-files '**/*.ts' | sed -n '1,200p'

echo "== candidate tool schema files =="
git ls-files | grep -E '(packages/types/src|src/core/tools).*(schema|tool|argument|typescript).*ts$' | sed -n '1,200p'

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== structural validator signals =="
sed -n '239,295p' src/core/tools/error-interception/StructuralValidator.ts | cat -n

echo "== exact validator imports/usages =="
rg -n "validateNestedParams|validateCwdParameter|INTERCEPT.*SIG|interceptTool|interceptError|StructuralValidator" src/core packages/types --glob '*.ts' --glob '!**/node_modules/**' --glob '!**/__tests__/**' | head -220

echo "== call sites with context =="
rg -n "validateNestedParams|validateCwdParameter" src/core packages/types --glob '*.ts' --glob '!**/node_modules/**' --glob '!**/__tests__/**' -C 20 | head -260

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 21032


Scope nested-param validation to schema-approved object values

validateNestedParams runs for every native tool and treats any nested object containing two KNOWN_PARAMETER_KEYS as a nested tool invocation, then blocks execution with tool_result. This breaks legitimate schemas such as edit.apply_diff.entry, write_to_file.diff, update_todo_list, and use_mcp_tool.arguments; it also makes read_file.indentation pass only by the narrow current allowlist. Remove or narrow the global two-key heuristic, or validate nested object parameters against each tool’s declared schema instead of scanning a shared key vocabulary.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/tools/error-interception/StructuralValidator.ts` around lines 35 -
82, The global KNOWN_PARAMETER_KEYS two-key heuristic over-classifies legitimate
nested schema objects as tool invocations. Update validateNestedParams to
validate nested objects only against the current tool’s declared schema and
explicitly allowed object parameters, removing or narrowing the shared-key scan
while preserving genuine nested invocation detection.

Comment on lines +194 to +237
function visitNested(
value: unknown,
topParameter: string,
depth: number,
state: { visited: number; seen: Set<unknown> },
): NestedSearchResult {
if (value === null || typeof value !== "object") {
return { found: false }
}
if (state.seen.has(value)) {
return { found: false, cycleDetected: true }
}
state.seen.add(value)
state.visited += 1
if (state.visited > NESTED_DETECTION_MAX_NODES) {
return { found: false, nodeLimitExceeded: true }
}
if (depth > NESTED_DETECTION_MAX_DEPTH) {
return { found: false, depthExceeded: true }
}

if (Array.isArray(value)) {
for (const item of value) {
const nested = visitNested(item, topParameter, depth + 1, state)
if (nested.found || nested.cycleDetected || nested.depthExceeded || nested.nodeLimitExceeded) {
return nested
}
}
return { found: false }
}

const record = value as Record<string, unknown>
const signature = detectToolSignature(record)
if (signature !== undefined) {
return { found: true, parameter: topParameter, signature }
}
for (const child of Object.values(record)) {
const nested = visitNested(child, topParameter, depth + 1, state)
if (nested.found || nested.cycleDetected || nested.depthExceeded || nested.nodeLimitExceeded) {
return nested
}
}
return { found: false }
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

seen is a global visited set, so shared (non-cyclic) references are reported as cycles.

Entries are added at Line 206 and never removed, so a DAG where two sibling keys reference the same object yields cycleDetected — which the caller turns into a blocking NESTED_PARAM_OVERFLOW result (presentAssistantMessage.ts Lines 719-726). Scope the set to the current traversal path.

🐛 Path-scoped cycle detection
 	if (Array.isArray(value)) {
 		for (const item of value) {
 			const nested = visitNested(item, topParameter, depth + 1, state)
 			if (nested.found || nested.cycleDetected || nested.depthExceeded || nested.nodeLimitExceeded) {
 				return nested
 			}
 		}
+		state.seen.delete(value)
 		return { found: false }
 	}
@@
 	for (const child of Object.values(record)) {
 		const nested = visitNested(child, topParameter, depth + 1, state)
 		if (nested.found || nested.cycleDetected || nested.depthExceeded || nested.nodeLimitExceeded) {
 			return nested
 		}
 	}
+	state.seen.delete(value)
 	return { found: false }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function visitNested(
value: unknown,
topParameter: string,
depth: number,
state: { visited: number; seen: Set<unknown> },
): NestedSearchResult {
if (value === null || typeof value !== "object") {
return { found: false }
}
if (state.seen.has(value)) {
return { found: false, cycleDetected: true }
}
state.seen.add(value)
state.visited += 1
if (state.visited > NESTED_DETECTION_MAX_NODES) {
return { found: false, nodeLimitExceeded: true }
}
if (depth > NESTED_DETECTION_MAX_DEPTH) {
return { found: false, depthExceeded: true }
}
if (Array.isArray(value)) {
for (const item of value) {
const nested = visitNested(item, topParameter, depth + 1, state)
if (nested.found || nested.cycleDetected || nested.depthExceeded || nested.nodeLimitExceeded) {
return nested
}
}
return { found: false }
}
const record = value as Record<string, unknown>
const signature = detectToolSignature(record)
if (signature !== undefined) {
return { found: true, parameter: topParameter, signature }
}
for (const child of Object.values(record)) {
const nested = visitNested(child, topParameter, depth + 1, state)
if (nested.found || nested.cycleDetected || nested.depthExceeded || nested.nodeLimitExceeded) {
return nested
}
}
return { found: false }
}
function visitNested(
value: unknown,
topParameter: string,
depth: number,
state: { visited: number; seen: Set<unknown> },
): NestedSearchResult {
if (value === null || typeof value !== "object") {
return { found: false }
}
if (state.seen.has(value)) {
return { found: false, cycleDetected: true }
}
state.seen.add(value)
state.visited += 1
if (state.visited > NESTED_DETECTION_MAX_NODES) {
return { found: false, nodeLimitExceeded: true }
}
if (depth > NESTED_DETECTION_MAX_DEPTH) {
return { found: false, depthExceeded: true }
}
if (Array.isArray(value)) {
for (const item of value) {
const nested = visitNested(item, topParameter, depth + 1, state)
if (nested.found || nested.cycleDetected || nested.depthExceeded || nested.nodeLimitExceeded) {
return nested
}
}
state.seen.delete(value)
return { found: false }
}
const record = value as Record<string, unknown>
const signature = detectToolSignature(record)
if (signature !== undefined) {
return { found: true, parameter: topParameter, signature }
}
for (const child of Object.values(record)) {
const nested = visitNested(child, topParameter, depth + 1, state)
if (nested.found || nested.cycleDetected || nested.depthExceeded || nested.nodeLimitExceeded) {
return nested
}
}
state.seen.delete(value)
return { found: false }
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/tools/error-interception/StructuralValidator.ts` around lines 194 -
237, Update visitNested so state.seen tracks only objects on the current
recursion path: add a value when entering it and remove it before every return
after its children are processed. Preserve cycleDetected for back-edges while
allowing shared non-cyclic references in sibling branches to be traversed
without reporting a cycle.

Comment thread src/core/tools/error-interception/ToolErrorInterceptor.ts
k1yt added 2 commits July 25, 2026 03:57
Apply all 11 CodeRabbit review findings from PR Zoo-Code-Org#1009:

MAJOR fixes:
- Use module-scoped interceptor singleton instead of per-block creation
  so per-task WeakMap counters and circuit breakers persist across blocks
- Move pendingNativeProtocolGuide from undeclared cline property onto
  TaskErrorState with get/set/clear; consume in every tool_result path
- Reset PARAM_TYPE_MISMATCH state when structural fingerprint changes
  to prevent stale circuit state from affecting different tools
- Enforce requiresToolContext in ErrorClassifier both matching passes;
  skip tool-bound patterns when signal lacks toolName/toolCallId

MINOR fixes:
- Gate validateCwdParameter to execute_command tool only
- Preserve original error message alongside guided payload in validation
- Path-scoped cycle detection in StructuralValidator (delete after children)
- Preserve non-text blocks (images) in array result transformation
- Match JSON-RPC -32602 as both string and number
- Use TextEncoder for UTF-8 byte counting in MessageTransformer
- Update non-ASCII test to exercise multibyte truncation with byteLimit
Fix e2e apply_diff fixture to expect guided JSON payload format instead
of raw error text strings. The interceptor now transforms apply_diff
errors into structured DIFF_MATCH_FAILED guidance.

Add 16 new tests:
- 8 integration tests for presentAssistantMessage error-interception
  paths (XML detection, structural preflight, guide consumption)
- 8 edge case tests for ToolErrorInterceptor (array results with images,
  isErrorResult/inferStatus branches)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/core/tools/error-interception/__tests__/ToolErrorInterceptor.spec.ts (1)

623-644: 📐 Maintainability & Code Quality | 🔵 Trivial

Strengthen assertion in the file-not-found inferStatus test.

This test only checks toHaveBeenCalledTimes(1) — that's true whether the content was transformed or passed through unchanged, so it doesn't actually verify that inferStatus/classification detected "File does not exist" the way the sibling test above (Lines 596-621) does.

♻️ Suggested stronger assertion
 			decoratedPushToolResult(content)
 
 			expect(pushToolResult).toHaveBeenCalledTimes(1)
+			const pushed = (pushToolResult.mock.calls[0] as unknown as [Array<Record<string, unknown>>])[0]
+			expect(String(pushed[0].text)).toContain("guided_tool_error")
+			expect(String(pushed[0].text)).toContain("FILE_NOT_FOUND")
 		})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/tools/error-interception/__tests__/ToolErrorInterceptor.spec.ts`
around lines 623 - 644, Strengthen the assertion in the “infers 'file-not-found'
status when text contains 'File does not exist'” test to verify the argument
passed to pushToolResult includes the expected file-not-found classification,
matching the sibling inferStatus test’s assertion pattern. Keep the existing
invocation-count check and test setup unchanged.
ci-fix-commit.ps1 (1)

1-16: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Personal dev-workflow artifacts committed to the repo; consider removing before merge. All three files are local git-automation helpers (hardcoded paths/remote/branch) that bypass commit hooks, with no functional relationship to the interception middleware feature itself.

  • ci-fix-commit.ps1#L1-L16: remove, or move out of version control (e.g. local-only script / .gitignored dotfile) since it hardcodes cd Zoo-Code, uses git commit --no-verify, and disables Husky via $env:HUSKY = "0".
  • commit-and-push.ps1#L1-L5: remove for the same reason; also note $env:HUSKY = "0" is set after the git commit call it's meant to affect, so it has no effect there.
  • commit-message.txt#L1-L23: remove alongside the scripts above, as it's only consumed by commit-and-push.ps1 -F commit-message.txt.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ci-fix-commit.ps1` around lines 1 - 16, Remove the personal git-automation
artifacts from version control: delete ci-fix-commit.ps1, commit-and-push.ps1,
and commit-message.txt. Do not replace them with repository scripts; keep these
local-only if needed and exclude them via gitignore.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@ci-fix-commit.ps1`:
- Around line 1-16: Remove the personal git-automation artifacts from version
control: delete ci-fix-commit.ps1, commit-and-push.ps1, and commit-message.txt.
Do not replace them with repository scripts; keep these local-only if needed and
exclude them via gitignore.

In `@src/core/tools/error-interception/__tests__/ToolErrorInterceptor.spec.ts`:
- Around line 623-644: Strengthen the assertion in the “infers 'file-not-found'
status when text contains 'File does not exist'” test to verify the argument
passed to pushToolResult includes the expected file-not-found classification,
matching the sibling inferStatus test’s assertion pattern. Keep the existing
invocation-count check and test setup unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ca838f27-d1e4-4679-ab8d-2e729b854bc6

📥 Commits

Reviewing files that changed from the base of the PR and between 27644cf and d8165ca.

📒 Files selected for processing (6)
  • apps/vscode-e2e/src/fixtures/apply-diff.ts
  • ci-fix-commit.ps1
  • commit-and-push.ps1
  • commit-message.txt
  • src/core/assistant-message/__tests__/presentAssistantMessage-error-interception.spec.ts
  • src/core/tools/error-interception/__tests__/ToolErrorInterceptor.spec.ts

@github-actions github-actions Bot added the awaiting-review PR changes are ready and waiting for maintainer re-review label Jul 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting-review PR changes are ready and waiting for maintainer re-review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Add Error Interception Middleware for guided AI self-correcting tool errors

2 participants