Skip to content

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

Closed

Description

@banchichen

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":
    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).

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions