Skip to content

AI assistant: fix a long answer killing the next turn — duplicate tool_use id, and a failed chunk reported as a cancellation - #5706

Merged
jurgenwerk merged 2 commits into
mainfrom
fix-aibot-chunk-error-masquerades-as-cancel
Aug 10, 2026
Merged

AI assistant: fix a long answer killing the next turn — duplicate tool_use id, and a failed chunk reported as a cancellation#5706
jurgenwerk merged 2 commits into
mainfrom
fix-aibot-chunk-error-masquerades-as-cancel

Conversation

@jurgenwerk

@jurgenwerk jurgenwerk commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

(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 user sends a new message, or the user clicks Stop. This is a true cancellation.
  • The chunk handler finds an error when it sends a chunk. For example, the event is too large.

The program used the same procedure for the two conditions. It recorded the turn as cancelled. It also discarded the error, because it read chunkHandlingError only in the else part. 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:

Here's the Run card (the workout entry):

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 chunkHandlingError to 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.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Changes in packages/test-realm-cards/contents/skill-search-cards.json

Instructions 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}]

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Preview deployments

Host Test Results

    1 files  ±0      1 suites  ±0   2h 24m 57s ⏱️ - 7m 45s
3 839 tests ±0  3 825 ✅ ±0  14 💤 ±0  0 ❌ ±0 
3 858 runs  ±0  3 844 ✅ ±0  14 💤 ±0  0 ❌ ±0 

Results for commit 1be9a47. ± Comparison against earlier commit 782d6f0.

Realm Server Test Results

    1 files  ±0      1 suites  ±0   13m 34s ⏱️ -16s
2 086 tests ±0  2 085 ✅  - 1  0 💤 ±0  1 ❌ +1 
2 165 runs  ±0  2 164 ✅  - 1  0 💤 ±0  1 ❌ +1 

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.
@jurgenwerk
jurgenwerk force-pushed the fix-aibot-chunk-error-masquerades-as-cancel branch from 1be9a47 to cb4c51c Compare August 7, 2026 11:05
@jurgenwerk jurgenwerk changed the title Stop a failed chunk from being recorded as a user cancellation AI assistant: fix a long answer killing the next turn — duplicate tool_use id, and a failed chunk reported as a cancellation Aug 7, 2026
@jurgenwerk
jurgenwerk marked this pull request as ready for review August 7, 2026 12:14
@jurgenwerk
jurgenwerk requested a review from a team August 7, 2026 12:14

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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.
[],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

@jurgenwerk
jurgenwerk merged commit 3861ad6 into main Aug 10, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants