Skip to content

think: reconcile incoming messages to fix duplicate orphan assistant rows (#1381)#1396

Merged
threepointone merged 1 commit intomainfrom
think-message-reconciliation
Apr 26, 2026
Merged

think: reconcile incoming messages to fix duplicate orphan assistant rows (#1381)#1396
threepointone merged 1 commit intomainfrom
think-message-reconciliation

Conversation

@threepointone
Copy link
Copy Markdown
Contributor

@threepointone threepointone commented Apr 26, 2026

Fixes #1381.

Summary

When a user submits during a streaming tool turn, useAgentChat ships its optimistic in-flight assistant snapshot. With Session's INSERT-OR-IGNORE-by-ID semantics, the client's snapshot (client-generated ID, state: "input-available") gets persisted as a separate row alongside the eventual server-owned assistant for the same toolCallId. The next turn's convertToModelMessages then produces a malformed Anthropic prompt and the provider rejects it.

AIChatAgent already had the fix via reconcileMessages + resolveToolMergeId; Think did not. This PR:

  • Moves message-reconciler.ts from @cloudflare/ai-chat into agents/chat (the same pattern as the recent SubmitConcurrencyController extraction). AIChatAgent only changes the import path.
  • Wires reconciliation into Think._handleChatRequest before persistence. Stale input-available snapshots pick up the server's tool output via mergeServerToolOutputs, and any incoming assistant whose toolCallId already exists on a server row adopts the server's ID so persistence updates the existing row instead of inserting an orphan.
  • Adds a focused regression test for the issue's exact scenario, plus a test that an input-available snapshot with a matching server ID gets its tool output upgraded.
  • Updates design/chat-api.md, design/think-vs-aichat.md, design/think-roadmap.md, and design/chat-shared-layer.md to retract the "Session obviates reconciliation" claim.

Test plan

  • npx vitest --run -c src/chat/__tests__/vitest.config.ts (relocated reconciler tests + sibling unit tests, 193 passing)
  • npx vitest --run -c src/tests/vitest.config.ts src/tests/message-reconciliation.test.ts (new regression tests)
  • Full Think suite: npx vitest --run -c src/tests/vitest.config.ts (261 passing)
  • Full AIChat workers suite: npx vitest --project workers --run --sequence.shuffle false (387 passing)
  • npm run typecheck (75 projects)
  • ReadLints clean on touched files

Made with Cursor


Open in Devin Review

…rows (#1381)

When a user submits during a streaming tool turn, useAgentChat ships
its optimistic in-flight assistant snapshot. With Session's
INSERT-OR-IGNORE-by-ID semantics, the client's snapshot (client-
generated id, state: input-available) gets persisted as a separate row
alongside the eventual server-owned assistant for the same toolCallId.
The next turn's convertToModelMessages then produces a malformed
Anthropic prompt and the provider rejects it.

Fix:

- Move message-reconciler.ts from @cloudflare/ai-chat into agents/chat
  alongside the other shared chat primitives. AIChatAgent's import is
  the only behavioral change there.
- Wire reconcileMessages + resolveToolMergeId into Think's
  _handleChatRequest before persistence, mirroring AIChatAgent's
  persistMessages flow. Stale input-available snapshots pick up the
  server's tool output via mergeServerToolOutputs, and any incoming
  assistant whose toolCallId already exists on a server row adopts the
  server's id so persistence updates the existing row.
- Add a regression test for the issue's exact scenario (seed a
  server-owned tool assistant, post the client's optimistic snapshot,
  assert no duplicate row, server id and output preserved). Plus a
  test that a stale snapshot gets its tool output upgraded from the
  server row when ids match.
- Update design docs (chat-api.md, think-roadmap.md, think-vs-aichat.md,
  chat-shared-layer.md) to retract the "Session obviates reconciliation"
  claim.

Made-with: Cursor
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 26, 2026

🦋 Changeset detected

Latest commit: ddd013a

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

This PR includes changesets to release 3 packages
Name Type
agents Patch
@cloudflare/ai-chat Patch
@cloudflare/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

Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

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

View in Devin Review to see 4 additional findings.

Open in Devin Review

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 26, 2026

Open in StackBlitz

agents

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

@cloudflare/ai-chat

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

@cloudflare/codemode

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

hono-agents

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

@cloudflare/shell

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

@cloudflare/think

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

@cloudflare/voice

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

@cloudflare/worker-bundler

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

commit: ddd013a

@threepointone threepointone merged commit fdf5a8a into main Apr 26, 2026
2 checks passed
@threepointone threepointone deleted the think-message-reconciliation branch April 26, 2026 21:27
@github-actions github-actions Bot mentioned this pull request Apr 26, 2026
threepointone added a commit that referenced this pull request Apr 26, 2026
Add a new section summarizing four post-#1384 maintenance PRs (#1393#1396) and their effects on the multi-session assistant plan. Notes include facet bootstrap via explicit FacetStartupOptions.id (#1393) which removes the storage write/setName shim and makes MyAssistant.name resolve natively; the new beforeStep hook and TurnConfig.output passthrough (#1394); SubmitConcurrencyController being moved into agents/chat (#1395); and message-reconciler moved into agents/chat with Think now reconciling incoming messages (#1396). Clarifies that the chat-shared-layer has been incrementally hoisted into agents/chat and highlights the lack of a vitest+workers harness for examples/assistant, recommending a minimal test harness before hoisting useAgentChat.
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.

Think: concurrent user submit during a streaming turn creates orphan assistant + duplicate tool_use rows (400 on next turn)

1 participant