Skip to content

fix: deduplicate tool_use IDs and enable sanitization for Anthropic#4700

Closed
marcelomar21 wants to merge 4 commits intoopenclaw:mainfrom
marcelomar21:fix/deduplicate-tool-use-ids
Closed

fix: deduplicate tool_use IDs and enable sanitization for Anthropic#4700
marcelomar21 wants to merge 4 commits intoopenclaw:mainfrom
marcelomar21:fix/deduplicate-tool-use-ids

Conversation

@marcelomar21
Copy link

@marcelomar21 marcelomar21 commented Jan 30, 2026

Summary

Fixes Anthropic API rejection error: messages.X.content.Y: tool_use ids must be unique

This issue occurs when:

  1. Session transcripts accumulate multiple assistant messages with the same tool_use ID (e.g., from retries or long conversations)
  2. Tool IDs contain special characters (spaces, colons) that weren't being sanitized for Anthropic

Changes

  • Add deduplication logic in repairToolUseResultPairing() to detect and rename duplicate tool_use IDs in assistant messages (e.g., call_1call_1_2)
  • Update corresponding toolResult IDs to match the remapped tool_use IDs
  • Enable sanitizeToolCallIds for Anthropic provider (previously only Google/Mistral)
  • Add tests for deduplication scenarios

Test plan

  • Existing tests pass (session-transcript-repair.test.ts)
  • New tests added for duplicate ID scenarios
  • Verified fix resolves the error in production session with duplicate IDs

Greptile Overview

Greptile Summary

This PR strengthens transcript sanitization/repair for Anthropic-compatible requests by (1) enabling sanitizeToolCallIds for Anthropic in the transcript policy and (2) extending repairToolUseResultPairing() to deduplicate duplicate assistant tool_use IDs across a session and remap the corresponding toolResult IDs. Tests were updated/added to cover the new Anthropic sanitize flag and a basic duplicate-ID-across-messages scenario.

The changes fit into the existing transcript hygiene pipeline (sanitizeSessionHistory in src/agents/pi-embedded-runner/google.ts), where messages are first sanitized (including tool call IDs) and then repaired to ensure strict toolCall/toolResult ordering expected by providers like Anthropic.

Confidence Score: 3/5

  • This PR is close to safe to merge, but has a correctness edge case around duplicate IDs within a single assistant turn that could drop tool results.
  • The overall approach (deduplicate IDs + remap toolResult IDs, and enable sanitization for Anthropic) matches the reported provider constraint and is covered by new tests. However, the current remapping uses a Map keyed by the original ID, so duplicates within the same assistant message can collapse to one remapped ID and cause pushToolResult to treat later results as duplicates and drop them. That edge case could affect real transcripts if a retry duplicates blocks inside one message.
  • src/agents/session-transcript-repair.ts

(3/5) Reply to the agent's comments like "Can you suggest a fix for this @greptileai?" or ask follow-up questions!

Context used:

  • Context from dashboard - CLAUDE.md (source)
  • Context from dashboard - AGENTS.md (source)

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

1 file reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +156 to +158
// Check for duplicate tool_use IDs and remap them if necessary
const idRemapping = new Map<string, string>();
let assistantNeedsRewrite = false;
Copy link
Contributor

Choose a reason for hiding this comment

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

[P0] Deduplication remapping can drop tool calls/results when an assistant contains duplicate IDs in the same message.

Because idRemapping is keyed by the original ID, multiple tool calls with the same call.id in a single assistant turn will all map to the same new ID (the last one wins). Then effectiveToolCalls ends up with repeated id values, and pushToolResult will drop the second result as a duplicate (since it tracks seenToolResultIds by toolCallId). This can silently lose tool results and/or synthesize a missing result for the second call.

Scenario: assistant has two tool calls both id: "call_1", followed by two toolResults with toolCallId: "call_1" and different content. After rewrite, both tool calls become call_1_2, and the second toolResult is dropped as a duplicate.

Consider disambiguating duplicate IDs within a single assistant message (e.g., track per-call occurrence index rather than mapping by ID), or reject/repair at extraction time.

Also appears in: src/agents/session-transcript-repair.test.ts lacks coverage for same-message duplicate IDs.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/session-transcript-repair.ts
Line: 156:158

Comment:
[P0] Deduplication remapping can drop tool calls/results when an assistant contains duplicate IDs in the *same* message.

