@@ -234,19 +234,8 @@ interface WithdrawnSendResult {
234234 busy ?: boolean
235235 /** Not sent at all (its Stop handoff failed); kept queued for the user to send. */
236236 held ?: boolean
237- /**
238- * The server refused this id outright (busy, or a predecessor still shutting
239- * down). Within the 1-hour claim TTL it answers a retry of an admitted id as a
240- * duplicate instead, so the server is known not to have it, and its queue
241- * entry can be edited.
242- */
243- notAdmitted ?: boolean
244- /**
245- * This attempt never reached the server (its Stop did not settle). That says
246- * nothing about an earlier attempt the message resumes, whose uncertainty it
247- * keeps.
248- */
249- neverSent ?: boolean
237+ /** Whether the server may hold `userMessageId`; see `admissionUnknown` in `startSendMessage`. */
238+ admissionUnknown : boolean
250239}
251240
252241/**
@@ -270,6 +259,8 @@ interface StartSendMessageOptions {
270259 * opening a second chat and billing a second turn.
271260 */
272261 resumeUserMessageId ?: string
262+ /** The queued entry's `admissionUnknown`, for the id it reuses. */
263+ admissionUnknown ?: boolean
273264 requestMode ?: ChatRequestMode
274265 assistantSearch ?: WorkspaceSearchFilters
275266 assistantSearchLevel ?: AssistantSearchLevel
@@ -3575,8 +3566,15 @@ export function useChat(
35753566
35763567 /* A retry of a withdrawn send reuses its id so the server deduplicates
35773568 the two attempts; anything else mints a fresh one. */
3578- const userMessageId =
3579- queuedSendHandoff ?. userMessageId ?? options ?. resumeUserMessageId ?? generateId ( )
3569+ const reusedId = queuedSendHandoff ?. userMessageId ?? options ?. resumeUserMessageId
3570+ const userMessageId = reusedId ?? generateId ( )
3571+ /* Whether the server may already hold `userMessageId`: the one fact that keeps a
3572+ queued message from being edited into a second turn. A reused id may have been
3573+ sent before, unless its entry knows the server refused it; a fresh id is unsent
3574+ until its POST goes out. Only the server refusing the id clears it again
3575+ (within the 1-hour claim TTL a retry of an admitted id is answered as a
3576+ duplicate, never refused). It rides the stored handoff and every withdrawal. */
3577+ let admissionUnknown = reusedId !== undefined && options ?. admissionUnknown !== false
35803578 const assistantId = getLiveAssistantMessageId ( userMessageId )
35813579
35823580 const storedAttachments : PersistedFileAttachment [ ] | undefined =
@@ -3626,6 +3624,8 @@ export function useChat(
36263624 organizationId,
36273625 supersededStreamId : queuedSendHandoff . supersededStreamId ,
36283626 ...( queuedSendHandoff . stopRequired ? { stopRequired : true } : { } ) ,
3627+ /** Without a pending Stop, its POST goes out next. */
3628+ admissionUnknown : admissionUnknown || ! queuedSendHandoff . stopRequired ,
36293629 userMessageId,
36303630 message,
36313631 ...( fileAttachments ? { fileAttachments } : { } ) ,
@@ -3900,7 +3900,7 @@ export function useChat(
39003900 setError ( getErrorMessage ( err , 'Failed to stop the previous response' ) )
39013901 /* Nothing was sent. Hand the message back so it stays in its chat's queue
39023902 even if the user has switched chats since the Stop began. */
3903- return { userMessageId, held : true , neverSent : true }
3903+ return { userMessageId, held : true , admissionUnknown }
39043904 }
39053905 }
39063906
@@ -3924,6 +3924,7 @@ export function useChat(
39243924 ? { }
39253925 : await getDesktopChatCapabilities ( desktopScopeIdRef . current )
39263926
3927+ admissionUnknown = true
39273928 const response = await fetch ( apiPathRef . current , {
39283929 method : 'POST' ,
39293930 headers : { 'Content-Type' : 'application/json' } ,
@@ -4017,7 +4018,7 @@ export function useChat(
40174018 }
40184019 if ( viewOnSend )
40194020 setError ( 'Previous response is still shutting down; queued message was restored.' )
4020- return { userMessageId, held : true , notAdmitted : true }
4021+ return { userMessageId, held : true , admissionUnknown : false }
40214022 }
40224023 /** Withdraws this refused send so the queue retries it, under the same id, later. */
40234024 const releaseRefusedSend = ( ) => {
@@ -4056,7 +4057,7 @@ export function useChat(
40564057 /* Only the chat lock refuses without naming this id. Admission's
40574058 "superseded" conflict, where another attempt took this id's claim and may
40584059 admit it, is answered as a duplicate naming this id instead. */
4059- return { userMessageId, busy : true , notAdmitted : true }
4060+ return { userMessageId, busy : true , admissionUnknown : false }
40604061 }
40614062 /* "Already sent" with no stream for it means the earlier attempt is still
40624063 in flight on the server (or died before starting a turn), not that a turn
@@ -4078,7 +4079,7 @@ export function useChat(
40784079 )
40794080 if ( ! dedupedStreamExists ) {
40804081 releaseRefusedSend ( )
4081- return { userMessageId, busy : true }
4082+ return { userMessageId, busy : true , admissionUnknown }
40824083 }
40834084 /** The user may have moved on (another chat, another send) during the check. */
40844085 if ( streamGenRef . current !== gen ) return consumedByTranscript
@@ -4189,7 +4190,7 @@ export function useChat(
41894190 server deduplicates it against that turn instead of billing
41904191 another one. */
41914192 rollbackOptimisticSend ( )
4192- return { userMessageId }
4193+ return { userMessageId, admissionUnknown }
41934194 }
41944195 return consumedByTranscript
41954196 }
@@ -4224,6 +4225,7 @@ export function useChat(
42244225 )
42254226 return {
42264227 userMessageId,
4228+ admissionUnknown,
42274229 unreachable : true ,
42284230 ...( onlineEventsRef . current !== onlineEventsAtSend ? { networkReturned : true } : { } ) ,
42294231 }
@@ -4430,9 +4432,7 @@ export function useChat(
44304432 : { } ) ,
44314433 ...( result . held ? { retryRequired : true } : { } ) ,
44324434 ...( result . busy ? busyRetry ( 1 ) : { } ) ,
4433- /* Only a refusal of this id settles it. A direct send never waits on a Stop
4434- (one pending queues it instead), so `neverSent` cannot occur here. */
4435- admissionUnknown : ! result . notAdmitted ,
4435+ admissionUnknown : result . admissionUnknown ,
44364436 ...( ( result . unreachable || result . busy ) && activeChatKey . startsWith ( PENDING_CHAT_KEY_PREFIX )
44374437 ? { heldSurface : heldSendSurface }
44384438 : { } ) ,
@@ -4648,6 +4648,9 @@ export function useChat(
46484648 userMessageId : handoff . userMessageId ,
46494649 ...( handoff . stopRequired ? { stopRequired : true } : { } ) ,
46504650 } ,
4651+ ...( handoff . admissionUnknown !== undefined
4652+ ? { admissionUnknown : handoff . admissionUnknown }
4653+ : { } ) ,
46514654 } )
46524655 clearQueuedSendHandoffState ( handoff . id )
46534656 clearQueuedSendHandoffClaim ( handoff . id )
@@ -5062,17 +5065,7 @@ export function useChat(
50625065 ? { heldSurface : heldSendSurface }
50635066 : { } ) ,
50645067 ...( withdrawnUserMessageId ? { resumeUserMessageId : withdrawnUserMessageId } : { } ) ,
5065- /* A refusal of this id settles it; an attempt that never left keeps the
5066- earlier uncertainty; any other withdrawal may have reached the server. */
5067- ...( withdrawn
5068- ? {
5069- admissionUnknown : withdrawn . notAdmitted
5070- ? false
5071- : withdrawn . neverSent
5072- ? dispatched . admissionUnknown === true
5073- : true ,
5074- }
5075- : { } ) ,
5068+ ...( withdrawn ? { admissionUnknown : withdrawn . admissionUnknown } : { } ) ,
50765069 } )
50775070 }
50785071
@@ -5104,6 +5097,9 @@ export function useChat(
51045097 ...( liveMsg . resumeUserMessageId
51055098 ? { resumeUserMessageId : liveMsg . resumeUserMessageId }
51065099 : { } ) ,
5100+ ...( liveMsg . admissionUnknown !== undefined
5101+ ? { admissionUnknown : liveMsg . admissionUnknown }
5102+ : { } ) ,
51075103 ...( liveMsg . requestMode ? { requestMode : liveMsg . requestMode } : { } ) ,
51085104 ...( liveMsg . assistantSearch ? { assistantSearch : liveMsg . assistantSearch } : { } ) ,
51095105 ...( liveMsg . assistantSearchLevel !== undefined
0 commit comments