diff --git a/frontend/src/shared/state/agentsSlice.ts b/frontend/src/shared/state/agentsSlice.ts index c3d9d715..5be9f50a 100644 --- a/frontend/src/shared/state/agentsSlice.ts +++ b/frontend/src/shared/state/agentsSlice.ts @@ -1227,27 +1227,33 @@ const agentsSlice = createSlice({ .addCase(fetchSession.fulfilled, (state, action) => { const session = action.payload; const existing = state.sessions[session.id]; - // Preserve optimistic-pending messages the server snapshot doesn't carry - // yet. On remount mid-stream (leave the chat + come back) this fetch's - // snapshot predates the just-sent user turn, so a blind replace wiped the - // user's own message bubble while the assistant stream (separate slice) - // kept going. Carry forward any local pending message not already echoed - // in the snapshot; the WS echo / next fetch dedupes it by client_message_id. + // Preserve local messages the server snapshot doesn't carry yet. On + // remount mid-stream (leave the chat + come back) this fetch's snapshot + // predates the just-sent user turn, so a blind replace wiped the user's + // own bubble while the assistant stream (separate slice) kept going. + // The WS echo clears optimistic_status the instant it arrives, so the + // message is usually "confirmed but not yet server-persisted" rather than + // still 'pending' (that's why a pending-only filter missed it). Gate on + // the session being LIVE: on a running/streaming session, carry forward + // any local message the snapshot lacks; on a settled session the snapshot + // is authoritative (so a server-side delete isn't resurrected). const incomingMsgs = session.messages ?? []; + const liveStatus = session.status === 'running' || session.status === 'waiting_approval'; const incomingClientIds = new Set( incomingMsgs.map((m) => m.client_message_id).filter(Boolean), ); const incomingIds = new Set(incomingMsgs.map((m) => m.id)); - const survivingOptimistic = (existing?.messages ?? []).filter( - (m) => - m.optimistic_status === 'pending' && - !(m.client_message_id && incomingClientIds.has(m.client_message_id)) && - !incomingIds.has(m.id), - ); + const surviving = liveStatus + ? (existing?.messages ?? []).filter( + (m) => + !incomingIds.has(m.id) && + !(m.client_message_id && incomingClientIds.has(m.client_message_id)), + ) + : []; state.sessions[session.id] = { ...session, - messages: survivingOptimistic.length - ? [...incomingMsgs, ...survivingOptimistic] + messages: surviving.length + ? [...incomingMsgs, ...surviving] : incomingMsgs, pending_approvals: session.pending_approvals ?? existing?.pending_approvals ?? [], tool_group_meta: session.tool_group_meta ?? existing?.tool_group_meta ?? {},