Skip to content

Commit 8fc5dae

Browse files
committed
fix(mothership): finish an admitted turn whose POST never answered on return
Recovery left a send alone while its POST had not answered, so a turn the server admitted and finished, whose answer never reached the client, kept the chat on Stop with nothing to clear it. The send now counts as admitted once the loaded chat holds its message, and recovery resolves its stream as any other.
1 parent 66c195d commit 8fc5dae

2 files changed

Lines changed: 67 additions & 3 deletions

File tree

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

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1322,6 +1322,62 @@ describe('useChat remount send recovery', () => {
13221322
expect(getResult().isSending).toBe(true)
13231323
})
13241324

1325+
/**
1326+
* The server admitted the send and finished its turn, but the POST's answer
1327+
* never arrived. Once the chat holds the message, a return resolves the turn
1328+
* rather than leaving the chat on Stop behind a POST that will not answer.
1329+
*/
1330+
it('finishes an admitted turn whose POST never answered when the user returns', async () => {
1331+
let admitted = false
1332+
const history: MothershipChatHistory = {
1333+
id: 'chat-admitted-unanswered',
1334+
mode: 'agent',
1335+
title: 'Admitted, unanswered',
1336+
messages: [],
1337+
activeStreamId: null,
1338+
resources: [],
1339+
}
1340+
mockRequestJson.mockImplementation(() => {
1341+
const userMessageId = state.postBodies[0]?.userMessageId
1342+
return Promise.resolve({
1343+
chat: {
1344+
...history,
1345+
messages:
1346+
admitted && userMessageId
1347+
? [
1348+
{ id: userMessageId, role: 'user', content: 'Answer lost' },
1349+
{ id: 'saved-answer', role: 'assistant', content: 'Done.' },
1350+
]
1351+
: [],
1352+
},
1353+
})
1354+
})
1355+
vi.stubGlobal('fetch', async (input: RequestInfo | URL, init?: RequestInit) => {
1356+
const url = String(input)
1357+
if (url === '/api/mothership/chat' && init?.method === 'POST') {
1358+
state.postBodies.push(JSON.parse(String(init.body)))
1359+
return new Promise<Response>(() => {})
1360+
}
1361+
if (url.includes('/api/mothership/chat/stream')) {
1362+
return Response.json({ success: true, events: [], status: 'complete' })
1363+
}
1364+
return fetchStub(input, init)
1365+
})
1366+
const { getResult } = renderUseChatInChat(history.id, history)
1367+
await act(async () => {
1368+
void getResult().sendMessage('Answer lost')
1369+
})
1370+
await waitFor(() => state.postBodies.length === 1)
1371+
1372+
admitted = true
1373+
await act(async () => {
1374+
window.dispatchEvent(new Event('online'))
1375+
})
1376+
await waitFor(() => !getResult().isSending)
1377+
1378+
expect(state.postBodies).toHaveLength(1)
1379+
})
1380+
13251381
it('keeps re-attaching a long turn whose tails deliver events between separate network failures', async () => {
13261382
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
13271383
try {

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3014,10 +3014,18 @@ export function useChat(
30143014
one: it ended while nothing here was listening (its reader went silent, or a
30153015
recovery it superseded was attached). Resolve that stream instead of
30163016
leaving it running: its terminal state replays the rest and finalizes. A
3017-
send still waiting for its POST to be admitted is not such a stream: the
3018-
chat cannot list it yet, and recovering it would abort that POST. */
3017+
send whose POST has not answered yet is such a stream only once the loaded
3018+
chat holds its message (the server admitted it, and the answer is lost);
3019+
until then it may still be on its way, and recovering it would abort it. */
3020+
const pendingAdmission = pendingChatAdmissionRef.current
3021+
const admitted =
3022+
!pendingAdmission ||
3023+
(loadedStream.loaded &&
3024+
queryClient
3025+
.getQueryData<MothershipChatHistory>(mothershipChatKeys.detail(chatId))
3026+
?.messages.some((message) => message.id === pendingAdmission.userMessageId) === true)
30193027
const locallyRunningStreamId =
3020-
sendingRef.current && !pendingChatAdmissionRef.current
3028+
sendingRef.current && admitted
30213029
? (streamIdRef.current ?? activeTurnRef.current?.userMessageId)
30223030
: undefined
30233031
const streamId = loadedStream.loaded

0 commit comments

Comments
 (0)