Skip to content

Issue with regenerate() — transport drops trigger, server doesn't delete stale messages #1012

Closed

Description

@alexander-zuev

Bug

regenerate() from useAgentChat fails with Anthropic models. The AI SDK correctly truncates messages client-side (removes last assistant message), but two issues in @cloudflare/ai-chat prevent it from working:

  1. ws-chat-transport.ts drops the trigger field — AI SDK passes trigger: 'regenerate-message' to the transport, but the transport never includes it in the WebSocket body
  2. persistMessages only UPSERTs, never DELETEs — when the client sends a truncated array, the old assistant message row stays in SQLite

Result: this.messages on the server still ends with the stale assistant message → Anthropic rejects with 400:

AI_APICallError: This model does not support assistant message prefill.
The conversation must end with a user message.

statusCode: 400
requestBodyValues.messages: [{ role: 'user', ... }, { role: 'assistant', ... }]
                                                      ^^^^^^^^^^^^^^^^^^^^^^^^
                                                      stale — should have been deleted

Steps to reproduce

Repro repo: https://github.com/alexander-zuev/ai-chat-regenerate-bug-repro

pnpm install
npx wrangler dev
  1. Send a message, wait for assistant response
  2. Click "Regenerate Last"
  3. Fails with the error above

The repro includes a side-by-side comparison: stock SDK (broken) vs a patched persistMessages that deletes stale rows (works).

Root cause

Transport (ws-chat-transport.ts):

// trigger is on options but never included in the body
const bodyPayload = JSON.stringify({
  messages: options.messages,
  ...extraBody,  // trigger is NOT here
});

Server (persistMessages in index.ts):

// UPSERT only — old rows never deleted
this.sql`
  insert into cf_ai_chat_agent_messages (id, message)
  values (${safe.id}, ${json})
  on conflict(id) do update set message = excluded.message
`;
// Then reloads ALL rows from SQLite, including stale ones
const persisted = this._loadMessagesFromDb();
this.messages = autoTransformMessages(persisted);

Suggested fix

  1. ws-chat-transport.ts — include trigger in the body:
const bodyPayload = JSON.stringify({
  messages: options.messages,
  trigger: options.trigger,
  ...extraBody,
});
  1. Server handler — when trigger === 'regenerate-message', delete DB rows not in the incoming array:
const incomingIds = new Set(mergedMessages.map(m => m.id));
const allDbIds = this.sql`SELECT id FROM cf_ai_chat_agent_messages`.toArray();
for (const row of allDbIds) {
  if (!incomingIds.has(row.id)) {
    this.sql`DELETE FROM cf_ai_chat_agent_messages WHERE id = ${row.id}`;
  }
}

Versions

  • @cloudflare/ai-chat: 0.1.5
  • agents: 0.6.0
  • ai: 6.0.104

Activity

  1. alexander-zuev commented on Feb 27, 2026

    @alexander-zuev
    Author

    Note: Fix mentioned is just a very rough patch to verify the root cause

    Raised this with Claude, but verified in source it doesn't hallucinate -> seems like the regenerate trigger not sent from the client hook + server doesn't handle it

    Before / After calling regenerate():
    Image
    Image

  2. changed the title [-]regenerate() broken — transport drops trigger, server doesn't delete stale messages[/-] [+]Issue with regenerate() — transport drops trigger, server doesn't delete stale messages[/+] on Feb 27, 2026
  3. threepointone commented on Feb 27, 2026

    @threepointone
    Contributor

    Thanks for digging in here, will fix

  4. added a commit that references this issue on Feb 27, 2026
    caf9a07
  5. threepointone commented on Feb 27, 2026

    @threepointone
    Contributor

    can you try npm i https://pkg.pr.new/cloudflare/agents/@cloudflare/ai-chat@1014

  6. alexander-zuev commented on Feb 28, 2026

    @alexander-zuev
    Author

    Tested the pkg.pr.new build from #1014 against our production AIChatAgent subclass (multi-workflow scoping, persistMessages override with metadata stamping).

    Results — all three fixes confirmed working:

    1. Regenerate — SDK's native regenerate() works, no Anthropic 400. We were manually truncating + resending as a workaround before this.
    2. Stale row deletion — SQLite clean after multiple regenerates and edits. Our persistMessages override passes options through to super.persistMessages(), _deleteStaleRows works correctly.
    3. Identical-content dedup — Forced two identical "Yes." assistant responses in the same conversation, navigated away and back, sent more messages. No duplicate rows (12 messages, all unique assistant_* IDs).

    Previously had a content-based re-IDing workaround for #957 in our persistMessages override — removed it since this PR handles reconciliation upstream.

  7. threepointone commented on Feb 28, 2026

    @threepointone
    Contributor

    You don't need to pass _deleteStaleRows

  8. alexander-zuev commented on Feb 28, 2026

    @alexander-zuev
    Author

    Ok thx need some more coffee and I'll review more carefully!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions