Think: reply renders twice after a tool approval (continuation start chunk not stamped with message id)
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 77/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- typescript
- Domain
- backend
Research direction
Start by reading Think._alignStreamStartId and the StreamAccumulator continuation logic described in the issue. Reproduce the tool-approval flow with an OpenAI-compatible provider and inspect the continuation start frame. Done means the resumed reply renders once under the id used for persistence; run the relevant continuation and stream tests.
Written by the indexing model from the issue text.
Description
Describe the bug
After a tool approval, the resumed reply renders twice in the client: once in the existing assistant message, and again in a second assistant message with a client-generated id. The stored transcript has the reply once, so a reload shows it correctly. The approved tool call can also show in both messages, because the continuation stream re-sends its tool-output-available.
The cause looks like Think._alignStreamStartId, which skips continuations:
_alignStreamStartId(chunk, action, accumulator, continuation) {
if (action?.type === "start" && action.messageId == null && !continuation) chunk.messageId = accumulator.messageId;
}
The comment above it explains why new turns are stamped: without start.messageId, "the live stream and the persisted message broadcast then can't reconcile by id, and the originating tab briefly renders the turn twice". Continuations are left out on the assumption that they reuse the existing assistant message through the continuation frame flag.
For a tool-approval continuation that doesn't extend the leaf assistant message, though, the StreamAccumulator gets a new id (continuationAssistant?.id ?? crypto.randomUUID()). Think persists the reply under that new id. The start chunk reaches the client without a messageId, so on AI SDK v7 the client builds the resumed stream under an id of its own. The two never reconcile, and the reply shows twice until a reload.
To Reproduce
- A
Thinkagent whose model is an OpenAI-compatible chat provider (createOpenAI({ baseURL }).chat(model)), sostartchunks carry nomessageId. - One server tool with
needsApproval: true. - Client:
useAgent+useAgentChatfrom@cloudflare/think/react, approving withaddToolApprovalResponse({ id, approved: true }). - Send a message that calls the tool, then approve it.
- The reply after the approval appears twice. The client's
messageshold two assistant messages with the same text: the original id, and a new client-side id. - Reload the page: the reply shows once.
The frames for the continuation (the frame shape is from a capture; the payloads are abridged):
cf_agent_stream_resuming
cf_agent_use_chat_response {"type":"start"} continuation:true, replay:true ← no messageId
cf_agent_use_chat_response {"type":"tool-output-available", ...} continuation:true
cf_agent_use_chat_response {"type":"text-start"} ... {"type":"finish"} continuation:true
cf_agent_chat_messages [...] ← persisted list, reply under the accumulator's id
cf_agent_use_chat_response done:true continuation:true
Expected behavior
The resumed reply renders once, under the same id Think persists it under.
Workaround
Stamp continuations too: put the accumulator's id on a start chunk that has none. Patching it per instance fixes the duplicate in our tests:
(this as any)._alignStreamStartId = (chunk, action, accumulator) => {
if (action?.type === "start" && chunk.messageId == null) chunk.messageId = accumulator.messageId;
};
The duplicated tool part can be filtered out on the client by de-duplicating tool parts on toolCallId across messages.
Suggested fix
In _alignStreamStartId, also stamp continuation start chunks when they carry no messageId. When the continuation extends the leaf, the accumulator already holds the leaf's id, so stamping is harmless there and fixes the non-extending case.
Version:
@cloudflare/think0.20.0agents0.26.0ai7.0.127@ai-sdk/react4.0.130@ai-sdk/openai4.0.83
Additional context
Related, but a different path: #1876, the recovery continuation appending a duplicate assistant message (closed). This one happens in the live approval flow with no restart involved.
- Dominant language
- TypeScript
- Stars
- 5.9k
- Forks
- 814
- Avg merge
- 1d 11h
- Merged PRs (30d)
- 169
Getting set up
- No Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from cloudflare/agents
-
`agents/vite`: decorator transform fails under isolated installs (Babel plugin resolved by name, `@babel/core@^8` undeclared)Possibly taken @akhil9tiet claimed this 2 days ago. Openbug
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
cloudflare/agents#2727 · 1 comment ·
Maintainers usually reply within 1 day
-
createAI: the ai peer range allows ai@6, which the provider cannot work withPossibly taken @akhil9tiet claimed this 6 days ago. Openbug
Difficulty 1/5 Under an hour Newbie friendliness 83/100
cloudflare/agents#2690 · 3 comments ·
Maintainers usually reply within 1 day
-
createAI: text-only user content is joined with no separator, which glues parts together and breaks code fencesPossibly taken @akhil9tiet claimed this 6 days ago. Openbug
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
cloudflare/agents#2682 · 2 comments ·
Maintainers usually reply within 1 day
-
documentation
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
cloudflare/agents#2645 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
cloudflare/agents#2635 · 1 comment ·
Maintainers usually reply within 1 day
All issues in cloudflare/agents
Similar issues
-
Difficulty 1/5 1-3 hours Newbie friendliness 66/100
Maintainers usually reply within 1 day
-
area:plugins area:security area:skills bug has repro
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
anthropics/claude-code#105911 ·
Maintainers usually reply within 1 day
-
documentation good first issue help wanted
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
dev-five-git/devup-ui#879 ·
Maintainers usually reply within 11 days
-
openspec-apply-change skill: when blocked, may select a ready artifact unrelated to missingArtifactsOpen
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Fission-AI/OpenSpec#2165 ·
Maintainers usually reply within 1 day
-
🐞 bug 🧐 unconfirmed
Difficulty 2/5 1-3 hours Newbie friendliness 66/100
Tencent/tdesign-vue-next#7032 · 1 comment ·
Maintainers usually reply within 1 day