Skip to content

Commit be44382

Browse files
committed
fix(mothership): retry a deduplicated send whose stream lookup failed
A lookup that failed with a network error or 5xx was read as proof that the earlier attempt's stream existed, so a send that never started was reattached and finalized instead of re-sent. Only a lookup this send aborted skips the retry now; the server deduplicates the retry by id.
1 parent cec0ebb commit be44382

2 files changed

Lines changed: 42 additions & 2 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.dom.test.tsx‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3073,6 +3073,43 @@ describe('useChat remount send recovery', () => {
30733073
).toEqual(['Follow-up after the restore'])
30743074
})
30753075

3076+
/** A stream lookup that fails for another reason does not prove a turn ran either. */
3077+
it('retries a deduplicated send whose stream lookup failed', async () => {
3078+
const history = idleHistory('chat-deduped-lookup-failed')
3079+
mockRequestJson.mockImplementation(() => Promise.resolve({ chat: history }))
3080+
let posts = 0
3081+
vi.stubGlobal('fetch', async (input: RequestInfo | URL, init?: RequestInit) => {
3082+
const url = String(input)
3083+
if (url === '/api/mothership/chat' && init?.method === 'POST') {
3084+
state.postBodies.push(JSON.parse(String(init.body)))
3085+
posts++
3086+
if (posts === 1) {
3087+
return Response.json(
3088+
{
3089+
error: 'This message was already sent.',
3090+
activeStreamId: state.postBodies[0].userMessageId,
3091+
},
3092+
{ status: 409 }
3093+
)
3094+
}
3095+
return emptySseResponse()
3096+
}
3097+
if (url.includes('/api/mothership/chat/stream') && posts === 1) {
3098+
return Response.json({ error: 'Internal error' }, { status: 500 })
3099+
}
3100+
return fetchStub(input, init)
3101+
})
3102+
const { getResult } = renderUseChatInChat(history.id, history)
3103+
3104+
await act(async () => {
3105+
await getResult().sendMessage('Told it was already sent, lookup failed')
3106+
})
3107+
await waitFor(() => state.postBodies.length === 2, 5_000)
3108+
3109+
expect(state.postBodies[1].message).toBe('Told it was already sent, lookup failed')
3110+
expect(state.postBodies[1].userMessageId).toBe(state.postBodies[0].userMessageId)
3111+
})
3112+
30763113
/**
30773114
* On the new-chat surface the "already sent" answer also names the chat the
30783115
* earlier attempt opened. With no stream yet, the retry must still go out,

‎apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4024,14 +4024,17 @@ export function useChat(
40244024
ran: reattaching would read the missing stream as finished and drop the
40254025
message. Retry it later like a busy refusal; the server's claim settles.
40264026
This is checked before adopting the chat the answer names, so a retried
4027-
message stays under the key it was sent from. */
4027+
message stays under the key it was sent from. A lookup that fails for
4028+
another reason proves nothing either way, so it is retried too: the server
4029+
deduplicates the retry by id. Only a lookup this send aborted (Stop, or the
4030+
user moving on) is not retried. */
40284031
const dedupedStreamExists = await fetchStreamBatch(
40294032
conflictStreamId,
40304033
'0',
40314034
abortController.signal
40324035
).then(
40334036
() => true,
4034-
(error: unknown) => !isStreamGoneError(error)
4037+
(error: unknown) => !isStreamGoneError(error) && abortController.signal.aborted
40354038
)
40364039
if (!dedupedStreamExists) {
40374040
releaseRefusedSend()

0 commit comments

Comments
 (0)