From d678c6fb16409f723f2e3a6e5be818823cc5159f Mon Sep 17 00:00:00 2001 From: Ethan <39577870+ethanndickson@users.noreply.github.com> Date: Fri, 27 Mar 2026 17:55:19 +1100 Subject: [PATCH] fix(coderd/x/chatd): forward local status events to fix delayed-startup banner (#23650) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem The agent chat delayed-startup banner ("Response startup is taking longer than expected") could appear even though the model was already streaming. The root cause is in `Subscribe()`: `message_part` events were delivered via the fast local in-process stream, while `status` events were delivered via PostgreSQL pubsub. Both feed into the same `select` statement, and Go's `select` picks whichever channel is ready first — there is no ordering guarantee between channels. So a `message_part` could outrun the `status=running` that logically precedes it. The frontend saw content arrive while it still thought the chat was pending, triggering the banner. ## Fix Also forward `status` events from the local channel, alongside `message_part`. Both event types already travel through the same FIFO subscriber channel: `publishStatus()` is called before the first `message_part`, so channel ordering guarantees the frontend sees `status=running` before any content. Pubsub still delivers a duplicate `status` event later; the frontend deduplicates it (`setChatStatus` is idempotent — it early-returns when the status hasn't changed). --- coderd/x/chatd/chatd.go | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/coderd/x/chatd/chatd.go b/coderd/x/chatd/chatd.go index fd5685fa98..21e2b4af66 100644 --- a/coderd/x/chatd/chatd.go +++ b/coderd/x/chatd/chatd.go @@ -2871,9 +2871,20 @@ func (p *Server) Subscribe( continue } if hasPubsub { - // Only forward message_part events from local - // (durable events come via pubsub + cache). - if event.Type == codersdk.ChatStreamEventTypeMessagePart { + // Forward transient events from local. + // Durable events (messages, queue updates) + // come via pubsub + cache. Status is + // included alongside message_part because + // both travel through the same ordered + // channel: publishStatus is called before + // the first message_part, so FIFO delivery + // guarantees the frontend sees + // status=running before any content. + // Pubsub will deliver a duplicate status + // later; the frontend deduplicates it + // (setChatStatus is idempotent). + if event.Type == codersdk.ChatStreamEventTypeMessagePart || + event.Type == codersdk.ChatStreamEventTypeStatus { select { case <-mergedCtx.Done(): return