From 461903ca85e9c308fbdff44aef9e00da5c37604f Mon Sep 17 00:00:00 2001 From: Junmo Kim Date: Tue, 31 Mar 2026 01:57:04 +0900 Subject: [PATCH 1/2] fix(gemini): wire --resume flag through to Gemini backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The --resume flag sent by the runner was silently ignored because the gemini command did not parse it. This caused remote Gemini sessions to always start fresh instead of resuming the previous conversation context. Thread resumeSessionId through commands/gemini.ts → runGemini → geminiLoop → GeminiSession, mirroring the existing OpenCode implementation. When a session ID is present, use ACP loadSession instead of newSession to restore the previous session state, with fallback to newSession on failure. Remove the now-redundant resumeSessionId from createGeminiBackend to avoid double-resume (CLI --resume flag + ACP session/load). --- cli/src/commands/gemini.ts | 7 ++++++ cli/src/gemini/geminiRemoteLauncher.ts | 32 ++++++++++++++++++++++---- cli/src/gemini/loop.ts | 7 +++++- cli/src/gemini/runGemini.test.ts | 22 ++++++++++++++++++ cli/src/gemini/runGemini.ts | 2 ++ 5 files changed, 64 insertions(+), 6 deletions(-) diff --git a/cli/src/commands/gemini.ts b/cli/src/commands/gemini.ts index 964bdb7e9..09b4f95f6 100644 --- a/cli/src/commands/gemini.ts +++ b/cli/src/commands/gemini.ts @@ -15,6 +15,7 @@ export const geminiCommand: CommandDefinition = { startingMode?: 'local' | 'remote' permissionMode?: GeminiPermissionMode model?: string + resumeSessionId?: string } = {} for (let i = 0; i < commandArgs.length; i++) { @@ -30,6 +31,12 @@ export const geminiCommand: CommandDefinition = { } } else if (arg === '--yolo') { options.permissionMode = 'yolo' + } else if (arg === '--resume') { + const sessionId = commandArgs[++i] + if (!sessionId) { + throw new Error('Missing --resume value') + } + options.resumeSessionId = sessionId } else if (arg === '--model') { const model = commandArgs[++i] if (!model) { diff --git a/cli/src/gemini/geminiRemoteLauncher.ts b/cli/src/gemini/geminiRemoteLauncher.ts index 47e631798..68f306899 100644 --- a/cli/src/gemini/geminiRemoteLauncher.ts +++ b/cli/src/gemini/geminiRemoteLauncher.ts @@ -54,7 +54,6 @@ class GeminiRemoteLauncher extends RemoteLauncherBase { const backend = createGeminiBackend({ model: runtimeConfig.model, token: runtimeConfig.token, - resumeSessionId: session.sessionId, hookSettingsPath: this.hookSettingsPath, cwd: session.path, permissionMode: session.getPermissionMode() as string | undefined @@ -69,10 +68,33 @@ class GeminiRemoteLauncher extends RemoteLauncherBase { await backend.initialize(); - const acpSessionId = await backend.newSession({ - cwd: session.path, - mcpServers: toAcpMcpServers(mcpServers) - }); + const resumeSessionId = session.sessionId; + const acpMcpServers = toAcpMcpServers(mcpServers); + let acpSessionId: string; + if (resumeSessionId) { + try { + acpSessionId = await backend.loadSession({ + sessionId: resumeSessionId, + cwd: session.path, + mcpServers: acpMcpServers + }); + } catch (error) { + logger.warn('[gemini-remote] resume failed, starting new session', error); + session.sendSessionEvent({ + type: 'message', + message: 'Gemini resume failed; starting a new session.' + }); + acpSessionId = await backend.newSession({ + cwd: session.path, + mcpServers: acpMcpServers + }); + } + } else { + acpSessionId = await backend.newSession({ + cwd: session.path, + mcpServers: acpMcpServers + }); + } session.onSessionFound(acpSessionId); this.permissionHandler = new GeminiPermissionHandler( diff --git a/cli/src/gemini/loop.ts b/cli/src/gemini/loop.ts index d234200ec..c913944c0 100644 --- a/cli/src/gemini/loop.ts +++ b/cli/src/gemini/loop.ts @@ -19,6 +19,7 @@ interface GeminiLoopOptions { model?: string; hookSettingsPath?: string; allowedTools?: string[]; + resumeSessionId?: string; onSessionReady?: (session: GeminiSession) => void; } @@ -31,7 +32,7 @@ export async function geminiLoop(opts: GeminiLoopOptions): Promise { api: opts.api, client: opts.session, path: opts.path, - sessionId: null, + sessionId: opts.resumeSessionId ?? null, logPath, messageQueue: opts.messageQueue, onModeChange: opts.onModeChange, @@ -41,6 +42,10 @@ export async function geminiLoop(opts: GeminiLoopOptions): Promise { permissionMode: opts.permissionMode ?? 'default' }); + if (opts.resumeSessionId) { + session.onSessionFound(opts.resumeSessionId); + } + await runLocalRemoteSession({ session, startingMode: opts.startingMode, diff --git a/cli/src/gemini/runGemini.test.ts b/cli/src/gemini/runGemini.test.ts index be216e333..2069eeffa 100644 --- a/cli/src/gemini/runGemini.test.ts +++ b/cli/src/gemini/runGemini.test.ts @@ -106,4 +106,26 @@ describe('runGemini', () => { expect(harness.bootstrapArgs[0]?.model).toBeUndefined(); expect(harness.geminiLoopArgs[0]?.model).toBe('gemini-2.5-pro'); }); + + it('passes resumeSessionId through to geminiLoop', async () => { + resolveGeminiRuntimeConfigMock.mockReturnValue({ + model: 'gemini-2.5-pro', + modelSource: 'default' + }); + + await runGemini({ resumeSessionId: 'a6157ffa-f692-4b73-82d5-63d42177f4f9' }); + + expect(harness.geminiLoopArgs[0]?.resumeSessionId).toBe('a6157ffa-f692-4b73-82d5-63d42177f4f9'); + }); + + it('does not set resumeSessionId when not provided', async () => { + resolveGeminiRuntimeConfigMock.mockReturnValue({ + model: 'gemini-2.5-pro', + modelSource: 'default' + }); + + await runGemini({}); + + expect(harness.geminiLoopArgs[0]?.resumeSessionId).toBeUndefined(); + }); }); diff --git a/cli/src/gemini/runGemini.ts b/cli/src/gemini/runGemini.ts index 704d440b4..9c2a9c1a8 100644 --- a/cli/src/gemini/runGemini.ts +++ b/cli/src/gemini/runGemini.ts @@ -21,6 +21,7 @@ export async function runGemini(opts: { startingMode?: 'local' | 'remote'; permissionMode?: PermissionMode; model?: string; + resumeSessionId?: string; } = {}): Promise { const workingDirectory = getInvokedCwd(); const startedBy = opts.startedBy ?? 'terminal'; @@ -149,6 +150,7 @@ export async function runGemini(opts: { permissionMode: currentPermissionMode, model: resolvedModel, hookSettingsPath, + resumeSessionId: opts.resumeSessionId, onModeChange: createModeChangeHandler(session), onSessionReady: (instance) => { sessionWrapperRef.current = instance; From 6680e671cfe8d3deeefc711a8645fcc44a1751a2 Mon Sep 17 00:00:00 2001 From: Junmo Kim Date: Tue, 31 Mar 2026 01:57:15 +0900 Subject: [PATCH 2/2] fix(gemini): log prompt error message instead of empty object Error objects serialize to {} in JSON, making prompt failures impossible to diagnose from logs. Extract the error message before logging and surface it to the session UI, consistent with how stderr errors are already handled in the same file. --- cli/src/gemini/geminiRemoteLauncher.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cli/src/gemini/geminiRemoteLauncher.ts b/cli/src/gemini/geminiRemoteLauncher.ts index 68f306899..40b8ffa61 100644 --- a/cli/src/gemini/geminiRemoteLauncher.ts +++ b/cli/src/gemini/geminiRemoteLauncher.ts @@ -137,12 +137,13 @@ class GeminiRemoteLauncher extends RemoteLauncherBase { this.handleAgentMessage(message); }); } catch (error) { - logger.warn('[gemini-remote] prompt failed', error); + const errorMessage = error instanceof Error ? error.message : String(error); + logger.warn('[gemini-remote] prompt failed', { message: errorMessage }); session.sendSessionEvent({ type: 'message', - message: 'Gemini prompt failed. Check logs for details.' + message: `Gemini prompt failed: ${errorMessage}` }); - messageBuffer.addMessage('Gemini prompt failed', 'status'); + messageBuffer.addMessage(`Gemini prompt failed: ${errorMessage}`, 'status'); } finally { session.onThinkingChange(false); await this.permissionHandler?.cancelAll('Prompt finished');