AI assistant: fix a long answer killing the next turn — duplicate tool_use id, and a failed chunk reported as a cancellation - #5706
Conversation
Changes in packages/test-realm-cards/contents/skill-search-cards.jsonInstructions Changes--- /tmp/skill-diffs/old_instructions.txt 2026-08-06 14:42:06.234862865 +0000
+++ /tmp/skill-diffs/new_instructions.txt 2026-08-06 14:42:06.234862865 +0000
@@ -1 +1 @@
-
+Use search-cards-by-type-and-title to find cards by their type or title.Commands Changes--- /tmp/skill-diffs/old_commands.txt 2026-08-06 14:42:06.234862865 +0000
+++ /tmp/skill-diffs/new_commands.txt 2026-08-06 14:42:06.234862865 +0000
@@ -1 +1 @@
-[]
+[{"codeRef":{"name":"SearchCardsByTypeAndTitleCommand","module":"@cardstack/boxel-host/commands/search-cards"},"requiresApproval":true}] |
Preview deploymentsHost Test Results 1 files ±0 1 suites ±0 2h 24m 57s ⏱️ - 7m 45s Results for commit 1be9a47. ± Comparison against earlier commit 782d6f0. Realm Server Test Results 1 files ±0 1 suites ±0 13m 34s ⏱️ -16s Results for commit 1be9a47. ± Comparison against earlier commit 782d6f0. For more details on these errors, see this check. |
Aborting the runner always surfaces as APIUserAbortError, and the catch read that as the user having stopped the answer. The chunk handler aborts it too, when publishing a chunk fails — an event too large, say — and that path recorded the turn as canceled and dropped the error it had just captured. What the user sees is an answer that stops mid-sentence: no error, nothing to retry, and a log line saying it was canceled by user when nobody cancelled anything. The two are now told apart by the recorded chunk error, so a send failure is surfaced as the failure it is.
When an answer outgrows the event size limit it is cut into continuation events, and every piece was written with the whole tool-call list attached. The reader joins those pieces back into one message, so the same tool_use id appeared in it more than once and Anthropic refused the next request outright — "tool_use ids must be unique" — killing the turn after the one that split. Tool calls belong to the answer, not to each piece it was cut into, so only the final piece carries them.
1be9a47 to
cb4c51c
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cb4c51c60d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // tool_use id in that message more than once, which Anthropic rejects | ||
| // outright ("tool_use ids must be unique"), failing the whole next | ||
| // request. The final part below carries them. | ||
| [], |
There was a problem hiding this comment.
Route split tool calls through the root event
When an answer exceeds eventSizeMax and contains a host-executed tool call, this leaves the request only on the final continuation event. The host queues that continuation's ID in packages/host/app/services/matrix-service.ts:3009-3014, but packages/host/app/services/tool-service.ts:387-417 looks up the same ID in roomResource.messages, whose definition in packages/host/app/resources/room.ts:290-293 excludes all continuation messages. The retries therefore expire without validating or executing tools such as patchCard, leaving the tool and subsequent turn stuck. Keep the single request on the canonical root event, or canonicalize continuation IDs before tool processing.
Useful? React with 👍 / 👎.
(Written by Claude on Matic's behalf.)
Problem
An answer stops in the middle of a sentence. The user sees no error. The log shows only that the user stopped the answer. But the user did not stop it.
Two conditions stop the runner. Both give the same
APIUserAbortError:The program used the same procedure for the two conditions. It recorded the turn as cancelled. It also discarded the error, because it read
chunkHandlingErroronly in theelsepart. An abort does not go to that part.The second condition is different. The answer stops, but the user did not stop it. The user gets no message about the error.
Example
The assistant made a card family. The answer stopped after these words:
A large code block starts at this point. The event has
isCanceled: true. The room has no record of a cancellation. A subsequent turn then used a file that the assistant did not make. That turn gave a 404 error.Change
The program now uses
chunkHandlingErrorto identify the condition. If the chunk handler stopped the runner, the program shows the error. If the user stopped the answer, the behavior does not change.Limits
This change shows the error. It does not prevent the error. If large events are the cause, you must change how the program divides a long answer. That is a different change.