Skip to content

FM-044 — "Generate Fresh Brief" Race — Load Clobbers Send

openAIAgent navigated + queued pending message; conversation-load GET resolved after new POST and clobbered the new bubble.

FieldValue
SeveritySEV-2 (Smart Brief fresh-generation broken from Command Center)
CategorySidekick / Race conditions
Date2026-05-04
StatusArchived

User clicks “Generate fresh brief” in the Smart Brief dialog while viewing Command Center. Route changes to /c/<smartBriefId>, chat is visible, but no new request fires — the page sits on the prior brief’s stale content.

  1. openAIAgent listener for context: 'morning-brief': selectChat(id) + setState({ pendingMessage }) + navigate('/c/<id>'). On mount of SidekickConversationView:
    • Effect A sees selectedChatId is a number, calls handleConversationSelect(id), sets currentConversationId(id) synchronously, awaits GET /api/ai/conversations/:id
    • Effect A′ runs on next render — sees currentConversationId === selectedChatId and pendingMessage, fires chat.handleSendDirect(...) adding user bubble + starting new POST
    • GET resolves and setMessages(loadedMessages) clobbers the new bubbles with stale DB content
    • If data.streamingStatus === 'streaming', 2s poll continues to overwrite
  2. Server enforces a per-conversation request lock. If prior brief is still streaming server-side, new POST returns 429 with 30s auto-retry.
  1. Race guard in handleConversationSelect: after GET resolves, check if (isStreamingRef.current) return; BEFORE setMessages(loadedMessages) and BEFORE polling-resumption.
  2. Stop-stream registry in useSidekickStore: registerActiveStopStream(stopFn) / stopActiveStream() so callers outside the chat tree (Smart Brief refresh dialog, morning-brief listener) can abort an in-flight stream before queueing a new send. Both call stopActiveStream() first.
  • Any “queue a pending message into an existing chat” flow must abort the active stream first
  • When an effect mutates state synchronously before awaiting a fetch, add a guard before the post-fetch reload (isStreamingRef.current, or a generation counter)