@@ -730,11 +730,10 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
730730 // and the server-side dedupe sees one logical append.
731731 const partId = crypto . randomUUID ( ) ;
732732 const serializedBody = this . serializeInputChunk ( { kind : "message" , payload : wirePayload } ) ;
733- const sendChatMessage = async ( token : string ) => {
734- await this . appendInputChunk ( chatId , token , serializedBody , partId ) ;
735- } ;
733+ const sendChatMessage = ( token : string ) =>
734+ this . appendInputChunk ( chatId , token , serializedBody , partId ) ;
736735
737- await this . sendWithEvents (
736+ const inSeq = await this . sendWithEvents (
738737 chatId ,
739738 trigger ,
740739 {
@@ -755,7 +754,7 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
755754 state . isStreaming = true ;
756755 this . notifySessionChange ( chatId , state ) ;
757756
758- return this . subscribeToSessionStream ( state , abortSignal , chatId ) ;
757+ return this . subscribeToSessionStream ( state , abortSignal , chatId , { sinceInSeq : inSeq } ) ;
759758 } ;
760759
761760 /**
@@ -1124,12 +1123,13 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
11241123
11251124 const body = this . serializeInputChunk ( { kind : "message" , payload : wirePayload } ) ;
11261125 const partId = crypto . randomUUID ( ) ;
1127- const send = async ( token : string ) => {
1128- await this . appendInputChunk ( chatId , token , body , partId ) ;
1129- } ;
1126+ const send = ( token : string ) => this . appendInputChunk ( chatId , token , body , partId ) ;
11301127
1131- await this . sendWithEvents ( chatId , "action" , { partId, bodyBytes : byteLength ( body ) } , ( ) =>
1132- this . callWithAuthRetry ( chatId , state , send )
1128+ const inSeq = await this . sendWithEvents (
1129+ chatId ,
1130+ "action" ,
1131+ { partId, bodyBytes : byteLength ( body ) } ,
1132+ ( ) => this . callWithAuthRetry ( chatId , state , send )
11331133 ) ;
11341134
11351135 // Supersede any in-flight reader before subscribing — same as
@@ -1147,7 +1147,7 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
11471147 state . isStreaming = true ;
11481148 this . notifySessionChange ( chatId , state ) ;
11491149
1150- return this . subscribeToSessionStream ( state , undefined , chatId ) ;
1150+ return this . subscribeToSessionStream ( state , undefined , chatId , { sinceInSeq : inSeq } ) ;
11511151 } ;
11521152
11531153 // -------------------------------------------------------------------------
@@ -1191,15 +1191,15 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
11911191 }
11921192
11931193 /** Run a send op, emitting the terminal message-sent / message-send-failed event. */
1194- private async sendWithEvents (
1194+ private async sendWithEvents < T > (
11951195 chatId : string ,
11961196 source : ChatTransportSendSource ,
11971197 extras : { messageId ?: string ; partId ?: string ; bodyBytes ?: number } ,
1198- op : ( ) => Promise < void >
1199- ) : Promise < void > {
1198+ op : ( ) => Promise < T >
1199+ ) : Promise < T > {
12001200 const startedAt = Date . now ( ) ;
12011201 try {
1202- await op ( ) ;
1202+ const result = await op ( ) ;
12031203 const turnProducing =
12041204 source === "submit-message" || source === "regenerate-message" || source === "head-start" ;
12051205 if ( turnProducing ) {
@@ -1213,6 +1213,7 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
12131213 durationMs : Date . now ( ) - startedAt ,
12141214 ...extras ,
12151215 } ) ;
1216+ return result ;
12161217 } catch ( error ) {
12171218 const status = ( error as { status ?: unknown } ) . status ;
12181219 this . emitEvent ( {
@@ -1384,7 +1385,7 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
13841385 token : string ,
13851386 body : string ,
13861387 partId ?: string
1387- ) : Promise < void > {
1388+ ) : Promise < number | undefined > {
13881389 const ctx : ChatTransportEndpointContext = { endpoint : "in" , chatId } ;
13891390 const url = `${ this . resolveBaseURL ( ctx ) } /realtime/v1/sessions/${ encodeURIComponent ( chatId ) } /in/append` ;
13901391 // extraHeaders first so the fixed headers below win — a transport-wide
@@ -1407,24 +1408,26 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
14071408 err . status = response . status ;
14081409 throw err ;
14091410 }
1411+ // The appended record's `.in` seq, for correlating the response stream to
1412+ // this send. Omitted by older webapps / a lost idempotency claim.
1413+ const data = ( await response . json ( ) . catch ( ( ) => undefined ) ) as { seq ?: unknown } | undefined ;
1414+ return typeof data ?. seq === "number" ? data . seq : undefined ;
14101415 }
14111416
1412- private async callWithAuthRetry (
1417+ private async callWithAuthRetry < T > (
14131418 chatId : string ,
14141419 state : ChatSessionState ,
1415- op : ( token : string ) => Promise < void >
1416- ) : Promise < void > {
1420+ op : ( token : string ) => Promise < T >
1421+ ) : Promise < T > {
14171422 // 1) Try with the current PAT.
14181423 try {
1419- await op ( state . publicAccessToken ) ;
1420- return ;
1424+ return await op ( state . publicAccessToken ) ;
14211425 } catch ( err ) {
14221426 if ( isSessionNotFoundError ( err ) ) {
14231427 // The cached PAT authenticated but the session doesn't exist here —
14241428 // recreate it and retry.
14251429 await this . recreateSession ( chatId , state ) ;
1426- await op ( state . publicAccessToken ) ;
1427- return ;
1430+ return await op ( state . publicAccessToken ) ;
14281431 }
14291432 if ( ! isAuthError ( err ) ) throw err ;
14301433 }
@@ -1435,8 +1438,7 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
14351438 state . publicAccessToken = fresh ;
14361439 this . notifySessionChange ( chatId , state ) ;
14371440 try {
1438- await op ( fresh ) ;
1439- return ;
1441+ return await op ( fresh ) ;
14401442 } catch ( err ) {
14411443 if ( ! isSessionNotFoundError ( err ) ) throw err ;
14421444 }
@@ -1445,7 +1447,7 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
14451447 // state is stale (created in a different environment, or before the
14461448 // sessions upgrade). Recreate the session and retry once.
14471449 await this . recreateSession ( chatId , state ) ;
1448- await op ( state . publicAccessToken ) ;
1450+ return await op ( state . publicAccessToken ) ;
14491451 }
14501452
14511453 /**
@@ -1493,6 +1495,8 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
14931495 sendStopOnAbort ?: boolean ;
14941496 peekSettled ?: boolean ;
14951497 resumed ?: boolean ;
1498+ /** `.in` seq of the send that opened this stream; skip turn-completes below it (earlier turns). */
1499+ sinceInSeq ?: number ;
14961500 }
14971501 ) : ReadableStream < UIMessageChunk > {
14981502 const internalAbort = new AbortController ( ) ;
@@ -1750,6 +1754,15 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
17501754 }
17511755
17521756 if ( controlValue === TRIGGER_CONTROL_SUBTYPE . TURN_COMPLETE ) {
1757+ // Skip a turn-complete from an earlier turn (committed `.in` cursor
1758+ // below this send's seq), e.g. an undo action that raced this send.
1759+ if ( options ?. sinceInSeq !== undefined ) {
1760+ const cursorRaw = headerValue ( value . headers , SESSION_IN_EVENT_ID_HEADER ) ;
1761+ const cursor = cursorRaw !== undefined ? Number . parseInt ( cursorRaw , 10 ) : NaN ;
1762+ if ( ! Number . isNaN ( cursor ) && cursor < options . sinceInSeq ) {
1763+ continue ;
1764+ }
1765+ }
17531766 const refreshedToken =
17541767 headerValue ( value . headers , PUBLIC_ACCESS_TOKEN_HEADER ) ??
17551768 legacyChunk ?. publicAccessToken ;
0 commit comments