Skip to content

fix(ai): dedupe duplicate tool_results after compaction/batch (non-retryable 400)#1276

Merged
ocervell merged 1 commit into
ai-resiliencyfrom
fix/ai-dedupe-tool-results
Jul 5, 2026
Merged

fix(ai): dedupe duplicate tool_results after compaction/batch (non-retryable 400)#1276
ocervell merged 1 commit into
ai-resiliencyfrom
fix/ai-dedupe-tool-results

Conversation

@ocervell

@ocervell ocervell commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Bug (hit during CLI testing, after compaction)

[WRN] Chat history trimmed: dropped 10 messages to fit under 100000 tokens.
[ERR] LLM call failed with non-retryable 400: ... each tool_use must have a single result.
      Found multiple `tool_result` blocks with id: toolu_bdrk_01Bi8...

Providers fold consecutive tool messages into a single user turn and reject more than one tool_result per tool_use id — a non-retryable 400 that aborts the whole run.

Root cause

_dispatch_and_collect grouped batch results with itertools.groupby, which groups only consecutive equal keys. In batch mode (_run_batch) results interleave by tool_call_id (e.g. [X, Y, X]), so groupby produced several groups for one id → add_tool_result fired more than once for it → duplicate consecutive tool messages → litellm folds them into one user turn with duplicate tool_result blocks → 400. History trim/compaction restructures the window the same way (hence the correlation with compaction).

Fix (source + safety net)

  1. Order-preserving grouping — group collected by an ordered dict instead of groupby, so each tool_call_id yields exactly one result regardless of arrival order (also fixes token accounting for interleaved batches).
  2. _dedupe_tool_results in _repair_orphan_tool_uses — within each run of consecutive tool messages, keep the first result per id, drop the rest. Runs proactively before every LLM call and on the 400-repair-retry path, so the "multiple tool_result blocks" 400 is now repaired + retried instead of failing fast.

Tests

TestDedupeToolResults (drop consecutive dup, no-op when unique, per-run scoping, repair integration) + call_llm duplicate-400 → repaired-and-retried. Full AI suite: no new failures vs branch baseline.

Targets ai-resiliency (#1241).

🤖 Generated with Claude Code

…tryable 400)

Providers (Anthropic via OpenRouter) fold consecutive `tool` messages into one
user turn and reject >1 tool_result per tool_use id:
"each tool_use must have a single result. Found multiple tool_result blocks with
id X" — a NON-retryable 400 that aborted the run.

Root cause: _dispatch_and_collect grouped batch results with itertools.groupby,
which groups only *consecutive* keys. Batch results (_run_batch) interleave by
tool_call_id, so an interleaved id yielded several groups → several tool_result
messages for one tool_use. History trim/compaction can restructure the window
the same way. Fixes:
- Group by an order-preserving dict (not groupby) so each tool_call_id yields
  exactly one result regardless of arrival order.
- Add _dedupe_tool_results to the pre-LLM repair (_repair_orphan_tool_uses):
  within each run of consecutive tool messages keep the first result per id, drop
  the rest. This runs proactively before every call AND on the 400-repair-retry
  path — so the "multiple tool_result blocks" 400 is now repaired and retried
  instead of failing fast as non-retryable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNjPggRSVZ2xnLb7ZxWP5H
@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0165b4c9-4c21-4fb5-ad12-e62f654d072b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/ai-dedupe-tool-results

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.

@ocervell ocervell merged commit 9f0ddd4 into ai-resiliency Jul 5, 2026
1 check passed
@ocervell ocervell deleted the fix/ai-dedupe-tool-results branch July 5, 2026 12:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant