diff --git a/apps/sim/stores/mothership-queue/store.dom.test.ts b/apps/sim/stores/mothership-queue/store.dom.test.ts index 90e5d7b9c85..4d2ffb98c5b 100644 --- a/apps/sim/stores/mothership-queue/store.dom.test.ts +++ b/apps/sim/stores/mothership-queue/store.dom.test.ts @@ -77,7 +77,7 @@ describe('useMothershipQueueStore rehydration', () => { expect(plain?.admissionUnknown).toBeUndefined() }) - it('moves the reused id a saved Stop handoff carried onto the entry', async () => { + it('moves the reused id a saved Stop handoff carried onto the entry, over its own', async () => { const seed = { chatId: 'chat-A', supersededStreamId: 'previous-response', stopRequired: true } sessionStorage.setItem( 'mothership-queue', @@ -96,6 +96,12 @@ describe('useMothershipQueueStore rehydration', () => { admissionUnknown: false, queuedSendHandoff: { id: 'never-sent', ...seed, userMessageId: 'attempt-2' }, }, + { + id: 'both', + content: 'c', + resumeUserMessageId: 'withdrawn-attempt', + queuedSendHandoff: { id: 'both', ...seed, userMessageId: 'send-now-attempt' }, + }, ], }, }, @@ -120,6 +126,14 @@ describe('useMothershipQueueStore rehydration', () => { admissionUnknown: false, queuedSendHandoff: { id: 'never-sent', ...seed }, }, + { + id: 'both', + content: 'c', + /** The id that build sent: the handoff's, ahead of the entry's. */ + resumeUserMessageId: 'send-now-attempt', + admissionUnknown: true, + queuedSendHandoff: { id: 'both', ...seed }, + }, ]) }) }) diff --git a/apps/sim/stores/mothership-queue/store.ts b/apps/sim/stores/mothership-queue/store.ts index 40d56c61dd1..ee6c72c8c2d 100644 --- a/apps/sim/stores/mothership-queue/store.ts +++ b/apps/sim/stores/mothership-queue/store.ts @@ -85,12 +85,13 @@ function withCurrentWaitFields(value: unknown): unknown { if (!record) return value const { retryRequired, heldUntilOnline, sendRetries, notBefore, ...rest } = record /* An entry saved when its Stop handoff carried the reused id: that id is the - entry's `resumeUserMessageId` now. */ + entry's `resumeUserMessageId` now. It wins over the entry's own, as it did + when that build picked the id to send. */ const seed = toRecordOrNull(rest.queuedSendHandoff) if (seed && typeof seed.userMessageId === 'string') { const { userMessageId: seedRequestId, ...seedRest } = seed rest.queuedSendHandoff = seedRest - if (rest.resumeUserMessageId === undefined) rest.resumeUserMessageId = seedRequestId + rest.resumeUserMessageId = seedRequestId } return { ...rest, diff --git a/apps/sim/stores/mothership-queue/types.ts b/apps/sim/stores/mothership-queue/types.ts index b0ad092668a..8347e6fefc4 100644 --- a/apps/sim/stores/mothership-queue/types.ts +++ b/apps/sim/stores/mothership-queue/types.ts @@ -34,11 +34,13 @@ export type QueuedMothershipMessage = QueuedMessage & { */ retry?: SendRetry /** - * Message id of a prior attempt at this send that an unmount cleanup - * withdrew. Reused when the entry is dispatched so the server deduplicates - * against that attempt — it never sees the client's abort, so a request it - * had already accepted still opened the chat and billed the turn. Persisted, - * so a retry after a reload deduplicates too. + * Message id of an earlier attempt at this send, the one id it goes out + * under: a send an unmount cleanup withdrew, a Send-now restored from its + * stored Stop handoff, or a dispatch put back in the queue. Reused when the + * entry is dispatched so the server deduplicates against that attempt — it + * never sees the client's abort, so a request it had already accepted still + * opened the chat and billed the turn. Persisted, so a retry after a reload + * deduplicates too. */ resumeUserMessageId?: string }