Repository navigation
fix(agent-runtime): keep tool-call ids unique in every request - #760
zhangqingkun976 wants to merge 1 commit into
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-rebased.txt
7dc948a to
0d934d1
Compare
|
I verified that the duplicate tool-call-id failure in #718 is real, but this PR does not repair the writer or message-assembly path. It only drops later duplicates in convertToLlm at the outgoing boundary; persisted or compacted history can still contain the duplicate IDs, and the PR explicitly leaves the writer unfixed. The PR also records its E2E gate as NOT RUN. This is a downstream mitigation rather than a root-cause fix, so it is not safe to merge under the repository PR intake rules. I am closing this PR; issue #718 remains open. |
|
Rebased onto The only conflicts were append-vs-append — the E2E scenario catalog and the decisions log, where main and this branch each added an entry — and both sides are kept. The identifier bands are still free: upstream's newest ADR is 0299 and its newest decisions-log entry is D607 (the plugin crash report, which is this repository's own fix for issue #747 built on my #756). |
|
Closed by accident and GitHub will not reopen it: an API-side ref update to rebase the branch reads as a force-push, and reopening is blocked for such a branch. The same content continues in #780. |
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.