/** * @license * Copyright 2025 Vybestack LLC * SPDX-License-Identifier: Apache-2.0 */ /** * Lightweight bootstrap utilities for CLI startup. * These functions are designed to run BEFORE any heavy initialization * (settings loading, provider configuration, MCP servers, etc.) * to determine if the process needs to be relaunched. */ import v8 from 'node:v8'; import os from 'node:os'; import { debugLogger } from '@vybestack/llxprt-code-telemetry'; /** * Exit code used to signal that the child process wants a relaunch. * The parent process should check for this code and respawn if needed. */ export const RELAUNCH_EXIT_CODE = 75; export const MAX_HEAP_CAP_MB = 8192; /** * Bun does not honor --max-old-space-size, so the established non-empty * `process.versions.bun` convention is the same check the launcher uses. */ function isBunRuntime(): boolean { return ( typeof process.versions.bun === 'string' && process.versions.bun.length > 0 ); } /** * Check if debug mode is enabled via environment variables. * This is a lightweight check that doesn't require loading any configuration. */ export function isDebugMode(): boolean { return [process.env.DEBUG, process.env.DEBUG_MODE].some( (v) => v === 'true' || v === '1', ); } /** * Determine if the process should be relaunched with higher memory limits. * This check is performed BEFORE loading any configuration to avoid * wasting time on initialization that would be discarded during relaunch. * * @param debugMode - Whether to log debug information * @returns Array of Node.js arguments for relaunch, or empty array if no relaunch needed */ export function shouldRelaunchForMemory( debugMode: boolean, maxHeapCapMB: number = MAX_HEAP_CAP_MB, ): string[] { if (isBunRuntime()) { return []; } const cap = Math.floor(maxHeapCapMB); const totalMemoryMB = os.totalmem() / (1024 * 1024); const heapStats = v8.getHeapStatistics(); const currentMaxOldSpaceSizeMb = Math.floor( heapStats.heap_size_limit / 1024 / 1024, ); const targetMaxOldSpaceSizeInMB = Math.min( Math.floor(totalMemoryMB * 0.5), cap, ); if (debugMode) { debugLogger.debug( `Current heap size ${currentMaxOldSpaceSizeMb.toFixed(2)} MB`, ); } // Guard against infinite relaunch loops if (process.env.LLXPRT_CODE_NO_RELAUNCH) { return []; } if (targetMaxOldSpaceSizeInMB > currentMaxOldSpaceSizeMb) { if (debugMode) { debugLogger.debug( `Need to relaunch with more memory: ${targetMaxOldSpaceSizeInMB.toFixed(2)} MB`, ); } return [`--max-old-space-size=${targetMaxOldSpaceSizeInMB}`]; } return []; } /** * Parse a Docker/Podman memory format string into megabytes. * Docker format: plain number = bytes, k = kilobytes, m = megabytes, g = gigabytes. * * @param memoryStr - Memory string in Docker format (e.g. "6g", "4096m", "1073741824") * @returns Memory in MB, or undefined if unparseable */ // Parses a Docker memory string like "512m" or "1.5g". The pattern is passed to // RegExp via an identifier so it is not a static literal flagged by // sonarjs/regular-expr. const DOCKER_MEMORY_PATTERN = '^(\\d+(?:\\.\\d+)?)\\s*([bkmg])?$'; const DOCKER_MEMORY_REGEX = new RegExp(DOCKER_MEMORY_PATTERN, 'i'); export function parseDockerMemoryToMB(memoryStr: string): number | undefined { if (!memoryStr) { return undefined; } const match = memoryStr.match(DOCKER_MEMORY_REGEX); if (!match) { return undefined; } const [, valueMatch, suffixMatch] = match as [ string, string, string | undefined, ]; const value = parseFloat(valueMatch); const suffix = (suffixMatch ?? '').toLowerCase(); switch (suffix) { case 'g': return value * 1024; case 'm': return value; case 'k': return value / 1024; case 'b': default: // Plain number or 'b' suffix is bytes return value / (1024 * 1024); } } /** * Compute --max-old-space-size for a new Node sandbox process. * Bun ignores this flag, so Bun-fronted sandbox launches receive no memory args. * * @param debugMode - Whether to log debug information * @param containerMemoryMB - Container memory limit in MB, or undefined to use host memory * @returns A Node heap argument, or an empty array when running under Bun */ export function computeSandboxMemoryArgs( debugMode: boolean, containerMemoryMB?: number, maxHeapCapMB: number = MAX_HEAP_CAP_MB, ): string[] { if (isBunRuntime()) { return []; } const cap = Math.floor(maxHeapCapMB); const totalMemoryMB = containerMemoryMB ?? os.totalmem() / (1024 * 1024); const targetMaxOldSpaceSizeInMB = Math.max( 128, Math.min(Math.floor(totalMemoryMB * 0.5), cap), ); if (debugMode) { debugLogger.debug( `Sandbox memory: total=${totalMemoryMB.toFixed(2)} MB, target heap=${targetMaxOldSpaceSizeInMB} MB`, ); } return [`--max-old-space-size=${targetMaxOldSpaceSizeInMB}`]; }