From 8fce02102a680901920c23afba6d9054e04c4f30 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 7 Oct 2026 08:08:48 -0700 Subject: [PATCH] refactor(mothership): a saved Stop handoff's id wins on restore When an entry saved with an id on its Stop handoff also has its own resumeUserMessageId, the handoff's id is the one that build sent, so it is the one kept. resumeUserMessageId's doc names every path that sets it. --- .../stores/mothership-queue/store.dom.test.ts | 16 +++++++++++++++- apps/sim/stores/mothership-queue/store.ts | 5 +++-- apps/sim/stores/mothership-queue/types.ts | 12 +++++++----- 3 files changed, 25 insertions(+), 8 deletions(-) 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 }