AI bot “Retry” creates a duplicate reply instead of regenerating, in public (non-PM) topics

Severity

Normal — but high-nuisance on busy public topics: repeated Retry clicks flood the topic with duplicate bot posts, so to members it looks like the bot is “posting on its own / stuck in a loop.”

Platform

  • Server: Discourse core main (2026.7.x line), reproduced against latest origin/main (dbf5eedac40, 2026-07-14). discourse-ai bundled in core.
  • Client: reproduced from multiple browsers/devices (desktop Chrome, Android Chrome, iOS Safari) — it is server-side, so client is irrelevant.

Description

Actual result: In a public topic, clicking Retry on an AI-bot reply appends a brand-new bot post to the topic. The original reply is left untouched, so each Retry adds another post. N retries → N extra posts.

Expected result: Retry should regenerate the existing bot reply in place (replace its content), the same way it does inside an AI-bot PM / DM conversation. No new post should be created.

Steps to reproduce (from a fresh Discourse)

  1. Enable Discourse AI + AI bot; create/enable an AI agent (persona) with a bot user, and turn on “Allow topic mentions” so it can answer in public topics (allow_topic_mentions). Add your user’s group to the agent’s allowed groups and to ai_bot_allowed_groups.
  2. In a public topic, @mention the bot in a post so it replies.
  3. On the bot’s reply, open the post admin/wrench menu and click Retry (the regenerate action → POST /discourse-ai/ai-bot/post/:post_id/retry).
  4. Observe: a new bot reply is appended instead of the existing one being regenerated. Repeat Retry → one extra post each time.
  5. For contrast, do the same inside a PM with the bot: Retry correctly regenerates in place (no duplicate). The divergence between PM and public topic is the tell.

Reproduces 100% of the time on public topics.

Root cause (code pointers, current origin/main)

  • BotController#retry_response (plugins/discourse-ai/app/controllers/discourse_ai/ai_bot/bot_controller.rb) enqueues Jobs::CreateAiReply with reply_post_id: post.id — i.e. “regenerate this existing post.”
  • Jobs::CreateAiReply loads it as reply_post and passes it to Playground#reply_to(..., existing_reply_post: reply_post).
  • In Playground#reply_to (plugins/discourse-ai/lib/ai_bot/playground.rb):
    • stream_reply = post.topic.private_message? if stream_reply.nil?false for public topics (~L518).
    • existing_reply_post is only consumed inside the if stream_reply branch (reply_post = existing_reply_post, ~L538).
    • The non-streaming else branch (~L662) unconditionally does PostCreator.create!(...), appending a new post and never touching the post being retried.

So for any non-PM topic, the “regenerate in place” intent from retry_response is silently dropped and a duplicate is created instead. (The same asymmetry means public-topic bot replies are non-streaming, which is the pre-existing behaviour; the bug is specifically that Retry’s existing_reply_post is ignored on that path.)

Suggested fix (direction only)

Honour existing_reply_post regardless of streaming — e.g. in the non-streaming branch, if existing_reply_post is present, revise it instead of calling PostCreator.create!; or decouple the “reuse existing post” decision from stream_reply.

Safe mode

N/A — server-side plugin behaviour, independent of themes/site JS.

1 Like