fix: show promoted queued message in chat timeline immediately#23232
Merged
fix: show promoted queued message in chat timeline immediately#23232
Conversation
7e3f351 to
63e3c75
Compare
Two issues caused the promoted message to never appear: 1. handlePromoteQueuedMessage discarded the ChatMessage returned by the promote API, relying on the WebSocket to deliver it. 2. Even when the WebSocket did deliver it (via upsertDurableMessage), the queue_update event in the same batch called updateChatQueuedMessages, which mutated the React Query cache. This gave chatMessagesList a new reference, triggering the message sync effect. The effect found the promoted message in the store but not in the REST-fetched data, classified it as a stale entry (the path designed for edit truncation), and called replaceMessages, wiping it. Fix (1): capture the ChatMessage from the promote response and upsert it into the store, matching handleSend for non-queued messages. Fix (2): track the fetched message array elements across effect runs using element-level reference comparison. Only run the hasStaleEntries/replaceMessages path when the message objects actually changed (e.g. a refetch producing new objects from the server), not when only an unrelated field like queued_messages caused the query data reference to update. Element references work because useMemo(flatMap) preserves object identity when only non-message fields change in the page data.
63e3c75 to
28591cf
Compare
DanielleMaywood
approved these changes
Mar 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two issues caused the promoted message to never appear in the chat
timeline.
Problem 1:
handlePromoteQueuedMessagediscarded theChatMessagereturned by the promote API, relying entirely on the WebSocket to
deliver it.
Problem 2 (root cause): even when the WebSocket delivered the
message (via
upsertDurableMessage), thequeue_updateevent in thesame batch called
updateChatQueuedMessages, which mutated the ReactQuery cache. This gave
chatMessagesLista new reference, triggeringthe message sync effect. The effect found the promoted message in the
store but not in the (stale) REST-fetched data, classified it as a
"stale entry" (a code path designed for edit truncation), and called
replaceMessages, wiping the promoted message from the store beforeit ever rendered.
Fix 1 (
AgentDetail.tsx): capture theChatMessagefrom thepromote response and upsert it into the store immediately, matching
the pattern in
handleSendfor non-queued messages.Fix 2 (
ChatContext.ts): track the fetched message array elementsacross effect runs using element-level reference comparison. Only run
the
hasStaleEntries/replaceMessagespath when the messageobjects actually changed (e.g. a refetch producing new objects from the
server), not when only an unrelated field like
queued_messagescausedthe query data reference to update. Element references work because
useMemo(flatMap)preserves object identity when only non-messagefields change in the page data. This avoids the ghost-on-edit-truncation
regression that a pure ID-fingerprint approach would have (IDs stay the
same after content edits, so the guard would never fire).
A regression test verifies that a WebSocket-delivered message survives
a
queue_updatein the same batch without being wiped by the synceffect.