@@ -154,7 +154,13 @@ class ChatStore {
154154 } ) ;
155155 if ( convId === conversationsStore . activeConversation ?. id ) this . currentResponse = response ;
156156 }
157- private clearChatStreaming ( convId : string ) : void {
157+ private clearChatStreaming ( convId : string , messageId ?: string ) : void {
158+ // session aware: a stale generation must not wipe a newer one's streaming state on the
159+ // same conversation, that would drop the frozen stop identity and stop the wrong session
160+ if ( messageId !== undefined ) {
161+ const cur = this . chatStreamingStates . get ( convId ) ;
162+ if ( cur && cur . messageId !== messageId ) return ;
163+ }
158164 this . chatStreamingStates . delete ( convId ) ;
159165 if ( convId === conversationsStore . activeConversation ?. id ) this . currentResponse = '' ;
160166 }
@@ -1055,11 +1061,14 @@ class ChatStore {
10551061 modelOverride ?: string | null ,
10561062 firstUserMessageContent ?: string
10571063 ) : Promise < void > {
1058- let effectiveModel = modelOverride ;
1064+ // the ::model suffix in the stream identity is only for router mode, where it routes to the
1065+ // owning child. in single-model mode the identity stays the bare conv id so that attach, stop
1066+ // and reattach all agree, regardless of fresh send vs regenerate passing a resolved model
1067+ let effectiveModel : string | null | undefined = undefined ;
10591068
1060- if ( isRouterMode ( ) && ! effectiveModel ) {
1069+ if ( isRouterMode ( ) ) {
10611070 const conversationModel = this . getConversationModel ( allMessages ) ;
1062- effectiveModel = selectedModelName ( ) || conversationModel ;
1071+ effectiveModel = modelOverride || selectedModelName ( ) || conversationModel ;
10631072 }
10641073
10651074 if ( isRouterMode ( ) && effectiveModel ) {
@@ -1074,6 +1083,9 @@ class ChatStore {
10741083 let resolvedModel : string | null = null ;
10751084 let modelPersisted = false ;
10761085 const convId = assistantMessage . convId ;
1086+ // freeze the POST identity from t0 so a stop cancels with the exact session key,
1087+ // never a stale or empty model resolved later
1088+ this . setChatStreaming ( convId , streamedContent , currentMessageId , effectiveModel ) ;
10771089
10781090 const recordModel = ( modelName : string | null | undefined , persistImmediately = true ) : void => {
10791091 if ( ! modelName ) return ;
@@ -1103,15 +1115,15 @@ class ChatStore {
11031115 } ;
11041116
11051117 const updateStreamingUI = ( ) => {
1106- this . setChatStreaming ( convId , streamedContent , currentMessageId ) ;
1118+ this . setChatStreaming ( convId , streamedContent , currentMessageId , effectiveModel ) ;
11071119 const idx = conversationsStore . findMessageIndex ( currentMessageId ) ;
11081120 conversationsStore . updateMessageAtIndex ( idx , { content : streamedContent } ) ;
11091121 } ;
11101122
11111123 const cleanupStreamingState = ( ) => {
11121124 this . setStreamingActive ( false ) ;
11131125 this . setChatLoading ( convId , false ) ;
1114- this . clearChatStreaming ( convId ) ;
1126+ this . clearChatStreaming ( convId , currentMessageId ) ;
11151127 this . setProcessingState ( convId , null ) ;
11161128 } ;
11171129
@@ -1128,7 +1140,7 @@ class ChatStore {
11281140 onReasoningChunk : ( chunk : string ) => {
11291141 streamedReasoningContent += chunk ;
11301142 // mark streaming state so a stop mid-thinking can persist the partial reasoning
1131- this . setChatStreaming ( convId , streamedContent , currentMessageId ) ;
1143+ this . setChatStreaming ( convId , streamedContent , currentMessageId , effectiveModel ) ;
11321144 const idx = conversationsStore . findMessageIndex ( currentMessageId ) ;
11331145 conversationsStore . updateMessageAtIndex ( idx , {
11341146 reasoningContent : streamedReasoningContent
@@ -1405,7 +1417,7 @@ class ChatStore {
14051417 // detached drain keeps producing tokens until eos or max_tokens. use the frozen identity
14061418 // captured when the session started, not the live dropdown
14071419 const streamStateForStop = this . chatStreamingStates . get ( convId ) ;
1408- const modelForStop = streamStateForStop ?. model ?? selectedModelName ( ) ;
1420+ const modelForStop = streamStateForStop ?. model ;
14091421 void ChatService . cancelServerStream ( convId , modelForStop ) ;
14101422 this . abortRequest ( convId ) ;
14111423 this . setChatLoading ( convId , false ) ;
@@ -1846,6 +1858,14 @@ class ChatStore {
18461858 updateStreamingContent ( originalContent + appendedContent ) ;
18471859 this . setChatReasoning ( msg . convId , false ) ;
18481860 } ,
1861+ onCompletionId : ( id : string ) => {
1862+ if ( ! id ) return ;
1863+ // refresh the message id so a later skip targets the live slot after a continue
1864+ conversationsStore . updateMessageAtIndex ( conversationsStore . findMessageIndex ( msg . id ) , {
1865+ completionId : id
1866+ } ) ;
1867+ DatabaseService . updateMessage ( msg . id , { completionId : id } ) . catch ( ( ) => { } ) ;
1868+ } ,
18491869 onReasoningChunk : ( chunk : string ) => {
18501870 appendedReasoning += chunk ;
18511871 hasReceivedContent = true ;
0 commit comments