-
Notifications
You must be signed in to change notification settings - Fork 383
Description
Issue
Every assistant message gets stored twice in the DO's SQLite — once with the server-generated assistant_* ID, once with the client's nanoid ID.
After 3 user messages, SQLite has 8 rows (should be 6):
rowid id role text
----- --------------------------------- --------- --------------------------------------------------
1 MOhQOEEhu3YnXgsx user
2 assistant_1771621652018_po6druuea assistant Hello! How can I help you today?
3 RJHGHXxn1iP0pOi7 assistant Hello! How can I help you today? ← DUP
4 kwSt140SvyNPigp5 user
5 assistant_1771621789109_s9rq2j0va assistant Sure! Everything seems to be working fine...
6 q01cA3vA2iT0g8yT assistant Sure! Everything seems to be working fine... ← DUP
7 mFvYne67dIJ4YJjx user
8 assistant_1771621793779_ka5a8yav8 assistant Yes, I'm doing great...
Rows 2+3 and 5+6 have identical content, different IDs. Row 8 not yet duplicated (no subsequent `saveMessages()` round-trip).
Root cause
The server generates `assistant_*` IDs for assistant messages. The client (`useAgentChat`) independently generates nanoid IDs for the same messages during SSE streaming. When the client calls `saveMessages()`, both IDs arrive as separate rows.
The existing dedup mechanisms (`_mergeIncomingWithServerState` and `_resolveMessageForToolMerge`) only reconcile by `toolCallId` — plain text assistant messages have no `toolCallId`, so they pass through unmatched and get `INSERT`'d as duplicates.
Proposed fix
Option A: Client uses server-generated assistant_* ID from the SSE stream instead of generating its own nanoid.
Option B: Client stops sending assistant messages back via saveMessages() entirely. The server already has them — it generated them. Only new user messages need to round-trip.
Either fix eliminates the need for content-based reconciliation.
Reproduction
Minimal repro (code based on Build a Chat Agent):
https://github.com/alexander-zuev/ai-chat-message-duplication-repro
git clone https://github.com/alexander-zuev/ai-chat-message-duplication-repro
cd ai-chat-message-duplication-repro
npm install
npx wrangler dev
# Open http://localhost:8787, send 3+ messages, then:
sqlite3 .wrangler/state/v3/do/chat-agent-ChatAgent/*.sqlite \
"SELECT rowid, id, json_extract(message, '$.role') AS role, \
substr(json_extract(message, '$.parts[1].text'), 1, 60) AS text \
FROM cf_ai_chat_agent_messages ORDER BY rowid;"Workaround
Override `persistMessages()` to match client nanoid IDs to server `assistant_*` IDs by content:
override async persistMessages(messages: UIMessage[], excludeBroadcastIds?: string[]) {
const serverContentToId = new Map<string, string>();
for (const msg of this.messages) {
if (msg.role === 'assistant' && msg.id.startsWith('assistant_')) {
serverContentToId.set(JSON.stringify(msg.parts), msg.id);
}
}
const reconciled = messages.map((msg) => {
if (msg.role !== 'assistant' || msg.id.startsWith('assistant_')) return msg;
const serverId = serverContentToId.get(JSON.stringify(msg.parts));
return serverId ? { ...msg, id: serverId } : msg;
});
return super.persistMessages(reconciled, excludeBroadcastIds);
}Versions
- `@cloudflare/ai-chat`: 0.1.3
- `agents`: 0.5.1
- `ai`: 6.0.96
- `wrangler`: 4.67.0