From 5d12da2464465bf16d695dd252d66b9f88207b2c Mon Sep 17 00:00:00 2001 From: Amaan Bilwar Date: Thu, 9 Apr 2026 02:43:52 +0530 Subject: [PATCH 1/9] feat(cli): add sessionId to CliArgs --- packages/cli/src/config/config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 4e7e1db6f2..e0992051c0 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -94,6 +94,7 @@ export interface CliArgs { extensions: string[] | undefined; listExtensions: boolean | undefined; resume: string | typeof RESUME_LATEST | undefined; + sessionId: string | undefined; listSessions: boolean | undefined; deleteSession: string | undefined; includeDirectories: string[] | undefined; From 161342f6baf171b76e0750405769e52549046aab Mon Sep 17 00:00:00 2001 From: Amaan Bilwar Date: Thu, 9 Apr 2026 02:44:38 +0530 Subject: [PATCH 2/9] feat(cli): add --session-id CLI flag --- packages/cli/src/config/config.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index e0992051c0..292ab69da8 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -397,6 +397,11 @@ export async function parseArguments( return trimmed; }, }) + .option('session-id', { + type: 'string', + description: + 'Start a new session with a specific session ID (for deterministic orchestration).', + }) .option('list-sessions', { type: 'boolean', description: From d31cd85f0352182ff29136c1f04016ac0a88e027 Mon Sep 17 00:00:00 2001 From: Amaan Bilwar Date: Thu, 9 Apr 2026 02:45:25 +0530 Subject: [PATCH 3/9] feat(cli): validate --session-id and block --resume conflict --- packages/cli/src/config/config.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 292ab69da8..c17992dd43 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -243,6 +243,23 @@ export async function parseArguments( if (argv['yolo'] && argv['approvalMode']) { return 'Cannot use both --yolo (-y) and --approval-mode together. Use --approval-mode=yolo instead.'; } + if (argv['sessionId'] && argv['resume']) { + return 'Cannot use both --session-id and --resume together'; + } + + const providedSessionId = argv['sessionId']; + if (typeof providedSessionId === 'string') { + const trimmedSessionId = providedSessionId.trim(); + + if (!trimmedSessionId) { + return 'The --session-id flag requires a non-empty value'; + } + + if (trimmedSessionId === '.' || trimmedSessionId === '..') { + return 'Invalid --session-id value. "." and ".." are not allowed.'; + } + argv['sessionId'] = trimmedSessionId; + } const outputFormat = argv['outputFormat']; if ( From 0cd71a9f24541e3869e9bda520781a992460f976 Mon Sep 17 00:00:00 2001 From: Amaan Bilwar Date: Thu, 9 Apr 2026 02:46:30 +0530 Subject: [PATCH 4/9] fix(cli): bootstrap config with provided session id --- packages/cli/src/gemini.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index eedfcc950a..227477280d 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -319,7 +319,9 @@ export async function main() { const argv = await argvPromise; - const { sessionId, resumedSessionData } = await resolveSessionId(argv.resume); + const { sessionId: resolvedSessionId, resumedSessionData } = + await resolveSessionId(argv.resume); + const sessionId = argv.sessionId ?? resolvedSessionId; if ( (argv.allowedTools && argv.allowedTools.length > 0) || From d6562e0d57b746c6c7ed08f704adbee42f83ebd6 Mon Sep 17 00:00:00 2001 From: Amaan Bilwar Date: Thu, 9 Apr 2026 02:46:59 +0530 Subject: [PATCH 5/9] fix(cli): use active config session id in non interactive flow --- packages/cli/src/gemini.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index 227477280d..ce8a65e939 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -711,7 +711,7 @@ export async function main() { process.exit(ExitCodes.FATAL_INPUT_ERROR); } - const prompt_id = sessionId; + const prompt_id = config.getSessionId(); logUserPrompt( config, new UserPromptEvent( @@ -731,7 +731,7 @@ export async function main() { await config.refreshAuth(authType); if (config.getDebugMode()) { - debugLogger.log('Session ID: %s', sessionId); + debugLogger.log('Session ID: %s', config.getSessionId()); } initializeOutputListenersAndFlush(); From a6adcf37a21f41ec33fb700b2612e354d8b952ba Mon Sep 17 00:00:00 2001 From: Amaan Bilwar Date: Thu, 9 Apr 2026 02:48:00 +0530 Subject: [PATCH 6/9] test(cli):add sessionId to parseArguments mocks --- packages/cli/src/gemini.test.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/gemini.test.tsx b/packages/cli/src/gemini.test.tsx index 5b31d153fe..0918eb9804 100644 --- a/packages/cli/src/gemini.test.tsx +++ b/packages/cli/src/gemini.test.tsx @@ -547,6 +547,7 @@ describe('gemini.tsx main function kitty protocol', () => { screenReader: undefined, useWriteTodos: undefined, resume: undefined, + sessionId: undefined, listSessions: undefined, deleteSession: undefined, outputFormat: undefined, @@ -605,6 +606,7 @@ describe('gemini.tsx main function kitty protocol', () => { screenReader: undefined, useWriteTodos: undefined, resume: undefined, + sessionId: undefined, listSessions: undefined, deleteSession: undefined, outputFormat: undefined, @@ -876,9 +878,8 @@ describe('gemini.tsx main function kitty protocol', () => { }); it('should start normally with a warning when no sessions found for resume', async () => { - const { SessionSelector, SessionError } = await import( - './utils/sessionUtils.js' - ); + const { SessionSelector, SessionError } = + await import('./utils/sessionUtils.js'); vi.mocked(SessionSelector).mockImplementation( () => ({ @@ -933,9 +934,8 @@ describe('gemini.tsx main function kitty protocol', () => { }); it.skip('should log error when cleanupExpiredSessions fails', async () => { - const { cleanupExpiredSessions } = await import( - './utils/sessionCleanup.js' - ); + const { cleanupExpiredSessions } = + await import('./utils/sessionCleanup.js'); vi.mocked(cleanupExpiredSessions).mockRejectedValue( new Error('Cleanup failed'), ); From 028386a1a319b6fba0e4c9ccb105ccd9969b9c96 Mon Sep 17 00:00:00 2001 From: Amaan Bilwar Date: Thu, 9 Apr 2026 03:10:38 +0530 Subject: [PATCH 7/9] docs: update `--session-id` information in cli-reference docs --- docs/cli/cli-reference.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/cli/cli-reference.md b/docs/cli/cli-reference.md index e8217e226e..82b14fda21 100644 --- a/docs/cli/cli-reference.md +++ b/docs/cli/cli-reference.md @@ -60,7 +60,8 @@ These commands are available within the interactive REPL. | `--allowed-tools` | - | array | - | **Deprecated.** Use the [Policy Engine](../reference/policy-engine.md) instead. Tools that are allowed to run without confirmation (comma-separated or multiple flags) | | `--extensions` | `-e` | array | - | List of extensions to use. If not provided, all extensions are enabled (comma-separated or multiple flags) | | `--list-extensions` | `-l` | boolean | - | List all available extensions and exit | -| `--resume` | `-r` | string | - | Resume a previous session. Use `"latest"` for most recent or index number (for example `--resume 5`) | +| `--resume` | `-r` | string | - | Resume a previous session. Use `"latest"` for most recent or index number (e.g. `--resume 5`) | +| `--session-id` | - | string | - | Start a new session with a specific session ID for deterministic orchestration. Cannot be combined with `--resume`. | | `--list-sessions` | - | boolean | - | List available sessions for the current project and exit | | `--delete-session` | - | string | - | Delete a session by index number (use `--list-sessions` to see available sessions) | | `--include-directories` | - | array | - | Additional directories to include in the workspace (comma-separated or multiple flags) | From 9492422f28fa048cf18836b5c2e501b9384baa04 Mon Sep 17 00:00:00 2001 From: Amaan Bilwar Date: Thu, 9 Apr 2026 15:50:19 +0530 Subject: [PATCH 8/9] test(cli): add `--session-id` parser & startup wiring tests --- packages/cli/src/config/config.test.ts | 70 ++++++++++++++++++++++++++ packages/cli/src/gemini.test.tsx | 61 ++++++++++++++++++++++ 2 files changed, 131 insertions(+) diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index 04df366a98..1c53044b51 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -657,6 +657,76 @@ describe('parseArguments', () => { } }); + it('should parse --session-id', async () => { + process.argv = ['node', 'script.js', '--session-id', 'my-session-123']; + + const argv = await parseArguments(createTestMergedSettings()); + expect(argv.sessionId).toBe('my-session-123'); + }); + + it('should trim --session-id value', async () => { + process.argv = ['node', 'script.js', '--session-id', ' my-session-123 ']; + + const argv = await parseArguments(createTestMergedSettings()); + expect(argv.sessionId).toBe('my-session-123'); + }); + + it('should throw an error when using --session-id with --resume', async () => { + process.argv = [ + 'node', + 'script.js', + '--session-id', + 'my-session-123', + '--resume', + 'latest', + ]; + + const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => { + throw new Error('process.exit called'); + }); + + const mockConsoleError = vi + .spyOn(console, 'error') + .mockImplementation(() => {}); + + await expect(parseArguments(createTestMergedSettings())).rejects.toThrow( + 'process.exit called', + ); + + expect(mockConsoleError).toHaveBeenCalledWith( + expect.stringContaining( + 'Cannot use both --session-id and --resume together', + ), + ); + + mockExit.mockRestore(); + mockConsoleError.mockRestore(); + }); + + it.each(['.', '..', ' '])( + 'should reject invalid --session-id value "%s"', + async (badSessionId) => { + process.argv = ['node', 'script.js', '--session-id', badSessionId]; + + const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => { + throw new Error('process.exit called'); + }); + + const mockConsoleError = vi + .spyOn(console, 'error') + .mockImplementation(() => {}); + + await expect(parseArguments(createTestMergedSettings())).rejects.toThrow( + 'process.exit called', + ); + + expect(mockConsoleError).toHaveBeenCalled(); + + mockExit.mockRestore(); + mockConsoleError.mockRestore(); + }, + ); + it('should support comma-separated values for --allowed-tools', async () => { process.argv = [ 'node', diff --git a/packages/cli/src/gemini.test.tsx b/packages/cli/src/gemini.test.tsx index 0918eb9804..27e230b173 100644 --- a/packages/cli/src/gemini.test.tsx +++ b/packages/cli/src/gemini.test.tsx @@ -625,6 +625,67 @@ describe('gemini.tsx main function kitty protocol', () => { resumeSpy.mockRestore(); }); + it('should pass provided --session-id to loadCliConfig', async () => { + vi.mocked(loadCliConfig).mockResolvedValue( + createMockConfig({ + isInteractive: () => true, + getQuestion: () => '', + getSandbox: () => undefined, + }), + ); + vi.mocked(loadSettings).mockReturnValue( + createMockSettings({ + merged: { + advanced: {}, + security: { auth: {} }, + ui: {}, + }, + }), + ); + vi.mocked(parseArguments).mockResolvedValue({ + model: undefined, + sandbox: undefined, + debug: undefined, + prompt: undefined, + promptInteractive: undefined, + query: undefined, + yolo: undefined, + approvalMode: undefined, + policy: undefined, + adminPolicy: undefined, + allowedMcpServerNames: undefined, + allowedTools: undefined, + experimentalAcp: undefined, + extensions: undefined, + listExtensions: undefined, + includeDirectories: undefined, + screenReader: undefined, + useWriteTodos: undefined, + resume: undefined, + sessionId: 'fixed-session-id', + listSessions: undefined, + deleteSession: undefined, + outputFormat: undefined, + fakeResponses: undefined, + recordResponses: undefined, + rawOutput: undefined, + acceptRawOutputRisk: undefined, + isCommand: undefined, + }); + + await act(async () => { + await main(); + }); + + const sessionIdsPassedToConfig = vi + .mocked(loadCliConfig) + .mock.calls.map((call) => call[1]); + expect(sessionIdsPassedToConfig.length).toBeGreaterThan(0); + expect( + sessionIdsPassedToConfig.every((id) => id === 'fixed-session-id'), + ).toBe(true); + }); + it.each([ { flag: 'listExtensions' }, { flag: 'listSessions' }, From 1ab411addd00be7cfcbedf1b40641ec09e1a5407 Mon Sep 17 00:00:00 2001 From: Amaan Bilwar Date: Thu, 9 Apr 2026 16:54:02 +0530 Subject: [PATCH 9/9] fix(cli): stricter path validation to only permit safe characters permitted characters: alphanumeric characters, hyphens, and underscores --- packages/cli/src/config/config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index c17992dd43..9779000f91 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -255,8 +255,8 @@ export async function parseArguments( return 'The --session-id flag requires a non-empty value'; } - if (trimmedSessionId === '.' || trimmedSessionId === '..') { - return 'Invalid --session-id value. "." and ".." are not allowed.'; + if (!/^[a-zA-Z0-9_-]+$/.test(trimmedSessionId)) { + return 'Invalid --session-id value. Only alphanumeric characters, hyphens, and underscores are allowed.'; } argv['sessionId'] = trimmedSessionId; }