fix(agent): dedup duplicate tool_call_id after compaction (#58327) - #58362
fix(agent): dedup duplicate tool_call_id after compaction (#58327)#58362PRATHAMESH75 wants to merge 3 commits into
Conversation
…Research#58327) Context compaction can leave the SAME assistant(tool_calls) group duplicated across two messages; the stub-injection step then doubles a dropped result into two identical tool results. Strict providers (DeepSeek) reject any repeated tool_call_id with HTTP 400 "Duplicate value for 'tool_call_id' of ... in message[N]", wedging the session right after compaction. sanitize_api_messages is the unconditional last guard before every provider call (conversation_loop, chat_completion_helpers) and already repairs orphaned pairs, but never deduplicated. Add a pass enforcing unique tool_call_id on the live send path: drop a repeated assistant tool_call (first wins; empty turns get a placeholder) and drop a repeated tool result. Separate seen-sets for assistant calls vs results so a valid call+result pair is not misread as a duplicate. Complements the repair_message_sequence dedup (reload/persistence path) with the live send path, closing both routes to the 400.
|
Closing as superseded: maintainer commit Proof of redundancy: my own send-path regression tests pass against pristine |
Duplicate of #58350, which merged 2026-07-04 and fixes the same duplicate |
What does this PR do?
Fixes the strict-provider HTTP 400
Duplicate value for 'tool_call_id' of ... in message[N](DeepSeek) that wedges a session immediately after context compaction (#58327).When compaction cuts through a multi-turn tool sequence it can leave the same
assistant(tool_calls)group duplicated across two messages. The pre-call stub-injection step then doubles a dropped result into two identicaltoolresults. Strict providers reject any repeatedtool_call_id; lenient providers (OpenAI/Anthropic) silently tolerate it, masking the bug. The reporter's debug log shows the session going permanently unrecoverable right after a1047 → 643compaction, with both an orphaned-tool400 and the duplicate-tool_call_id400.The fix closes both routes to the duplicate, at the two distinct message-repair layers:
sanitize_api_messagesis the unconditional last guard before every provider call (agent/conversation_loop.py,agent/chat_completion_helpers.py). It already repaired orphaned pairs but never duplicates. Added a pass that enforces uniquetool_call_id: a repeated assistanttool_callis dropped (first wins; a resulting empty turn keeps a(tool call removed)placeholder) and a repeatedtoolresult is dropped. Separate seen-sets for assistant calls vs. results, so a legitimate call+result pair is never misread as a duplicate.repair_message_sequencenow drops a duplicatetoolresult for the same livetool_call_idwhile preserving distinct parallel results (folded from @enzo-adami's [codex] Drop duplicate tool results during message repair #54594 — see Credits).Related Issue
Fixes #58327
Type of Change
Changes Made
agent/agent_runtime_helpers.pysanitize_api_messages: new step 3 — enforce uniquetool_call_idon the send path (assistant-call dedup + tool-result dedup, separate seen-sets, placeholder for emptied turns).repair_message_sequence: drop duplicatetoolresult for the same livetool_call_id, keep distinct parallel results (@enzo-adami, [codex] Drop duplicate tool results during message repair #54594).tests/run_agent/test_agent_guardrails.py: 4 cases — duplicate result deduped, duplicate assistant call deduped, emptied-turn placeholder, distinct-ids-not-treated-as-duplicate.tests/run_agent/test_message_sequence_repair.py: regression for the reload-path dedup (@enzo-adami).scripts/release.py:AUTHOR_MAPentry for the folded author.How to Test
Result: 44 passed. Ruff clean;
check-windows-footguns.pyclean.Checklist
Code
fix(scope):,feat(scope):, etc.)Documentation & Housekeeping
docs/, docstrings) — or N/Acli-config.yaml.exampleif I added/changed config keys — or N/A (no config keys)CONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — or N/ACredits
repair_message_sequenceduplicate-toolresult dedup (reload/persistence path) is folded in verbatim, preserving their commit authorship. This PR adds the complementary send-path guard insanitize_api_messagesso the live request is deduped too.tool_call_id400 on the base repair layer: @enzo-adami's #58024 and @Robinlovelace's #55436 (the latter also bundles a separate bug: SQLite session transcript accumulates duplicate messages (3-4x token inflation) #860 DB-write dedup). Maintainers may prefer to close those in favour of this consolidated PR, trim this one back, or land a different combination — entirely their call.