From a46dbad334aa151d2a03cc4e8804e429c3844eba Mon Sep 17 00:00:00 2001 From: Menny Even Danan Date: Wed, 22 Apr 2026 18:57:50 +0000 Subject: [PATCH 1/5] fix(gemini): await sessionManager.ready before server starts accepting requests SessionManager loads all persisted Gemini sessions from ~/.gemini/sessions/*.json asynchronously in its constructor via `this.ready = this.init()`. Previously, `sessionManager.ready` was never awaited anywhere, so the server would start accepting WebSocket connections before loadSessions() had completed. This created a race condition on the very first request after a server restart: if a client tried to resume an existing Gemini session, `sessionManager.getSession(sessionId)` would return undefined (the session hadn't been loaded from disk yet), causing the `--resume ` flag to be silently omitted when spawning the Gemini CLI process. The CLI would then start a blank new session instead of continuing the prior conversation, with no error or indication to the user. The fix adds `await sessionManager.ready` in startServer() immediately after `await initializeDatabase()`, following the same pattern already established for database initialization. This guarantees the session store is fully populated before any request handler can call getSession(), createSession(), or addMessage(). No other providers are affected: Claude, Cursor, and Codex session providers are stateless per-request (they read from disk or SQLite on demand) and have no equivalent eager-loading singleton. Co-Authored-By: Claude Sonnet 4.6 --- server/index.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/index.js b/server/index.js index 62d85130ba..35e17d9372 100755 --- a/server/index.js +++ b/server/index.js @@ -2312,6 +2312,9 @@ async function startServer() { // Initialize authentication database await initializeDatabase(); + // Ensure Gemini session store is loaded from disk before accepting requests + await sessionManager.ready; + // Configure Web Push (VAPID keys) configureWebPush(); From 048431d215c38afeb5e45a926ed9fd3f864f00b0 Mon Sep 17 00:00:00 2001 From: Menny Even Danan Date: Wed, 22 Apr 2026 23:04:16 +0000 Subject: [PATCH 2/5] fix(gemini): use cliSessionId when fetching CLI session history fetchHistory used the UI's sessionId (e.g., gemini_) when falling back to getGeminiCliSessionMessages. However, the Gemini CLI stores its history keyed by its native cliSessionId. This caused the fallback to fail silently, preventing the restoration of conversation history from disk for existing sessions. The fix extracts the session object from sessionManager and uses session.cliSessionId if available, ensuring history is correctly retrieved using the CLI's native identifier. --- .../modules/providers/list/gemini/gemini-sessions.provider.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/modules/providers/list/gemini/gemini-sessions.provider.ts b/server/modules/providers/list/gemini/gemini-sessions.provider.ts index 7d5b5f1aa5..5ff50123ee 100644 --- a/server/modules/providers/list/gemini/gemini-sessions.provider.ts +++ b/server/modules/providers/list/gemini/gemini-sessions.provider.ts @@ -122,7 +122,9 @@ export class GeminiSessionsProvider implements IProviderSessions { rawMessages = sessionManager.getSessionMessages(sessionId) as AnyRecord[]; if (rawMessages.length === 0) { - rawMessages = await getGeminiCliSessionMessages(sessionId) as AnyRecord[]; + const session = sessionManager.getSession(sessionId); + const cliSessionId = (session as any)?.cliSessionId || sessionId; + rawMessages = await getGeminiCliSessionMessages(cliSessionId) as AnyRecord[]; } } catch (error) { const message = error instanceof Error ? error.message : String(error); From b481ad2ff513b8fff074b511f11fed07904358e8 Mon Sep 17 00:00:00 2001 From: Menny Even Danan Date: Wed, 22 Apr 2026 23:14:48 +0000 Subject: [PATCH 3/5] fix(gemini): resolve missing resume flag and prevent orphaned processes This fix addresses several interconnected issues in Gemini session management: 1. Prevent Orphaned Processes (Issue #1): now checks for and aborts any existing process for a session before starting a new one. This prevents multiple CLI processes from running concurrently for the same session. 2. Defensive Session Readiness (Issue #3): Added in to ensure session data is fully loaded from disk before attempting to look up . 3. Improved SIGKILL Logic (Issue #11): now verifies the specific process object is still active before sending SIGKILL, preventing stale key issues and accidental killing of replacement processes. 4. Diagnostic Logging: Added warnings in when a session is found but its is missing, making it easier to diagnose CLI initialization failures. These changes ensure the --resume flag is reliably passed and system resources are properly managed during rapid interactions. --- server/gemini-cli.js | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/server/gemini-cli.js b/server/gemini-cli.js index 2e68a9388f..72364dbd72 100644 --- a/server/gemini-cli.js +++ b/server/gemini-cli.js @@ -16,6 +16,16 @@ let activeGeminiProcesses = new Map(); // Track active processes by session ID async function spawnGemini(command, options = {}, ws) { const { sessionId, projectPath, cwd, toolsSettings, permissionMode, images, sessionSummary } = options; + + // Ensure session store is loaded (Issue #3) + await sessionManager.ready; + + // Abort existing process for this session to prevent orphans (Issue #1) + if (sessionId && activeGeminiProcesses.has(sessionId)) { + console.log(`[Gemini] Aborting existing process for session ${sessionId} before starting new one`); + abortGeminiSession(sessionId); + } + let capturedSessionId = sessionId; // Track session ID throughout the process let sessionCreatedSent = false; // Track if we've already sent session-created event let assistantBlocks = []; // Accumulate the full response blocks including tools @@ -40,6 +50,10 @@ async function spawnGemini(command, options = {}, ws) { const session = sessionManager.getSession(sessionId); if (session && session.cliSessionId) { args.push('--resume', session.cliSessionId); + } else if (session) { + console.warn(`[Gemini] Session ${sessionId} found but missing cliSessionId. Resuming without --resume.`); + } else { + console.warn(`[Gemini] Session ${sessionId} not found in sessionManager. Resuming without --resume.`); } } @@ -437,10 +451,21 @@ function abortGeminiSession(sessionId) { if (geminiProc) { try { geminiProc.kill('SIGTERM'); + const targetProc = geminiProc; // Capture for closure setTimeout(() => { - if (activeGeminiProcesses.has(processKey)) { + // Check if the exact same process is still in the map (by value, not just by key) + // This ensures we don't SIGKILL a NEW process that might have taken the same key + let isStillActive = false; + for (const proc of activeGeminiProcesses.values()) { + if (proc === targetProc) { + isStillActive = true; + break; + } + } + + if (isStillActive) { try { - geminiProc.kill('SIGKILL'); + targetProc.kill('SIGKILL'); } catch (e) { } } }, 2000); // Wait 2 seconds before force kill From afe6503ddfc687ddc372f31a1e9d17bbc24d423c Mon Sep 17 00:00:00 2001 From: Menny Even Danan Date: Thu, 23 Apr 2026 02:00:33 +0000 Subject: [PATCH 4/5] refactor(gemini): adopt Claude-style native session identifiers This refactoring eliminates duplicate Gemini sessions in the sidebar by aligning the session ID lifecycle with the Claude SDK implementation: 1. Native ID Capture: Shifted session initialization from the raw stdout handler to the structured 'onInit' event. The system now waits for the Gemini CLI to emit its native 'session_id' (e.g., chat_...) before creating the session in the session manager. 2. Unified Identifier: By using the native CLI ID as the primary key for both the UI and disk storage, we ensure that projects.js can correctly merge UI and CLI sessions based on a single, shared ID. 3. Fallback Logic: Maintained random-suffixed fallback ID generation for unstructured plain-text mode to ensure robustness. These changes root out the cause of duplicate sidebar entries and simplify session management across the Gemini provider. --- server/gemini-cli.js | 104 ++++++++++++++++++++++++++++++------------- 1 file changed, 72 insertions(+), 32 deletions(-) diff --git a/server/gemini-cli.js b/server/gemini-cli.js index 72364dbd72..3e9fa648aa 100644 --- a/server/gemini-cli.js +++ b/server/gemini-cli.js @@ -48,10 +48,13 @@ async function spawnGemini(command, options = {}, ws) { // If we have a sessionId, we want to resume if (sessionId) { const session = sessionManager.getSession(sessionId); - if (session && session.cliSessionId) { - args.push('--resume', session.cliSessionId); - } else if (session) { - console.warn(`[Gemini] Session ${sessionId} found but missing cliSessionId. Resuming without --resume.`); + if (session) { + // Use native ID for resumption: + // 1. If it's a legacy session, it has a cliSessionId field. + // 2. In the new unified architecture, the session ID itself is the native ID. + const resumeId = session.cliSessionId || session.id || sessionId; + args.push('--resume', resumeId); + console.log(`[Gemini] Resuming native session: ${resumeId}`); } else { console.warn(`[Gemini] Session ${sessionId} not found in sessionManager. Resuming without --resume.`); } @@ -290,12 +293,40 @@ async function spawnGemini(command, options = {}, ws) { } }, onInit: (event) => { - if (capturedSessionId) { - const sess = sessionManager.getSession(capturedSessionId); - if (sess && !sess.cliSessionId) { - sess.cliSessionId = event.session_id; - sessionManager.saveSession(capturedSessionId); + // Match Claude's implementation: wait for the native session_id before creating the UI session + if (event.session_id && !capturedSessionId) { + capturedSessionId = event.session_id; + sessionCreatedSent = true; + + // Create session in session manager using the native native ID + sessionManager.createSession(capturedSessionId, cwd || process.cwd()); + + // Save the user message now that we have the native session ID + if (command) { + sessionManager.addMessage(capturedSessionId, 'user', command); } + + // Re-key the active process map to use the native native ID + if (processKey !== capturedSessionId) { + activeGeminiProcesses.delete(processKey); + activeGeminiProcesses.set(capturedSessionId, geminiProcess); + geminiProcess.sessionId = capturedSessionId; + } + + // Inform the writer and client about the newly established native ID + if (ws) { + if (typeof ws.setSessionId === 'function') { + ws.setSessionId(capturedSessionId); + } + ws.send(createNormalizedMessage({ + kind: 'session_created', + newSessionId: capturedSessionId, + sessionId: capturedSessionId, + provider: 'gemini' + })); + } + + console.log(`[Gemini] Native session established: ${capturedSessionId}`); } } }); @@ -306,34 +337,43 @@ async function spawnGemini(command, options = {}, ws) { const rawOutput = data.toString(); startTimeout(); // Re-arm the timeout - // For new sessions, create a session ID FIRST - if (!sessionId && !sessionCreatedSent && !capturedSessionId) { - capturedSessionId = `gemini_${Date.now()}`; - sessionCreatedSent = true; - - // Create session in session manager - sessionManager.createSession(capturedSessionId, cwd || process.cwd()); + if (responseHandler) { + responseHandler.processData(rawOutput); + } else if (rawOutput) { + // Fallback for raw CLI mode without structured JSON streaming + if (!sessionId && !sessionCreatedSent && !capturedSessionId) { + capturedSessionId = `gemini_${Date.now()}`; + sessionCreatedSent = true; - // Save the user message now that we have a session ID - if (command) { - sessionManager.addMessage(capturedSessionId, 'user', command); - } + // Create session in session manager + sessionManager.createSession(capturedSessionId, cwd || process.cwd()); - // Update process key with captured session ID - if (processKey !== capturedSessionId) { - activeGeminiProcesses.delete(processKey); - activeGeminiProcesses.set(capturedSessionId, geminiProcess); - } + // Save the user message now that we have a fallback session ID + if (command) { + sessionManager.addMessage(capturedSessionId, 'user', command); + } - ws.setSessionId && typeof ws.setSessionId === 'function' && ws.setSessionId(capturedSessionId); + // Update process key + if (processKey !== capturedSessionId) { + activeGeminiProcesses.delete(processKey); + activeGeminiProcesses.set(capturedSessionId, geminiProcess); + geminiProcess.sessionId = capturedSessionId; + } - ws.send(createNormalizedMessage({ kind: 'session_created', newSessionId: capturedSessionId, sessionId: capturedSessionId, provider: 'gemini' })); - } + if (ws) { + if (typeof ws.setSessionId === 'function') { + ws.setSessionId(capturedSessionId); + } + ws.send(createNormalizedMessage({ + kind: 'session_created', + newSessionId: capturedSessionId, + sessionId: capturedSessionId, + provider: 'gemini' + })); + } + } - if (responseHandler) { - responseHandler.processData(rawOutput); - } else if (rawOutput) { - // Fallback to direct sending for raw CLI mode without WS + // Fallback direct streaming if (assistantBlocks.length > 0 && assistantBlocks[assistantBlocks.length - 1].type === 'text') { assistantBlocks[assistantBlocks.length - 1].text += rawOutput; } else { From 51125821266a00d9e1b4bea46b244a608e1a33b9 Mon Sep 17 00:00:00 2001 From: Menny Even Danan Date: Thu, 23 Apr 2026 02:10:01 +0000 Subject: [PATCH 5/5] fix(gemini): prevent double-fire and clear timeouts on process exit Implemented a 'settled' flag and 'settleOnce' helper in spawnGemini to ensure the process promise only resolves or rejects once. This prevents duplicate error messages from being sent to the client when both 'error' and 'close' events fire, and ensures that the execution timeout is properly cleared when the process finishes or fails. --- server/gemini-cli.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/server/gemini-cli.js b/server/gemini-cli.js index 3e9fa648aa..8cd214bbb5 100644 --- a/server/gemini-cli.js +++ b/server/gemini-cli.js @@ -186,6 +186,15 @@ async function spawnGemini(command, options = {}, ws) { } return new Promise((resolve, reject) => { + let settled = false; + + const settleOnce = (callback) => { + if (settled) return; + settled = true; + if (timeout) clearTimeout(timeout); + callback(); + }; + const geminiProcess = spawnFunction(spawnCmd, spawnArgs, { cwd: workingDir, stdio: ['pipe', 'pipe', 'pipe'], @@ -244,11 +253,12 @@ async function spawnGemini(command, options = {}, ws) { if (timeout) clearTimeout(timeout); timeout = setTimeout(() => { const socketSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : (capturedSessionId || sessionId || processKey); - terminalFailureReason = `Gemini CLI timeout - no response received for ${timeoutMs / 1000} seconds`; + terminalFailureReason = 'Gemini CLI process timed out'; ws.send(createNormalizedMessage({ kind: 'error', content: terminalFailureReason, sessionId: socketSessionId, provider: 'gemini' })); try { geminiProcess.kill('SIGTERM'); } catch (e) { } + settleOnce(() => reject(new Error(terminalFailureReason))); }, timeoutMs); }; @@ -433,7 +443,7 @@ async function spawnGemini(command, options = {}, ws) { if (code === 0) { notifyTerminalState({ code }); - resolve(); + settleOnce(() => resolve()); } else { // code 127 = shell "command not found" — check installation if (code === 127) { @@ -448,7 +458,7 @@ async function spawnGemini(command, options = {}, ws) { code, error: code === null ? 'Gemini CLI process was terminated or timed out' : null }); - reject(new Error(code === null ? 'Gemini CLI process was terminated or timed out' : `Gemini CLI exited with code ${code}`)); + settleOnce(() => reject(new Error(code === null ? 'Gemini CLI process was terminated or timed out' : `Gemini CLI exited with code ${code}`)))); } }); @@ -468,7 +478,7 @@ async function spawnGemini(command, options = {}, ws) { ws.send(createNormalizedMessage({ kind: 'error', content: errorContent, sessionId: errorSessionId, provider: 'gemini' })); notifyTerminalState({ error }); - reject(error); + settleOnce(() => reject(error)); }); });