@@ -109,15 +109,26 @@ function buildChatEventDedupeKey(eventState: string, event: Record<string, unkno
109109 return null ;
110110}
111111
112+ function getFinalMessageIdDedupeKey ( eventState : string , event : Record < string , unknown > ) : string | null {
113+ if ( eventState !== 'final' ) return null ;
114+ const msg = ( event . message && typeof event . message === 'object' )
115+ ? event . message as Record < string , unknown >
116+ : null ;
117+ if ( msg ?. id != null ) return `final-msgid|${ String ( msg . id ) } ` ;
118+ return null ;
119+ }
120+
112121function isDuplicateChatEvent ( eventState : string , event : Record < string , unknown > ) : boolean {
113122 const key = buildChatEventDedupeKey ( eventState , event ) ;
114- if ( ! key ) return false ;
123+ const msgKey = getFinalMessageIdDedupeKey ( eventState , event ) ;
124+ if ( ! key && ! msgKey ) return false ;
115125 const now = Date . now ( ) ;
116126 pruneChatEventDedupe ( now ) ;
117- if ( _chatEventDedupe . has ( key ) ) {
127+ if ( ( key && _chatEventDedupe . has ( key ) ) || ( msgKey && _chatEventDedupe . has ( msgKey ) ) ) {
118128 return true ;
119129 }
120- _chatEventDedupe . set ( key , now ) ;
130+ if ( key ) _chatEventDedupe . set ( key , now ) ;
131+ if ( msgKey ) _chatEventDedupe . set ( msgKey , now ) ;
121132 return false ;
122133}
123134
@@ -1118,38 +1129,50 @@ export const useChatStore = create<ChatState>((set, get) => ({
11181129 }
11191130
11201131 // Background: fetch first user message for every non-main session to populate labels upfront.
1121- // Uses a small limit so it's cheap; runs in parallel and doesn't block anything .
1132+ // Retries on "gateway startup" errors since the gateway may still be initializing .
11221133 const sessionsToLabel = sessionsWithCurrent . filter ( ( s ) => ! s . key . endsWith ( ':main' ) ) ;
11231134 if ( sessionsToLabel . length > 0 ) {
1124- void Promise . all (
1125- sessionsToLabel . map ( async ( session ) => {
1126- try {
1127- const r = await useGatewayStore . getState ( ) . rpc < Record < string , unknown > > (
1128- 'chat.history' ,
1129- { sessionKey : session . key , limit : 1000 } ,
1130- ) ;
1131- const msgs = Array . isArray ( r . messages ) ? r . messages as RawMessage [ ] : [ ] ;
1132- const firstUser = msgs . find ( ( m ) => m . role === 'user' ) ;
1133- const lastMsg = msgs [ msgs . length - 1 ] ;
1134- set ( ( s ) => {
1135- const next : Partial < typeof s > = { } ;
1136- if ( firstUser ) {
1137- const labelText = getMessageText ( firstUser . content ) . trim ( ) ;
1138- if ( labelText ) {
1139- const truncated = labelText . length > 50 ? `${ labelText . slice ( 0 , 50 ) } …` : labelText ;
1140- next . sessionLabels = { ...s . sessionLabels , [ session . key ] : truncated } ;
1135+ const LABEL_RETRY_DELAYS = [ 2_000 , 5_000 , 10_000 ] ;
1136+ void ( async ( ) => {
1137+ let pending = sessionsToLabel ;
1138+ for ( let attempt = 0 ; attempt <= LABEL_RETRY_DELAYS . length ; attempt += 1 ) {
1139+ const failed : typeof pending = [ ] ;
1140+ await Promise . all (
1141+ pending . map ( async ( session ) => {
1142+ try {
1143+ const r = await useGatewayStore . getState ( ) . rpc < Record < string , unknown > > (
1144+ 'chat.history' ,
1145+ { sessionKey : session . key , limit : 1000 } ,
1146+ ) ;
1147+ const msgs = Array . isArray ( r . messages ) ? r . messages as RawMessage [ ] : [ ] ;
1148+ const firstUser = msgs . find ( ( m ) => m . role === 'user' ) ;
1149+ const lastMsg = msgs [ msgs . length - 1 ] ;
1150+ set ( ( s ) => {
1151+ const next : Partial < typeof s > = { } ;
1152+ if ( firstUser ) {
1153+ const labelText = getMessageText ( firstUser . content ) . trim ( ) ;
1154+ if ( labelText ) {
1155+ const truncated = labelText . length > 50 ? `${ labelText . slice ( 0 , 50 ) } …` : labelText ;
1156+ next . sessionLabels = { ...s . sessionLabels , [ session . key ] : truncated } ;
1157+ }
1158+ }
1159+ if ( lastMsg ?. timestamp ) {
1160+ next . sessionLastActivity = { ...s . sessionLastActivity , [ session . key ] : toMs ( lastMsg . timestamp ) } ;
1161+ }
1162+ return next ;
1163+ } ) ;
1164+ } catch ( err ) {
1165+ if ( classifyHistoryStartupRetryError ( err ) === 'gateway_startup' ) {
1166+ failed . push ( session ) ;
11411167 }
11421168 }
1143- if ( lastMsg ?. timestamp ) {
1144- next . sessionLastActivity = { ...s . sessionLastActivity , [ session . key ] : toMs ( lastMsg . timestamp ) } ;
1145- }
1146- return next ;
1147- } ) ;
1148- } catch {
1149- // ignore per-session errors
1150- }
1151- } ) ,
1152- ) ;
1169+ } ) ,
1170+ ) ;
1171+ if ( failed . length === 0 || attempt >= LABEL_RETRY_DELAYS . length ) break ;
1172+ await sleep ( LABEL_RETRY_DELAYS [ attempt ] ! ) ;
1173+ pending = failed ;
1174+ }
1175+ } ) ( ) ;
11531176 }
11541177 }
11551178 } catch ( err ) {
0 commit comments