Repository navigation
fix(agent-runtime): keep tool-call ids unique in every request - #780
Merged
Merged
Conversation
Anthropic-family endpoints, DeepSeek's included, reject the whole turn with `tool_use ids must be unique` when one call id appears twice, and the session cannot continue while that lasts (issue vastsa#718). The transcript is an append-only snapshot stream that tolerates a retried append, so the same call can reach the assembled context twice: under one row id (which the host's keep-last read already collapses) or under two (which it cannot). The last view before the wire now keeps the first occurrence of each `toolCall` id and drops a later call or a later result for that id, so the call/result pair the provider validates stays well-formed. A request with no duplicates is returned unchanged, and a drop is reported once on the `agent` log channel with the session and the ids. The guard sits at the request boundary (`convertToLlm`) rather than at the history rebuild, so it also covers a duplicate that appears while the session runs. Nothing is rewritten on disk; no compaction, retention or budget rule changes. C:\Users\10470\.pi-desktop\scratch\cb9e2d41-8a55-4a18-899d-92ca2791ab51\commit-718-r2.txt
vastsa
added a commit
that referenced
this pull request
Sep 21, 2026
Follow-up fix for #780: apply request-boundary tool-call deduplication to Task subagents and resumed delegate history.
This was referenced Sep 21, 2026
Open
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Toward #718. This does not claim to find the writer that duplicated the call — it makes the request valid when one exists, and it makes the next report name the writer.
Why this shape
@muzimu217's data-side check narrowed the 400 to the assembly path and listed three candidates (snapshot merging, the compaction boundary, retry replay). I verified what I could against the source and ruled two of them out:
apps/desktop/electron/main/runtime/event-persistence.ts:327,:334), and the host read is keep-last by id (crates/host-core/src/sessions.rs:752 dedupe_records, applied on bothsession.getpaths). So the "same call, same row id" case is already collapsed — the surviving shape is one call under two different row ids, which nothing collapses.packages/agent-runtime/src/reasoning-replay.ts:109, "Tool-call blocks are dropped on purpose"), and the retained tail keeps user messages only.I could not pin the writer without a DeepSeek key or the reporter's transcript, so the change is built to be correct either way:
A request can never carry a duplicate
tool_useid. The guard runs inconvertToLlm, the last view before the wire, which is the one place that sees both a rebuilt history and one that grew during this process:agentlog channel:[agent-runtime] dropped N duplicate tool call entries before the request (session=… ids=…).That log line is the point of the exercise: the next session that hits the provider's sentence turns it into the answer instead of another hypothesis about which of the three candidates it was.
What it does not do
Validation
packages/agent-runtime/src/runtime.test.ts:toolCall(call-1) and exactly onetoolResultwith the matchingtoolCallId, and logs one line naming the session and the id.Full suite:
pnpm --filter @pi-desktop/agent-runtime test→ 46/47 files; the one failure is the pre-existingnative-pi-sessioncase that fails identically on pristinemain.pnpm --filter @pi-desktop/agent-runtime build0;pnpm lintclean;node scripts/check-architecture.mjspassed;pnpm docs:check(78 EN/zh pairs, 495 pages) exit 0.E2E gate: NOT RUN. The scenario is written (
E2E-RUNTIME-unique-tool-call-ids-per-request, both locales, with traceability rows) but no desktop E2E suite was executed: it needs a deterministic provider fixture that captures the outgoing request, which this repository does not have yet. Alternative validation: the runtime-level tests above build a real runtime from a duplicated history and assert on the same view the request is built from. Remaining risk: the live-appended half of the guard (a duplicate that appears mid-session) is covered by the shared code path but not by a test that provokes it.Docs
03-runtime/02-agent-runtime.md§5 (the restored-history bullets),08-meta/decisions-log.mdD608, and the E2E scenario above — all mirrored todocs/zh-CN/….D608 is numbered after the D604–D607 proposed in my open #721, #748 and #756; I will renumber if those land first.