mirror of
https://github.com/simstudioai/sim.git
synced 2026-10-31 03:47:12 +08:00
fix(chat): keep sends FIFO across the streaming-to-idle drain gap
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
reconcileLiveAssistantTurn,
|
||||
selectReconnectReplayState,
|
||||
shouldActivateResourceEvent,
|
||||
shouldQueueOutgoingMessage,
|
||||
waitForDetachedChatResolution,
|
||||
} from '@/app/workspace/[workspaceId]/home/hooks/use-chat'
|
||||
import type {
|
||||
@@ -45,6 +46,27 @@ describe('shouldActivateResourceEvent', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('shouldQueueOutgoingMessage', () => {
|
||||
it('queues while a send is in flight', () => {
|
||||
expect(shouldQueueOutgoingMessage(true, false, 0)).toBe(true)
|
||||
})
|
||||
|
||||
it('queues while a stop is still settling', () => {
|
||||
expect(shouldQueueOutgoingMessage(false, true, 0)).toBe(true)
|
||||
})
|
||||
|
||||
it('queues behind messages still waiting after the turn ended', () => {
|
||||
// The regression: a message queued mid-stream must dispatch before one
|
||||
// typed in the idle gap after the turn stopped — a direct send here would
|
||||
// jump the queue and swap the user's message order.
|
||||
expect(shouldQueueOutgoingMessage(false, false, 1)).toBe(true)
|
||||
})
|
||||
|
||||
it('sends directly on an idle chat with an empty queue', () => {
|
||||
expect(shouldQueueOutgoingMessage(false, false, 0)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
function userMessage(id: string): PersistedMessage {
|
||||
return {
|
||||
id,
|
||||
|
||||
@@ -1202,6 +1202,25 @@ export function shouldActivateResourceEvent(
|
||||
return options?.activate === true || !activeResourceId || activeResourceId === resourceId
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a fresh outbound message must join the chat's send queue instead of
|
||||
* dispatching directly. Queueing while a send or stop is in flight is the
|
||||
* obvious half; the queued-ahead term preserves FIFO across the
|
||||
* streaming→idle boundary — a message queued while the previous turn streamed
|
||||
* must reach the model before one typed after that turn ended but before the
|
||||
* queue drained. Without it the fresh send jumps the queue and both the
|
||||
* transcript and the model see the user's messages in swapped order. The two
|
||||
* signals never gap mid-dispatch: a queued message stays in the queue until
|
||||
* its optimistic send applies, which is after the in-flight flag is set.
|
||||
*/
|
||||
export function shouldQueueOutgoingMessage(
|
||||
sendInFlight: boolean,
|
||||
stopPending: boolean,
|
||||
queuedAheadCount: number
|
||||
): boolean {
|
||||
return sendInFlight || stopPending || queuedAheadCount > 0
|
||||
}
|
||||
|
||||
export interface UseChatOptions {
|
||||
onResourceEvent?: ResourceEventHandler
|
||||
apiPath?: string
|
||||
@@ -4188,12 +4207,23 @@ export function useChat(
|
||||
|
||||
// An in-flight send drains the queue from `finalize`; a pending stop kicks
|
||||
// the dispatcher itself, since nothing else will once the stop settles.
|
||||
if (sendingRef.current || pendingStopPromiseRef.current) {
|
||||
// A non-empty queue forces queueing even on an idle chat: messages
|
||||
// queued while the previous turn streamed must go out first, so a fresh
|
||||
// send lands behind them instead of jumping the line in the drain gap
|
||||
// after a turn ends.
|
||||
const queuedAheadCount = (queueStore.queues[activeChatKey] ?? EMPTY_MESSAGE_QUEUE).length
|
||||
if (
|
||||
shouldQueueOutgoingMessage(
|
||||
Boolean(sendingRef.current),
|
||||
Boolean(pendingStopPromiseRef.current),
|
||||
queuedAheadCount
|
||||
)
|
||||
) {
|
||||
queueStore.enqueue(
|
||||
activeChatKey,
|
||||
createQueuedMessage(message, fileAttachments, contexts, options?.resumeUserMessageId)
|
||||
)
|
||||
if (pendingStopPromiseRef.current) {
|
||||
if (pendingStopPromiseRef.current || (queuedAheadCount > 0 && !sendingRef.current)) {
|
||||
void enqueueQueueDispatchRef.current({ type: 'send_head' })
|
||||
}
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user