From 395af2a1e40e63e73200885be2c7ef549359e3c1 Mon Sep 17 00:00:00 2001 From: Jack Wotherspoon Date: Mon, 9 Mar 2026 14:24:20 +0100 Subject: [PATCH] chore: update restart to use env var --- packages/cli/src/gemini.tsx | 21 +++++-- packages/cli/src/utils/processUtils.test.ts | 32 ++++------ packages/cli/src/utils/processUtils.ts | 7 +-- packages/cli/src/utils/relaunch.test.ts | 68 +++------------------ packages/cli/src/utils/relaunch.ts | 23 ++----- 5 files changed, 47 insertions(+), 104 deletions(-) diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index 3bed5335a6..60813ed3ba 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -693,12 +693,19 @@ export async function main() { })), ]; - // Handle --resume flag + // Handle session resume — either from explicit --resume flag or from + // auto-restart via GEMINI_RESUME_SESSION_ID env var. + const resumeArg = argv.resume ?? process.env['GEMINI_RESUME_SESSION_ID']; + const isAutoRestart = + !argv.resume && !!process.env['GEMINI_RESUME_SESSION_ID']; + // Clean up the env var so it doesn't leak to further restarts + delete process.env['GEMINI_RESUME_SESSION_ID']; + let resumedSessionData: ResumedSessionData | undefined = undefined; - if (argv.resume) { + if (resumeArg) { const sessionSelector = new SessionSelector(config); try { - const result = await sessionSelector.resolveSession(argv.resume); + const result = await sessionSelector.resolveSession(resumeArg); resumedSessionData = { conversation: result.sessionData, filePath: result.sessionPath, @@ -706,14 +713,18 @@ export async function main() { // Use the existing session ID to continue recording to the same session config.setSessionId(resumedSessionData.conversation.sessionId); } catch (error) { - if ( + if (error instanceof SessionError && isAutoRestart) { + // Auto-restart tried to resume a session that doesn't exist on + // disk yet (e.g. empty session with no messages). Silently start + // a new session. + } else if ( error instanceof SessionError && error.code === 'NO_SESSIONS_FOUND' ) { // No sessions to resume — start a fresh session with a warning startupWarnings.push({ id: 'resume-failure', - message: 'Could not resume session. Started a new session.', + message: `${error.message} Started a new session.`, priority: WarningPriority.High, }); } else { diff --git a/packages/cli/src/utils/processUtils.test.ts b/packages/cli/src/utils/processUtils.test.ts index a79f93a2b1..487c702dc5 100644 --- a/packages/cli/src/utils/processUtils.test.ts +++ b/packages/cli/src/utils/processUtils.test.ts @@ -22,33 +22,27 @@ describe('processUtils', () => { .spyOn(process, 'exit') .mockReturnValue(undefined as never); const runExitCleanup = vi.spyOn(cleanup, 'runExitCleanup'); + const originalSend = process.send; beforeEach(() => { _resetRelaunchStateForTesting(); + process.send = vi.fn(); }); - afterEach(() => vi.clearAllMocks()); - - it('should wait for updates, run cleanup, send resume session ID, and exit with the relaunch code', async () => { - const originalSend = process.send; - process.send = vi.fn(); - - await relaunchApp(); - expect(handleAutoUpdate.waitForUpdateCompletion).toHaveBeenCalledTimes(1); - expect(runExitCleanup).toHaveBeenCalledTimes(1); - expect(process.send).toHaveBeenCalledWith({ - type: 'relaunch-session', - sessionId: expect.any(String), - }); - expect(processExit).toHaveBeenCalledWith(RELAUNCH_EXIT_CODE); - + afterEach(() => { + vi.clearAllMocks(); process.send = originalSend; }); - it('should wait for updates, run cleanup, send override resume session ID, and exit with the relaunch code', async () => { - const originalSend = process.send; - process.send = vi.fn(); + it('should not send IPC message when no sessionId is provided', async () => { + await relaunchApp(); + expect(handleAutoUpdate.waitForUpdateCompletion).toHaveBeenCalledTimes(1); + expect(runExitCleanup).toHaveBeenCalledTimes(1); + expect(process.send).not.toHaveBeenCalled(); + expect(processExit).toHaveBeenCalledWith(RELAUNCH_EXIT_CODE); + }); + it('should send resume session ID via IPC when sessionId is provided', async () => { await relaunchApp('custom-session-id'); expect(handleAutoUpdate.waitForUpdateCompletion).toHaveBeenCalledTimes(1); expect(runExitCleanup).toHaveBeenCalledTimes(1); @@ -57,7 +51,5 @@ describe('processUtils', () => { sessionId: 'custom-session-id', }); expect(processExit).toHaveBeenCalledWith(RELAUNCH_EXIT_CODE); - - process.send = originalSend; }); }); diff --git a/packages/cli/src/utils/processUtils.ts b/packages/cli/src/utils/processUtils.ts index 3c2ce1a1f8..9ebb5c2730 100644 --- a/packages/cli/src/utils/processUtils.ts +++ b/packages/cli/src/utils/processUtils.ts @@ -6,7 +6,6 @@ import { runExitCleanup } from './cleanup.js'; import { waitForUpdateCompletion } from './handleAutoUpdate.js'; -import { sessionId } from '@google/gemini-cli-core'; /** * Exit code used to signal that the CLI should be relaunched. @@ -23,16 +22,16 @@ export function _resetRelaunchStateForTesting(): void { isRelaunching = false; } -export async function relaunchApp(sessionIdOverride?: string): Promise { +export async function relaunchApp(sessionId?: string): Promise { if (isRelaunching) return; isRelaunching = true; await waitForUpdateCompletion(); await runExitCleanup(); - if (process.send) { + if (process.send && sessionId) { process.send({ type: 'relaunch-session', - sessionId: sessionIdOverride ?? sessionId, + sessionId, }); } diff --git a/packages/cli/src/utils/relaunch.test.ts b/packages/cli/src/utils/relaunch.test.ts index ba545d1876..a1ab43b15f 100644 --- a/packages/cli/src/utils/relaunch.test.ts +++ b/packages/cli/src/utils/relaunch.test.ts @@ -316,7 +316,7 @@ describe('relaunchAppInChildProcess', () => { expect(processExitSpy).toHaveBeenCalledWith(1); }); - it('should append --resume on the next spawn if relaunch-session message is received', async () => { + it('should set GEMINI_RESUME_SESSION_ID env var on the next spawn if relaunch-session message is received', async () => { process.argv = ['/usr/bin/node', '/app/cli.js', '--some-flag']; let spawnCount = 0; @@ -348,65 +348,17 @@ describe('relaunchAppInChildProcess', () => { expect(mockedSpawn).toHaveBeenCalledTimes(2); - // First spawn should not have --resume - expect(mockedSpawn.mock.calls[0][1]).not.toContain('--resume'); + // First spawn should not have the env var set + const firstEnv = mockedSpawn.mock.calls[0][2]?.env; + expect(firstEnv?.['GEMINI_RESUME_SESSION_ID']).toBeUndefined(); - // Second spawn should have --resume test-session-123 appended + // Second spawn should have GEMINI_RESUME_SESSION_ID set + const secondEnv = mockedSpawn.mock.calls[1][2]?.env; + expect(secondEnv?.['GEMINI_RESUME_SESSION_ID']).toBe('test-session-123'); + + // Args should not contain --resume const secondArgs = mockedSpawn.mock.calls[1][1]; - expect(secondArgs).toContain('--resume'); - expect(secondArgs).toContain('test-session-123'); - - // Check exact order at the end of arguments - expect(secondArgs.slice(-2)).toEqual(['--resume', 'test-session-123']); - }); - - it('should strip existing --resume flags when appending new one', async () => { - process.argv = [ - '/usr/bin/node', - '/app/cli.js', - '--resume', - 'old-session', - '--resume=other-session', - '--flag', - ]; - - let spawnCount = 0; - mockedSpawn.mockImplementation(() => { - spawnCount++; - const mockChild = createMockChildProcess(0, false); - - if (spawnCount === 1) { - setImmediate(() => { - mockChild.emit('message', { - type: 'relaunch-session', - sessionId: 'new-session-456', - }); - mockChild.emit('close', RELAUNCH_EXIT_CODE); - }); - } else if (spawnCount === 2) { - setImmediate(() => { - mockChild.emit('close', 0); - }); - } - - return mockChild; - }); - - const promise = relaunchAppInChildProcess([], []); - await expect(promise).rejects.toThrow('PROCESS_EXIT_CALLED'); - - expect(mockedSpawn).toHaveBeenCalledTimes(2); - - const secondArgs = mockedSpawn.mock.calls[1][1] as string[]; - - // Should not contain the old resumes - expect(secondArgs).not.toContain('old-session'); - expect(secondArgs).not.toContain('--resume=other-session'); - - // Should contain the new resume at the end - expect(secondArgs.slice(-2)).toEqual(['--resume', 'new-session-456']); - // Should still contain other flags - expect(secondArgs).toContain('--flag'); + expect(secondArgs).not.toContain('--resume'); }); }); }); diff --git a/packages/cli/src/utils/relaunch.ts b/packages/cli/src/utils/relaunch.ts index b69ee51c4c..ad690acd79 100644 --- a/packages/cli/src/utils/relaunch.ts +++ b/packages/cli/src/utils/relaunch.ts @@ -47,22 +47,7 @@ export async function relaunchAppInChildProcess( // process.argv is [node, script, ...args] // We want to construct [ ...nodeArgs, script, ...scriptArgs] const script = process.argv[1]; - let scriptArgs = process.argv.slice(2); - - if (resumeSessionId) { - const filteredArgs: string[] = []; - for (let i = 0; i < scriptArgs.length; i++) { - if (scriptArgs[i] === '--resume') { - i++; // Skip the next argument as well - continue; - } - if (scriptArgs[i].startsWith('--resume=')) { - continue; - } - filteredArgs.push(scriptArgs[i]); - } - scriptArgs = [...filteredArgs, '--resume', resumeSessionId]; - } + const scriptArgs = process.argv.slice(2); const nodeArgs = [ ...process.execArgv, @@ -71,7 +56,11 @@ export async function relaunchAppInChildProcess( ...additionalScriptArgs, ...scriptArgs, ]; - const newEnv = { ...process.env, GEMINI_CLI_NO_RELAUNCH: 'true' }; + const newEnv: Record = { + ...process.env, + GEMINI_CLI_NO_RELAUNCH: 'true', + GEMINI_RESUME_SESSION_ID: resumeSessionId, + }; // The parent process should not be reading from stdin while the child is running. process.stdin.pause();