Because `idRemapping` is keyed by the original ID, multiple tool calls with the same `call.id` in a single assistant turn will all map to the same new ID (the last one wins). Then `effectiveToolCalls` ends up with repeated `id` values, and `pushToolResult` will drop the second result as a duplicate (since it tracks `seenToolResultIds` by `toolCallId`). This can silently lose tool results and/or synthesize a missing result for the second call.

Scenario: assistant has two tool calls both `id: "call_1"`, followed by two toolResults with `toolCallId: "call_1"` and different content. After rewrite, both tool calls become `call_1_2`, and the second toolResult is dropped as a duplicate.

Consider disambiguating duplicate IDs *within a single assistant message* (e.g., track per-call occurrence index rather than mapping by ID), or reject/repair at extraction time.

Also appears in: `src/agents/session-transcript-repair.test.ts` lacks coverage for same-message duplicate IDs.

How can I resolve this? If you propose a fix, please make it concise.

marcelomar21 and others added 3 commits February 3, 2026 16:59
Anthropic API rejects requests with duplicate tool_use IDs across messages.
This can happen when:
1. Session transcripts have multiple assistant messages with the same tool_use ID
2. IDs contain special characters that weren't being sanitized for Anthropic

Changes:
- Add deduplication logic in repairToolUseResultPairing() to detect and rename
  duplicate tool_use IDs in assistant messages (e.g., call_1 -> call_1_2)
- Update corresponding toolResult IDs to match the remapped tool_use IDs
- Enable sanitizeToolCallIds for Anthropic provider (was only Google/Mistral)
- Add tests for deduplication scenarios

Fixes error: "messages.X.content.Y: tool_use ids must be unique"
Previously, idRemapping used a Map<originalId, newId> which caused all
tool calls with the same ID in a single message to map to the last
generated newId. This resulted in lost tool results for earlier
occurrences.

Changes:
- Track remappings by block index instead of ID
- Collect multiple toolResults per ID in an array
- Consume results in order using a counter per originalId
- Add tests for same-message duplicate ID scenarios

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@marcelomar21 marcelomar21 force-pushed the fix/deduplicate-tool-use-ids branch from 7930ec3 to 820e0ac Compare February 3, 2026 20:19
@marcelomar21
Copy link
Author

Addressed: Duplicate IDs within same assistant message

Thanks for the excellent catch @greptileai! The issue was real - using Map<originalId, newId> caused all tool calls with the same ID in a single message to collapse to the same new ID, losing tool results.

Fix

Changed the remapping strategy from ID-based to block-index-based:

  1. Track by block index: Instead of idRemapping.set(call.id, newId), we now use blockIndexToNewId.set(blockIndex, newId) - each occurrence gets its own unique remapped ID
  2. Collect all results per ID: Changed spanResultsById from Map<string, toolResult> to Map<string, toolResult[]> to collect multiple results with the same original ID
  3. Consume in order: Added consumedCountById counter to match results to tool calls in the order they appear

Tests added

  • deduplicates tool_use IDs within the SAME assistant message - 2 tool calls with same ID, both results preserved
  • handles multiple duplicate IDs within the same message with varying counts - 3 tool calls with same ID, all 3 results preserved

Both scenarios now pass correctly with unique IDs (call_1, call_1_2, call_1_3) and matching tool results.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 3, 2026

Perfect! Your block-index-based fix elegantly solves the collision problem I identified. The key improvements are spot on:

What makes this solution robust:

  • Block-index tracking: blockIndexToNewId.set(blockIndex, newId) ensures each tool call occurrence gets its own unique ID, regardless of duplicates
  • Array-based collection: Map<string, toolResult[]> properly handles multiple results with the same original ID
  • Sequential consumption: consumedCountById matches results to tool calls in the correct order

Edge cases now handled:
✅ Cross-message duplicates (call_1 in message 1, call_1 in message 2 → call_1, call_1_2)
✅ Intra-message duplicates (3x call_1 in one message → call_1, call_1_2, call_1_3)
✅ All tool results preserved and correctly matched

The test coverage is thorough and the solution maintains the existing transcript repair pipeline while satisfying Anthropic's unique ID requirement. This should resolve the tool_use ids must be unique API rejection error completely.

Ready to ship! 🚢

@mudrii

This comment was marked as spam.

@mudrii

This comment was marked as spam.

@openclaw-barnacle
Copy link

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle bot added the stale Marked as stale due to inactivity label Mar 7, 2026
@openclaw-barnacle
Copy link

Closing due to inactivity.
If you believe this PR should be revived, post in #pr-thunderdome-dangerzone on Discord to talk to a maintainer.
That channel is the escape hatch for high-quality PRs that get auto-closed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling stale Marked as stale due to inactivity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants