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.
| Field | Value |
|---|---|
| Severity | SEV-2 (Smart Brief fresh-generation broken from Command Center) |
| Category | Sidekick / Race conditions |
| Date | 2026-05-04 |
| Status | Archived |
Symptom
Section titled “Symptom”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.
Root cause (compounded)
Section titled “Root cause (compounded)”openAIAgentlistener forcontext: 'morning-brief':selectChat(id)+setState({ pendingMessage })+navigate('/c/<id>'). On mount ofSidekickConversationView:- Effect A sees
selectedChatIdis a number, callshandleConversationSelect(id), setscurrentConversationId(id)synchronously, awaitsGET /api/ai/conversations/:id - Effect A′ runs on next render — sees
currentConversationId === selectedChatIdandpendingMessage, fireschat.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
- Effect A sees
- Server enforces a per-conversation request lock. If prior brief is still streaming server-side, new POST returns 429 with 30s auto-retry.
- Race guard in
handleConversationSelect: after GET resolves, checkif (isStreamingRef.current) return;BEFOREsetMessages(loadedMessages)and BEFORE polling-resumption. - 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 callstopActiveStream()first.
Prevention
Section titled “Prevention”- 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)