diff --git a/docs/cli/settings.md b/docs/cli/settings.md index dbb3651a4f..88a5d2ff83 100644 --- a/docs/cli/settings.md +++ b/docs/cli/settings.md @@ -153,9 +153,9 @@ they appear in the UI. ### Advanced -| UI Label | Setting | Description | Default | -| --------------------------------- | ------------------------------ | --------------------------------------------- | ------- | -| Auto Configure Max Old Space Size | `advanced.autoConfigureMemory` | Automatically configure Node.js memory limits | `true` | +| UI Label | Setting | Description | Default | +| --------------------------------- | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | +| Auto Configure Max Old Space Size | `advanced.autoConfigureMemory` | Automatically configure Node.js memory limits. Note: Because memory is allocated during the initial process boot, this setting is only read from the global user settings file and ignores workspace-level overrides. | `true` | ### Experimental diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 1fdbc755f0..f10336a0d9 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -1578,7 +1578,10 @@ their corresponding top-level category object in your `settings.json` file. #### `advanced` - **`advanced.autoConfigureMemory`** (boolean): - - **Description:** Automatically configure Node.js memory limits + - **Description:** Automatically configure Node.js memory limits. Note: + Because memory is allocated during the initial process boot, this setting is + only read from the global user settings file and ignores workspace-level + overrides. - **Default:** `true` - **Requires restart:** Yes diff --git a/packages/cli/index.ts b/packages/cli/index.ts index d94a2dd191..d857831fb7 100644 --- a/packages/cli/index.ts +++ b/packages/cli/index.ts @@ -6,9 +6,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { main } from './src/gemini.js'; -import { FatalError, writeToStderr } from '@google/gemini-cli-core'; -import { runExitCleanup } from './src/utils/cleanup.js'; +import { spawn } from 'node:child_process'; +import os from 'node:os'; +import v8 from 'node:v8'; // --- Global Entry Point --- @@ -28,44 +28,162 @@ process.on('uncaughtException', (error) => { // For other errors, we rely on the default behavior, but since we attached a listener, // we must manually replicate it. if (error instanceof Error) { - writeToStderr(error.stack + '\n'); + process.stderr.write(error.stack + '\n'); } else { - writeToStderr(String(error) + '\n'); + process.stderr.write(String(error) + '\n'); } process.exit(1); }); -main().catch(async (error) => { - // Set a timeout to force exit if cleanup hangs - const cleanupTimeout = setTimeout(() => { - writeToStderr('Cleanup timed out, forcing exit...\n'); - process.exit(1); - }, 5000); - +async function getMemoryNodeArgs(): Promise { + let autoConfigureMemory = true; try { - await runExitCleanup(); - } catch (cleanupError) { - writeToStderr( - `Error during final cleanup: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}\n`, - ); - } finally { - clearTimeout(cleanupTimeout); - } - - if (error instanceof FatalError) { - let errorMessage = error.message; - if (!process.env['NO_COLOR']) { - errorMessage = `\x1b[31m${errorMessage}\x1b[0m`; + const { readFileSync } = await import('node:fs'); + const { join } = await import('node:path'); + // Respect GEMINI_CLI_HOME environment variable, falling back to os.homedir() + const baseDir = + process.env['GEMINI_CLI_HOME'] || join(os.homedir(), '.gemini'); + const settingsPath = join(baseDir, 'settings.json'); + const rawSettings = readFileSync(settingsPath, 'utf8'); + const settings = JSON.parse(rawSettings); + if (settings?.advanced?.autoConfigureMemory === false) { + autoConfigureMemory = false; } - writeToStderr(errorMessage + '\n'); - process.exit(error.exitCode); + } catch { + // ignore } - writeToStderr('An unexpected critical error occurred:'); - if (error instanceof Error) { - writeToStderr(error.stack + '\n'); - } else { - writeToStderr(String(error) + '\n'); + if (autoConfigureMemory) { + const totalMemoryMB = os.totalmem() / (1024 * 1024); + const heapStats = v8.getHeapStatistics(); + const currentMaxOldSpaceSizeMb = Math.floor( + heapStats.heap_size_limit / 1024 / 1024, + ); + const targetMaxOldSpaceSizeInMB = Math.floor(totalMemoryMB * 0.5); + + if (targetMaxOldSpaceSizeInMB > currentMaxOldSpaceSizeMb) { + return [`--max-old-space-size=${targetMaxOldSpaceSizeInMB}`]; + } } - process.exit(1); -}); + + return []; +} + +async function run() { + if (!process.env['GEMINI_CLI_NO_RELAUNCH'] && !process.env['SANDBOX']) { + // --- Lightweight Parent Process / Daemon --- + // We avoid importing heavy dependencies here to save ~1.5s of startup time. + + const nodeArgs: string[] = [...process.execArgv]; + const scriptArgs = process.argv.slice(2); + + const memoryArgs = await getMemoryNodeArgs(); + nodeArgs.push(...memoryArgs); + + const script = process.argv[1]; + nodeArgs.push(script); + nodeArgs.push(...scriptArgs); + + const newEnv = { ...process.env, GEMINI_CLI_NO_RELAUNCH: 'true' }; + const RELAUNCH_EXIT_CODE = 199; + let latestAdminSettings: unknown = undefined; + + // Prevent the parent process from exiting prematurely on signals. + // The child process will receive the same signals and handle its own cleanup. + for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) { + process.on(sig as NodeJS.Signals, () => {}); + } + + const runner = () => { + process.stdin.pause(); + + const child = spawn(process.execPath, nodeArgs, { + stdio: ['inherit', 'inherit', 'inherit', 'ipc'], + env: newEnv, + }); + + if (latestAdminSettings) { + child.send({ type: 'admin-settings', settings: latestAdminSettings }); + } + + child.on('message', (msg: { type?: string; settings?: unknown }) => { + if (msg.type === 'admin-settings-update' && msg.settings) { + latestAdminSettings = msg.settings; + } + }); + + return new Promise((resolve) => { + child.on('error', (err) => { + process.stderr.write( + 'Error: Failed to start child process: ' + err.message + '\n', + ); + resolve(1); + }); + child.on('close', (code) => { + process.stdin.resume(); + resolve(code ?? 1); + }); + }); + }; + + while (true) { + try { + const exitCode = await runner(); + if (exitCode !== RELAUNCH_EXIT_CODE) { + process.exit(exitCode); + } + } catch (error: unknown) { + process.stdin.resume(); + process.stderr.write( + `Fatal error: Failed to relaunch the CLI process.\n${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`, + ); + process.exit(1); + } + } + } else { + // --- Heavy Child Process --- + // Now we can safely import everything. + const { main } = await import('./src/gemini.js'); + const { FatalError, writeToStderr } = await import( + '@google/gemini-cli-core' + ); + const { runExitCleanup } = await import('./src/utils/cleanup.js'); + + main().catch(async (error: unknown) => { + // Set a timeout to force exit if cleanup hangs + const cleanupTimeout = setTimeout(() => { + writeToStderr('Cleanup timed out, forcing exit...\n'); + process.exit(1); + }, 5000); + + try { + await runExitCleanup(); + } catch (cleanupError: unknown) { + writeToStderr( + `Error during final cleanup: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}\n`, + ); + } finally { + clearTimeout(cleanupTimeout); + } + + if (error instanceof FatalError) { + let errorMessage = error.message; + if (!process.env['NO_COLOR']) { + errorMessage = `\x1b[31m${errorMessage}\x1b[0m`; + } + writeToStderr(errorMessage + '\n'); + process.exit(error.exitCode); + } + + writeToStderr('An unexpected critical error occurred:'); + if (error instanceof Error) { + writeToStderr(error.stack + '\n'); + } else { + writeToStderr(String(error) + '\n'); + } + process.exit(1); + }); + } +} + +run(); diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index c041aaa8c3..076978b203 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -1907,7 +1907,8 @@ const SETTINGS_SCHEMA = { category: 'Advanced', requiresRestart: true, default: true, - description: 'Automatically configure Node.js memory limits', + description: + 'Automatically configure Node.js memory limits. Note: Because memory is allocated during the initial process boot, this setting is only read from the global user settings file and ignores workspace-level overrides.', showInDialog: true, }, dnsResolutionOrder: { diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index f496bee37b..166ee0e7eb 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -81,10 +81,7 @@ import { validateNonInteractiveAuth } from './validateNonInterActiveAuth.js'; import { appEvents, AppEvent } from './utils/events.js'; import { SessionError, SessionSelector } from './utils/sessionUtils.js'; -import { - relaunchAppInChildProcess, - relaunchOnExitCode, -} from './utils/relaunch.js'; +import { relaunchOnExitCode } from './utils/relaunch.js'; import { loadSandboxConfig } from './config/sandboxConfig.js'; import { deleteSession, listSessions } from './utils/sessions.js'; import { createPolicyUpdater } from './config/policy.js'; @@ -439,6 +436,12 @@ export async function main() { // Set remote admin settings if returned from CCPA. if (remoteAdminSettings) { settings.setRemoteAdminSettings(remoteAdminSettings); + if (process.send) { + process.send({ + type: 'admin-settings-update', + settings: remoteAdminSettings, + }); + } } // Run deferred command now that we have admin settings. @@ -496,10 +499,6 @@ export async function main() { ); await runExitCleanup(); process.exit(ExitCodes.SUCCESS); - } else { - // Relaunch app so we always have a child process that can be internally - // restarted if needed. - await relaunchAppInChildProcess(memoryArgs, [], remoteAdminSettings); } } diff --git a/schemas/settings.schema.json b/schemas/settings.schema.json index bb5c9a9d54..1281d0f429 100644 --- a/schemas/settings.schema.json +++ b/schemas/settings.schema.json @@ -2725,8 +2725,8 @@ "properties": { "autoConfigureMemory": { "title": "Auto Configure Max Old Space Size", - "description": "Automatically configure Node.js memory limits", - "markdownDescription": "Automatically configure Node.js memory limits\n\n- Category: `Advanced`\n- Requires restart: `yes`\n- Default: `true`", + "description": "Automatically configure Node.js memory limits. Note: Because memory is allocated during the initial process boot, this setting is only read from the global user settings file and ignores workspace-level overrides.", + "markdownDescription": "Automatically configure Node.js memory limits. Note: Because memory is allocated during the initial process boot, this setting is only read from the global user settings file and ignores workspace-level overrides.\n\n- Category: `Advanced`\n- Requires restart: `yes`\n- Default: `true`", "default": true, "type": "boolean" },