@@ -40,11 +40,10 @@ import { GeminiDiffProcessor } from '@/gemini/utils/diffProcessor';
4040import type { GeminiMode , CodexMessagePayload } from '@/gemini/types' ;
4141import type { PermissionMode } from '@/api/types' ;
4242import { GEMINI_MODEL_ENV , DEFAULT_GEMINI_MODEL , CHANGE_TITLE_INSTRUCTION } from '@/gemini/constants' ;
43- import {
44- readGeminiLocalConfig ,
45- determineGeminiModel ,
43+ import {
44+ readGeminiLocalConfig ,
4645 saveGeminiModelToConfig ,
47- getInitialGeminiModel
46+ getInitialGeminiModel
4847} from '@/gemini/utils/config' ;
4948import {
5049 parseOptionsFromText ,
@@ -137,17 +136,45 @@ export async function runGemini(opts: {
137136 // Permission handler declared here so it can be updated in onSessionSwap callback
138137 // (assigned later after Happy server setup)
139138 let permissionHandler : GeminiPermissionHandler ;
139+
140+ // Session swap synchronization to prevent race conditions during message processing
141+ // When a swap is requested during processing, it's queued and applied after the current cycle
142+ let isProcessingMessage = false ;
143+ let pendingSessionSwap : ApiSessionClient | null = null ;
144+
145+ /**
146+ * Apply a pending session swap. Called between message processing cycles.
147+ * This ensures session swaps happen at safe points, not during message processing.
148+ */
149+ const applyPendingSessionSwap = ( ) => {
150+ if ( pendingSessionSwap ) {
151+ logger . debug ( '[gemini] Applying pending session swap' ) ;
152+ session = pendingSessionSwap ;
153+ if ( permissionHandler ) {
154+ permissionHandler . updateSession ( pendingSessionSwap ) ;
155+ }
156+ pendingSessionSwap = null ;
157+ }
158+ } ;
159+
140160 const { session : initialSession , reconnectionHandle } = setupOfflineReconnection ( {
141161 api,
142162 sessionTag,
143163 metadata,
144164 state,
145165 response,
146166 onSessionSwap : ( newSession ) => {
147- session = newSession ;
148- // Update permission handler with new session to avoid stale reference
149- if ( permissionHandler ) {
150- permissionHandler . updateSession ( newSession ) ;
167+ // If we're processing a message, queue the swap for later
168+ // This prevents race conditions where session changes mid-processing
169+ if ( isProcessingMessage ) {
170+ logger . debug ( '[gemini] Session swap requested during message processing - queueing' ) ;
171+ pendingSessionSwap = newSession ;
172+ } else {
173+ // Safe to swap immediately
174+ session = newSession ;
175+ if ( permissionHandler ) {
176+ permissionHandler . updateSession ( newSession ) ;
177+ }
151178 }
152179 }
153180 } ) ;
@@ -911,10 +938,10 @@ export async function runGemini(opts: {
911938 await geminiBackend . dispose ( ) ;
912939 geminiBackend = null ;
913940 }
914-
941+
915942 // Create new backend with new model
916943 const modelToUse = message . mode ?. model === undefined ? undefined : ( message . mode . model || null ) ;
917- geminiBackend = createGeminiBackend ( {
944+ const backendResult = createGeminiBackend ( {
918945 cwd : process . cwd ( ) ,
919946 mcpServers,
920947 permissionHandler,
@@ -924,16 +951,14 @@ export async function runGemini(opts: {
924951 // If explicitly null, will skip local config and use env/default
925952 model : modelToUse ,
926953 } ) ;
927-
954+ geminiBackend = backendResult . backend ;
955+
928956 // Set up message handler again
929957 setupGeminiMessageHandler ( geminiBackend ) ;
930-
931- // Start new session
932- // Determine actual model that will be used (from backend creation logic)
933- // Replicate backend logic: message model > env var > local config > default
934- const localConfigForModel = readGeminiLocalConfig ( ) ;
935- const actualModel = determineGeminiModel ( modelToUse , localConfigForModel ) ;
936- logger . debug ( `[gemini] Model change - modelToUse=${ modelToUse } , actualModel=${ actualModel } ` ) ;
958+
959+ // Use model from factory result (single source of truth - no duplicate resolution)
960+ const actualModel = backendResult . model ;
961+ logger . debug ( `[gemini] Model change - modelToUse=${ modelToUse } , actualModel=${ actualModel } (from ${ backendResult . modelSource } )` ) ;
937962
938963 // Update conversation history with new model
939964 conversationHistory . setCurrentModel ( actualModel ) ;
@@ -961,12 +986,15 @@ export async function runGemini(opts: {
961986 const userMessageToShow = message . mode ?. originalUserMessage || message . message ;
962987 messageBuffer . addMessage ( userMessageToShow , 'user' ) ;
963988
989+ // Mark that we're processing a message to synchronize session swaps
990+ isProcessingMessage = true ;
991+
964992 try {
965993 if ( first || ! wasSessionCreated ) {
966994 // First message or session not created yet - create backend and start session
967995 if ( ! geminiBackend ) {
968996 const modelToUse = message . mode ?. model === undefined ? undefined : ( message . mode . model || null ) ;
969- geminiBackend = createGeminiBackend ( {
997+ const backendResult = createGeminiBackend ( {
970998 cwd : process . cwd ( ) ,
971999 mcpServers,
9721000 permissionHandler,
@@ -976,25 +1004,14 @@ export async function runGemini(opts: {
9761004 // If explicitly null, will skip local config and use env/default
9771005 model : modelToUse ,
9781006 } ) ;
979-
1007+ geminiBackend = backendResult . backend ;
1008+
9801009 // Set up message handler
9811010 setupGeminiMessageHandler ( geminiBackend ) ;
982-
983- // Determine actual model that will be used
984- // Backend will determine model from: message model > env var > local config > default
985- // We need to replicate this logic here to show correct model in UI
986- const localConfigForModel = readGeminiLocalConfig ( ) ;
987- const actualModel = determineGeminiModel ( modelToUse , localConfigForModel ) ;
988-
989- const modelSource = modelToUse !== undefined
990- ? 'message'
991- : process . env [ GEMINI_MODEL_ENV ]
992- ? 'env-var'
993- : localConfigForModel . model
994- ? 'local-config'
995- : 'default' ;
996-
997- logger . debug ( `[gemini] Backend created, model will be: ${ actualModel } (from ${ modelSource } )` ) ;
1011+
1012+ // Use model from factory result (single source of truth - no duplicate resolution)
1013+ const actualModel = backendResult . model ;
1014+ logger . debug ( `[gemini] Backend created, model will be: ${ actualModel } (from ${ backendResult . modelSource } )` ) ;
9981015 logger . debug ( `[gemini] Calling updateDisplayedModel with: ${ actualModel } ` ) ;
9991016 updateDisplayedModel ( actualModel , false ) ; // Don't save - this is backend initialization
10001017
@@ -1258,7 +1275,11 @@ export async function runGemini(opts: {
12581275
12591276 // Use same logic as Codex - emit ready if idle (no pending operations, no queue)
12601277 emitReadyIfIdle ( ) ;
1261-
1278+
1279+ // Message processing complete - safe to apply any pending session swap
1280+ isProcessingMessage = false ;
1281+ applyPendingSessionSwap ( ) ;
1282+
12621283 logger . debug ( `[gemini] Main loop: turn completed, continuing to next iteration (queue size: ${ messageQueue . size ( ) } )` ) ;
12631284 }
12641285 }
0 commit comments