diff --git a/docs/configuration.md b/docs/configuration.md index 0eb5715ee3..13ef502011 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -131,6 +131,7 @@ signal it, so a stale file left by a crash cannot stop an unrelated process. | `BB_SERVER_URL` | `bb-app config` | Remote CLI/host use | Server URL for standalone `bb` CLI and `host-daemon` commands on the current machine. The CLI defaults to `http://127.0.0.1:38886` when unset. | | `BB_SERVER_BIND_HOST` | `bb-app env`, environment, or `--server-bind-host` | Startup-only | Server listener host. Defaults to `127.0.0.1`; accepts only `127.0.0.1` or `0.0.0.0`. A full launcher or desktop app restart is required; until then, a previous `0.0.0.0` listener remains exposed. This is not a `bb-app config` key. | | `BB_SERVER_PORT` | `bb-app env`, environment, or `--server-port` | Startup-only | HTTP listener port. Defaults to `38886`. A full launcher or desktop app restart is required after a persistent set or unset. | +| `BB_SERVER_MAX_OLD_SPACE_MB` | `bb-app env` or environment | Startup-only | Optional V8 old-space heap cap for the server process, in megabytes. Unset by default, in which case Node sizes the heap from total system memory and ignores any cgroup limit. Applied only to the server, because `NODE_OPTIONS` would also be inherited by spawned provider processes. | | `BB_HOST_DAEMON_PORT` | `bb-app env`, environment, or `--host-daemon-port` | Startup-only | Local host-daemon API port. Defaults to `38887`. A full launcher or desktop app restart is required after a persistent set or unset. | | `BB_LOG_LEVEL` | `bb-app config` | Startup-only debugging | Log level: `trace`, `debug`, `info`, `warn`, `error`, or `fatal`. A full launcher or desktop app restart is required. | | `OPENAI_API_KEY` | `bb-app env` | OpenAI opt-in routes | Required only when selecting explicit OpenAI provider routes such as `openai/gpt-4o-mini` or `openai/gpt-transcribe`. | diff --git a/packages/bb-app/src/launcher.ts b/packages/bb-app/src/launcher.ts index bfd962194d..d9a09c7243 100644 --- a/packages/bb-app/src/launcher.ts +++ b/packages/bb-app/src/launcher.ts @@ -609,6 +609,8 @@ interface ResolveHostDaemonCommandResult { kind: "join" | "start"; } +const SERVER_MAX_OLD_SPACE_ENV_NAME = "BB_SERVER_MAX_OLD_SPACE_MB"; + function color(code: number, value: string): string { return `\x1b[${code}m${value}\x1b[0m`; } @@ -2668,14 +2670,18 @@ Usage: } assertBbAppArtifacts(runtime.context); - const childProcess = spawn(process.execPath, [runtime.context.serverEntry], { - cwd: process.cwd(), - env: createServerEnv({ - context: runtime.context, - env: runtime.serverEnv, - }), - stdio: "inherit", - }); + const childProcess = spawn( + process.execPath, + [...resolveServerNodeFlags(process.env), runtime.context.serverEntry], + { + cwd: process.cwd(), + env: createServerEnv({ + context: runtime.context, + env: runtime.serverEnv, + }), + stdio: "inherit", + }, + ); process.exitCode = toExitCode(await waitForProcessExit(childProcess)); } @@ -2895,11 +2901,39 @@ function logManagedProcessStartupFailureContext( log(" ", dim(`logs: ${args.context.logDir}/`)); } +/** + * Node sizes the V8 old-space heap from total system memory and ignores any + * cgroup memory limit the process runs under. A server in a memory-capped unit + * is therefore allowed a heap far larger than the unit can tolerate, and fills + * much of it with garbage it has no incentive to collect before that ceiling. + * + * NODE_OPTIONS is not a usable workaround: sanitizeInheritedChildProcessEnv + * strips only NODE_ENV and BB_*, so a NODE_OPTIONS set for the server is also + * inherited by every spawned provider process and would cap those too. The + * limit has to be applied here, to the server process alone. + */ +function resolveServerNodeFlags(env: NodeJS.ProcessEnv): string[] { + const raw = env[SERVER_MAX_OLD_SPACE_ENV_NAME]; + if (raw === undefined || raw.trim() === "") { + return []; + } + const megabytes = Number.parseInt(raw.trim(), 10); + if (!Number.isInteger(megabytes) || megabytes <= 0) { + throw new Error( + `${SERVER_MAX_OLD_SPACE_ENV_NAME} must be a positive integer number of megabytes; received "${raw}".`, + ); + } + return [`--max-old-space-size=${megabytes}`]; +} + async function startFullStackServerProcess( args: StartFullStackServerProcessArgs, ): Promise { const serverRun = spawnNamedManagedProcess({ - args: [args.context.serverEntry], + args: [ + ...resolveServerNodeFlags(process.env), + args.context.serverEntry, + ], command: process.execPath, env: args.env, outputBuffer: args.outputBuffer,