@@ -237,19 +237,8 @@ interface WithdrawnSendResult {
237237 busy ?: boolean
238238 /** Not sent at all (its Stop handoff failed); kept queued for the user to send. */
239239 held ?: boolean
240- /**
241- * The server refused this id outright (busy, or a predecessor still shutting
242- * down). Within the 1-hour claim TTL it answers a retry of an admitted id as a
243- * duplicate instead, so the server is known not to have it, and its queue
244- * entry can be edited.
245- */
246- notAdmitted ?: boolean
247- /**
248- * This attempt never reached the server (its Stop did not settle). That says
249- * nothing about an earlier attempt the message resumes, whose uncertainty it
250- * keeps.
251- */
252- neverSent ?: boolean
240+ /** Whether the server may hold `userMessageId`; see `admissionUnknown` in `startSendMessage`. */
241+ admissionUnknown : boolean
253242}
254243
255244/**
@@ -273,6 +262,8 @@ interface StartSendMessageOptions {
273262 * opening a second chat and billing a second turn.
274263 */
275264 resumeUserMessageId ?: string
265+ /** The queued entry's `admissionUnknown`, for the id it reuses. */
266+ admissionUnknown ?: boolean
276267 requestMode ?: ChatRequestMode
277268 assistantSearch ?: WorkspaceSearchFilters
278269 assistantSearchLevel ?: AssistantSearchLevel
@@ -3586,8 +3577,15 @@ export function useChat(
35863577
35873578 /* A retry of a withdrawn send reuses its id so the server deduplicates
35883579 the two attempts; anything else mints a fresh one. */
3589- const userMessageId =
3590- queuedSendHandoff ?. userMessageId ?? options ?. resumeUserMessageId ?? generateId ( )
3580+ const reusedId = queuedSendHandoff ?. userMessageId ?? options ?. resumeUserMessageId
3581+ const userMessageId = reusedId ?? generateId ( )
3582+ /* Whether the server may already hold `userMessageId`: the one fact that keeps a
3583+ queued message from being edited into a second turn. A reused id may have been
3584+ sent before, unless its entry knows the server refused it; a fresh id is unsent
3585+ until its POST goes out. Only the server refusing the id clears it again
3586+ (within the 1-hour claim TTL a retry of an admitted id is answered as a
3587+ duplicate, never refused). It rides the stored handoff and every withdrawal. */
3588+ let admissionUnknown = reusedId !== undefined && options ?. admissionUnknown !== false
35913589 const assistantId = getLiveAssistantMessageId ( userMessageId )
35923590
35933591 const storedAttachments : PersistedFileAttachment [ ] | undefined =
@@ -3637,6 +3635,8 @@ export function useChat(
36373635 organizationId,
36383636 supersededStreamId : queuedSendHandoff . supersededStreamId ,
36393637 ...( queuedSendHandoff . stopRequired ? { stopRequired : true } : { } ) ,
3638+ /** Without a pending Stop, its POST goes out next. */
3639+ admissionUnknown : admissionUnknown || ! queuedSendHandoff . stopRequired ,
36403640 userMessageId,
36413641 message,
36423642 ...( fileAttachments ? { fileAttachments } : { } ) ,
@@ -3911,7 +3911,7 @@ export function useChat(
39113911 setError ( getErrorMessage ( err , 'Failed to stop the previous response' ) )
39123912 /* Nothing was sent. Hand the message back so it stays in its chat's queue
39133913 even if the user has switched chats since the Stop began. */
3914- return { userMessageId, held : true , neverSent : true }
3914+ return { userMessageId, held : true , admissionUnknown }
39153915 }
39163916 }
39173917
@@ -3935,6 +3935,7 @@ export function useChat(
39353935 ? { }
39363936 : await getDesktopChatCapabilities ( desktopScopeIdRef . current )
39373937
3938+ admissionUnknown = true
39383939 const response = await fetch ( apiPathRef . current , {
39393940 method : 'POST' ,
39403941 headers : { 'Content-Type' : 'application/json' } ,
@@ -4031,7 +4032,7 @@ export function useChat(
40314032 }
40324033 if ( viewOnSend )
40334034 setError ( 'Previous response is still shutting down; queued message was restored.' )
4034- return { userMessageId, held : true , notAdmitted : true }
4035+ return { userMessageId, held : true , admissionUnknown : false }
40354036 }
40364037 /** Withdraws this refused send so the queue retries it, under the same id, later. */
40374038 const releaseRefusedSend = ( ) => {
@@ -4070,7 +4071,7 @@ export function useChat(
40704071 /* Only the chat lock refuses without naming this id. Admission's
40714072 "superseded" conflict, where another attempt took this id's claim and may
40724073 admit it, is answered as a duplicate naming this id instead. */
4073- return { userMessageId, busy : true , notAdmitted : true }
4074+ return { userMessageId, busy : true , admissionUnknown : false }
40744075 }
40754076 /* "Already sent" with no stream for it means the earlier attempt is still
40764077 in flight on the server (or died before starting a turn), not that a turn
@@ -4092,7 +4093,7 @@ export function useChat(
40924093 )
40934094 if ( ! dedupedStreamExists ) {
40944095 releaseRefusedSend ( )
4095- return { userMessageId, busy : true }
4096+ return { userMessageId, busy : true , admissionUnknown }
40964097 }
40974098 /** The user may have moved on (another chat, another send) during the check. */
40984099 if ( streamGenRef . current !== gen ) return consumedByTranscript
@@ -4203,7 +4204,7 @@ export function useChat(
42034204 server deduplicates it against that turn instead of billing
42044205 another one. */
42054206 rollbackOptimisticSend ( )
4206- return { userMessageId }
4207+ return { userMessageId, admissionUnknown }
42074208 }
42084209 return consumedByTranscript
42094210 }
@@ -4245,6 +4246,7 @@ export function useChat(
42454246 )
42464247 return {
42474248 userMessageId,
4249+ admissionUnknown,
42484250 unreachable : true ,
42494251 ...( retryLater ? { } : { heldUntilOnline : true } ) ,
42504252 }
@@ -4449,9 +4451,7 @@ export function useChat(
44494451 ...( result . heldUntilOnline ? { retryRequired : true , heldUntilOnline : true } : { } ) ,
44504452 ...( result . held ? { retryRequired : true } : { } ) ,
44514453 ...( ( result . unreachable && ! result . heldUntilOnline ) || result . busy ? sendRetry ( 1 ) : { } ) ,
4452- /* Only a refusal of this id settles it. A direct send never waits on a Stop
4453- (one pending queues it instead), so `neverSent` cannot occur here. */
4454- admissionUnknown : ! result . notAdmitted ,
4454+ admissionUnknown : result . admissionUnknown ,
44554455 ...( ( result . unreachable || result . busy ) && activeChatKey . startsWith ( PENDING_CHAT_KEY_PREFIX )
44564456 ? { heldSurface : heldSendSurface }
44574457 : { } ) ,
@@ -4667,6 +4667,9 @@ export function useChat(
46674667 userMessageId : handoff . userMessageId ,
46684668 ...( handoff . stopRequired ? { stopRequired : true } : { } ) ,
46694669 } ,
4670+ ...( handoff . admissionUnknown !== undefined
4671+ ? { admissionUnknown : handoff . admissionUnknown }
4672+ : { } ) ,
46704673 } )
46714674 clearQueuedSendHandoffState ( handoff . id )
46724675 clearQueuedSendHandoffClaim ( handoff . id )
@@ -5083,17 +5086,7 @@ export function useChat(
50835086 ? { heldSurface : heldSendSurface }
50845087 : { } ) ,
50855088 ...( withdrawnUserMessageId ? { resumeUserMessageId : withdrawnUserMessageId } : { } ) ,
5086- /* A refusal of this id settles it; an attempt that never left keeps the
5087- earlier uncertainty; any other withdrawal may have reached the server. */
5088- ...( withdrawn
5089- ? {
5090- admissionUnknown : withdrawn . notAdmitted
5091- ? false
5092- : withdrawn . neverSent
5093- ? dispatched . admissionUnknown === true
5094- : true ,
5095- }
5096- : { } ) ,
5089+ ...( withdrawn ? { admissionUnknown : withdrawn . admissionUnknown } : { } ) ,
50975090 } )
50985091 }
50995092
@@ -5125,6 +5118,9 @@ export function useChat(
51255118 ...( liveMsg . resumeUserMessageId
51265119 ? { resumeUserMessageId : liveMsg . resumeUserMessageId }
51275120 : { } ) ,
5121+ ...( liveMsg . admissionUnknown !== undefined
5122+ ? { admissionUnknown : liveMsg . admissionUnknown }
5123+ : { } ) ,
51285124 ...( liveMsg . requestMode ? { requestMode : liveMsg . requestMode } : { } ) ,
51295125 ...( liveMsg . assistantSearch ? { assistantSearch : liveMsg . assistantSearch } : { } ) ,
51305126 ...( liveMsg . assistantSearchLevel !== undefined
0 commit comments