Skip to content

fix(chat): release useAgentChat streaming protection when the socket closes - #2496

Merged
ben-reitz merged 3 commits into
mainfrom
fix/use-agent-chat-stale-reply-after-close
Oct 6, 2026
Merged

ben-reitz merged 3 commits into
mainfrom
fix/use-agent-chat-stale-reply-after-close

Conversation

@ben-reitz

@ben-reitz ben-reitz commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Part of #2464, the client side. This fully fixes the bug for Think. AIChatAgent still needs its server-side change; see Merge Danger.

Summary

useAgentChat protects a locally streaming assistant reply so that stale snapshots can't replace it. Only that request's done/error frame released the protection. When the socket closes mid-stream, the transport ends the local stream and that frame never arrives. The protection stayed armed, so every later cf_agent_chat_messages snapshot had the cut-off local copy swapped back in. That included the full reply Think sends when a client reconnects between turns. The next send then shipped the cut-off copy to the server.

 onAgentClose()
   sawClose = true
   serverReportsRecovery = false
   fallbackAckedResumeRequestIds.clear()
+  protectedStreamingAssistantRef.current = null   # the done/error frame won't come

If the turn is still running on reconnect, the server's replayed start chunk re-arms protection. The existing replay reset de-duplicates the text, and it doesn't depend on protection.

Alternatives I rejected:

  • Release on resume_none idle: too late. Think's idle snapshot arrives before the probe answer, so the snapshot has already been overwritten, and this misses resume: false.
  • A "detached" flag (release on the next snapshot, re-attach on replay): it behaved the same in every reachable path, with extra state.

Evidence

New tests in packages/ai-chat/src/react-tests/use-agent-chat.test.tsx, in the describe block "useAgentChat stale reply after a mid-stream close (#2464)":

stream "Hel" → socket closes → server finishes "Hello world" → reconnect → snapshot
  expect the reply on screen to be "Hello world"; next send carries "Hello world"
Test main this PR
idle-connect snapshot replaces the cut-off reply after the turn ended while disconnected ❌ received Hel ✅
a later snapshot replaces the cut-off reply with resume disabled ❌ received Hel ✅
a mid-stream reconnect keeps protecting the resumed reply, with no doubled or lost text ✅ ✅
rebuilds the reply once when a snapshot without it lands before the replay ✅ ✅
an observer tab's cut-off copy is replaced by the idle snapshot ✅ ✅

The three rows that pass on main guard against the fix breaking resume. Suites: ai-chat react 106/106, agents react 168/168, agents chat unit 615/615, Think react 2/2. pnpm run check passes.

Merge Danger

Door: two-way

The change is one line.

Blast Radius: chat clients

Every useAgentChat user, on both Think and AIChatAgent. The only path that changes is a socket close during a local stream; resume replays still re-arm protection.

Still open in #2464:

  • AIChatAgent sends no transcript on idle connect. Until it does, an AIChatAgent client keeps the partial copy after reconnecting and still sends it on the next turn.
  • Servers persist a client's stale copy over a newer stored reply. This applies to both Think and AIChatAgent.

Already on main, not changed here: sending a second message mid-stream, then a close and a resume, duplicates the first reply.


Devin Review

@changeset-bot

changeset-bot Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0e491a7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
agents Patch
@cloudflare/agent-think Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@agent-think

agent-think Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

✅ agents import sizes: no significant changes (04922ec5 → 0e491a74, workflow run)

@devin-ai-integration devin-ai-integration Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note

Newer findings are available below. Devin Review posted a newer report on this PR, in addition to the findings presented here.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Devin Review

@pkg-pr-new

pkg-pr-new Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

agents

npm i https://pkg.pr.new/agents@2496

@cloudflare/ai-chat

npm i https://pkg.pr.new/@cloudflare/ai-chat@2496

@cloudflare/codemode

npm i https://pkg.pr.new/@cloudflare/codemode@2496

hono-agents

npm i https://pkg.pr.new/hono-agents@2496

@cloudflare/shell

npm i https://pkg.pr.new/@cloudflare/shell@2496

@cloudflare/think

npm i https://pkg.pr.new/@cloudflare/think@2496

@cloudflare/voice

npm i https://pkg.pr.new/@cloudflare/voice@2496

@cloudflare/worker-bundler

npm i https://pkg.pr.new/@cloudflare/worker-bundler@2496

commit: 0e491a7

useAgentChat protects a locally streaming assistant from behind snapshots
until that request's done/error frame. A WebSocket close ends the local
stream without that frame, so the protection stayed armed and swapped the
cut-off local copy into every later cf_agent_chat_messages snapshot. The
stored reply never showed, and the next send shipped the partial copy.

Release the protection on close. A resumed replay re-arms it from its
start chunk, so mid-stream resume still neither doubles nor drops text.

Refs #2464
@ben-reitz
ben-reitz force-pushed the fix/use-agent-chat-stale-reply-after-close branch from e337d9c to 6a5f1f5 Compare October 5, 2026 20:17
devin-ai-integration[bot]

This comment was marked as resolved.

@ben-reitz
ben-reitz merged commit cf7c9e3 into main Oct 6, 2026
17 checks passed
@ben-reitz
ben-reitz deleted the fix/use-agent-chat-stale-reply-after-close branch October 6, 2026 11:16
@github-actions github-actions Bot mentioned this pull request Oct 6, 2026
tombeckenham added a commit to tombeckenham/agents-tom that referenced this pull request Oct 7, 2026
Brings react-agui.tsx level with the legacy hook in agents/chat/react,
which received these through the upstream merge:

- cloudflare#2344 the server snapshot heals a diverged observed assistant
- cloudflare#2348 an observer skips replayed continuation frames it already applied
- cloudflare#2361 onToolCall fires only after the stream ends
- cloudflare#2378 onTurnEnd fires once per ended request
- cloudflare#2383 turns settle across replay, resume and observers (held and
  recovering requests, the resume:false idle probe)
- cloudflare#2496 streaming protection is released when the socket closes
- cloudflare#2394 follow the socket when the agent name changes
- cloudflare#2403 the tool-result cleanup no longer dispatches per chunk
- cloudflare#2039 keep an optimistic send the reconnect transcript omits

The last three had no failing test: their upstream tests are ported into
react-tests (the address-change one against a fake of useAgent's pending
socket, since this package has no test worker) and failed before the fix.

restoreBufferedSends, MAX_REMEMBERED_ENDED_TURNS and
isSocketAddressPending are exported from agents/chat/react so the two
hooks share them.
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.

1 participant