cloudflare / cloudflare/agents

useAgentChat: cf_agent_chat_messages handler replaces the message array wholesale, dropping client-buffered sends on reconnect

Open
#1,983 0 comments 1 reaction 1 assignee Claimed by @threepointone View on GitHub
bug
Dominant language
TypeScript
Stars
5.6k
Forks
711
Avg merge
1d 20h
Merged PRs (30d)
53

Description

**Describe the bug**

`useAgentChat`'s transport applies the server's authoritative transcript frame (`cf_agent_chat_messages`) with a whole-array `setMessages(next)`. On an idle connect/reconnect this replaces the local list with the server snapshot, dropping any optimistic message the client just added via `sendMessage()` that the snapshot doesn't yet contain.

Concretely: the user sends a message while the socket is reconnecting. PartySocket buffers the frame (so the turn is reliably delivered) and `sendMessage` renders the message optimistically — but the reconnect snapshot doesn't include it yet, so `setMessages(next)` erases it from the UI. It reappears only after the turn completes and the server rebroadcasts. So it's not data loss; it's a UI-consistency issue — the SDK optimistically shows a send it has committed to delivering, then its own replay wipes it mid-flight.

**To Reproduce**

1. Render a chat with existing messages via `useAgentChat`.
2. Drop the network (toggle Wi‑Fi off), then let the socket reconnect.
3. During the reconnect window (socket not yet `OPEN`), call `sendMessage(...)`. The message renders optimistically; PartySocket buffers the frame.
4. On reconnect, the agent's idle-connect flow pushes the transcript as `cf_agent_chat_messages` (without the just-sent message).
5. `setMessages(next)` runs → the optimistic message disappears (the user sees the reply but not their own message), returning only after the turn ends.

**Expected behavior**

The reconnect transcript replay shouldn't drop an in-flight optimistic message the client just sent. Merge the snapshot with local state (preserve trailing local-only messages it doesn't include) instead of replacing wholesale — or expose a reconcile hook so hosts can merge it themselves.

**Screenshots**

The sent message is visible, disappears during reconnect, then reappears after the turn.

Image

**Version:**

`agents@0.18.0` (also reproduces on `0.17.3`). Handler in `agents/dist/chat/react.js`, re-exported unchanged by `@cloudflare/think`.

**Additional context**

- Location: `useAgentChat` → `onAgentMessage`, `case "cf_agent_chat_messages"`:
```js
let next = preserveProtectedStreamingAssistant(data.messages);
// ...observing-accumulator merge...
setMessages(next); // whole-array replace — drops client-local optimistic sends
```
- The frame is pushed on every idle connect/reconnect (e.g. `@cloudflare/think`'s `_buildIdleConnectMessages` when no stream is active; `AIChatAgent` has an equivalent replay).
- Not data loss: the send is buffered and flushed on reconnect, so the turn runs and persists. Visible recovery depends on the server rebroadcasting the transcript to the *sender* after the turn; if the sender is excluded, it stays gone until reload.
- Host-side workaround we use: merge trailing local-only `role: "user"` messages instead of replacing (scoped to `user` so a legitimate assistant replacement like regenerate isn't resurrected).

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.