From ff687aa780ebf12f0504d5f07e1509e9ca888b1b Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Fri, 27 Feb 2026 15:06:36 -0500 Subject: [PATCH] fix: re-read chat before publishing status event to preserve AI title (#22402) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem Chat titles revert to the fallback truncated title after briefly showing the AI-generated title. Even reloading the page doesn't help — the correct title flashes then gets overwritten. ## Root Cause Single bug, two symptoms. In `processChat` (`coderd/chatd/chatd.go`), the `chat` variable is passed by value. The flow: 1. `processChat(ctx, chat)` receives `chat` with the initial fallback title (truncated first message). 2. Inside `runChat`, `maybeGenerateChatTitle` generates an AI title, writes it to the DB via `UpdateChatByID`, and publishes a `title_change` event. **The DB has the correct title.** The client briefly displays it. 3. `runChat` returns. The **deferred cleanup** in `processChat` publishes `publishChatPubsubEvent(chat, StatusChange)` — but `chat` here is the original value copy that still has the **old fallback title**. 4. The frontend receives the `status_change` SSE event and **unconditionally applies `title` from every event kind** (see `AgentsPage.tsx` line ~305: `title: updatedChat.title`). This overwrites the correct AI title with the stale fallback. **Why reload doesn't help:** If the chat is still processing when the page reloads, `listChats` loads the correct title from the DB, but then the deferred `status_change` event arrives moments later and clobbers it. The title was always in the DB — it was the pubsub event that kept overwriting it. ## Fix Re-read the chat from the database in the deferred cleanup before publishing the final `status_change` event, so it carries the current (AI-generated) title. --- coderd/chatd/chatd.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/coderd/chatd/chatd.go b/coderd/chatd/chatd.go index 7fce12ce06..64d088ee2b 100644 --- a/coderd/chatd/chatd.go +++ b/coderd/chatd/chatd.go @@ -1689,6 +1689,16 @@ func (p *Server) processChat(ctx context.Context, chat database.Chat) { } p.publishStatus(chat.ID, status, uuid.NullUUID{}) + // Re-read the chat from the database to pick up any title + // changes made during processing (e.g. AI-generated titles + // from maybeGenerateChatTitle). The local `chat` variable + // is a value copy and won't reflect updates made in runChat. + if freshChat, readErr := p.db.GetChatByID(ctx, chat.ID); readErr == nil { + chat = freshChat + } else { + logger.Warn(ctx, "failed to re-read chat for status event", + slog.F("chat_id", chat.ID), slog.Error(readErr)) + } chat.Status = status p.publishChatPubsubEvent(chat, coderdpubsub.ChatEventKindStatusChange) }() @@ -1729,7 +1739,10 @@ func (p *Server) runChat( if err != nil { return xerrors.Errorf("get chat messages: %w", err) } - p.maybeGenerateChatTitle(ctx, chat, messages, model, logger) + // Fire title generation asynchronously so it doesn't block the + // chat response. It uses a detached context so it can finish + // even after the chat processing context is canceled. + go p.maybeGenerateChatTitle(context.WithoutCancel(ctx), chat, messages, model, logger) prompt, err := chatprompt.ConvertMessages(messages) if err != nil {