Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

Think: reply renders twice after a tool approval (continuation start chunk not stamped with message id)

Open Beginner friendly
#2,503 0 comments 0 reactions 0 assignees View on GitHub

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

  1. A Think agent whose model is an OpenAI-compatible chat provider (createOpenAI({ baseURL }).chat(model)), so start chunks carry no messageId.
  2. One server tool with needsApproval: true.
  3. Client: useAgent + useAgentChat from @cloudflare/think/react, approving with addToolApprovalResponse({ id, approved: true }).
  4. Send a message that calls the tool, then approve it.
  5. The reply after the approval appears twice. The client's messages hold two assistant messages with the same text: the original id, and a new client-side id.
  6. 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/think 0.20.0
  • agents 0.26.0
  • ai 7.0.127
  • @ai-sdk/react 4.0.130
  • @ai-sdk/openai 4.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

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from cloudflare/agents

All issues in cloudflare/agents

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.