Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions apps/desktop/src/backend/DesktopBackendConfiguration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ describe("DesktopBackendConfiguration", () => {
assert.equal(first.cwd, environment.backendCwd);
assert.equal(first.captureOutput, true);
assert.equal(first.env.ELECTRON_RUN_AS_NODE, "1");
// Heap headroom for the primary backend (issue #21). extendEnv:true means
// this config value wins over an inherited process.env NODE_OPTIONS.
assert.include(first.env.NODE_OPTIONS ?? "", "--max-old-space-size=4096");
assert.isUndefined(first.env.T3CODE_PORT);
assert.isUndefined(first.env.T3CODE_MODE);
assert.isUndefined(first.env.T3CODE_DESKTOP_LAN_HOST);
Expand All @@ -152,6 +155,43 @@ describe("DesktopBackendConfiguration", () => {
),
);

it.effect("resolvePrimary sets a bare heap flag when NODE_OPTIONS is unset", () => {
const previous = process.env.NODE_OPTIONS;
delete process.env.NODE_OPTIONS;
return withHarness(
Effect.gen(function* () {
const configuration = yield* DesktopBackendConfiguration.DesktopBackendConfiguration;
const config = yield* configuration.resolvePrimary;
assert.equal(config.env.NODE_OPTIONS, "--max-old-space-size=4096");
}),
).pipe(Effect.ensuring(Effect.sync(() => restoreEnv("NODE_OPTIONS", previous))));
});

it.effect("resolvePrimary appends heap headroom to an existing NODE_OPTIONS", () => {
const previous = process.env.NODE_OPTIONS;
process.env.NODE_OPTIONS = "--enable-source-maps";
return withHarness(
Effect.gen(function* () {
const configuration = yield* DesktopBackendConfiguration.DesktopBackendConfiguration;
const config = yield* configuration.resolvePrimary;
assert.equal(config.env.NODE_OPTIONS, "--enable-source-maps --max-old-space-size=4096");
}),
).pipe(Effect.ensuring(Effect.sync(() => restoreEnv("NODE_OPTIONS", previous))));
});

it.effect("resolvePrimary preserves an explicit NODE_OPTIONS heap setting", () => {
const previous = process.env.NODE_OPTIONS;
process.env.NODE_OPTIONS = "--max-old-space-size=1024";
return withHarness(
Effect.gen(function* () {
const configuration = yield* DesktopBackendConfiguration.DesktopBackendConfiguration;
const config = yield* configuration.resolvePrimary;
// An operator override wins; we do not append a second, conflicting flag.
assert.equal(config.env.NODE_OPTIONS, "--max-old-space-size=1024");
}),
).pipe(Effect.ensuring(Effect.sync(() => restoreEnv("NODE_OPTIONS", previous))));
});

it.effect("resolveWsl reuses the primary's bootstrap token", () =>
withHarness(
Effect.gen(function* () {
Expand Down Expand Up @@ -279,6 +319,8 @@ describe("DesktopBackendConfiguration", () => {
"env",
"PATH=/home/test user's/.nvm/versions/node/v22.0.0/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/test user/bin:/opt/test's tools/bin:/usr/bin:/bin",
nodePath,
// Heap headroom, passed as a V8 execArg before the entry script (issue #21).
"--max-old-space-size=4096",
linuxEntryPath,
"--bootstrap-fd",
"0",
Expand Down
29 changes: 29 additions & 0 deletions apps/desktop/src/backend/DesktopBackendConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,30 @@ const WSL_FORWARDED_ENV_NAMES = ["OPENAI_API_KEY", "ANTHROPIC_API_KEY"] as const

const WSL_SERVER_SYSTEM_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";

// Give the backend a predictable V8 old-space ceiling instead of inheriting the
// default. This is headroom for transient heap spikes (large diffs, big file
// reads) so a memory-constrained host is less likely to push the backend into a
// GC-thrash / stall while it is busy — part of the robustness work for issue
// #21. It is a ceiling, not a reservation, so it does not raise steady-state
// memory use (the backend's normal RSS is well under this).
const DESKTOP_BACKEND_MAX_OLD_SPACE_MB = 4096;
const DESKTOP_BACKEND_MAX_OLD_SPACE_FLAG = `--max-old-space-size=${DESKTOP_BACKEND_MAX_OLD_SPACE_MB}`;

// Append our heap-headroom flag to any NODE_OPTIONS already exported (by the
// user or the dev-runner) rather than replacing it. If an explicit
// --max-old-space-size is already present we leave it untouched so an operator
// can override the ceiling. Electron-run-as-node honours the NODE_OPTIONS
// safelist, which includes --max-old-space-size.
const backendNodeOptions = (existing: string | undefined): string => {
const trimmed = existing?.trim() ?? "";
if (trimmed.includes("--max-old-space-size")) {
return trimmed;
}
return trimmed.length > 0
? `${trimmed} ${DESKTOP_BACKEND_MAX_OLD_SPACE_FLAG}`
: DESKTOP_BACKEND_MAX_OLD_SPACE_FLAG;
};

const backendChildEnvPatch = (): Record<string, string | undefined> =>
Object.fromEntries(DESKTOP_BACKEND_ENV_NAMES.map((name) => [name, undefined]));

Expand Down Expand Up @@ -356,6 +380,7 @@ const resolvePrimaryStartConfig = Effect.fn("desktop.backendConfiguration.resolv
env: {
...backendChildEnvPatch(),
ELECTRON_RUN_AS_NODE: "1",
NODE_OPTIONS: backendNodeOptions(process.env.NODE_OPTIONS),
},
// Primary wants process.env (PATH, dev-runner's T3CODE_HOME, etc.).
extendEnv: true,
Expand Down Expand Up @@ -546,6 +571,10 @@ const resolveWslStartConfig = Effect.fn("desktop.backendConfiguration.resolveWsl
"env",
`PATH=${launchPath}`,
preflight.nodePath,
// Match the primary backend's heap headroom. WSL runs plain node, so the
// flag is passed as a V8 execArg (before the entry script) rather than via
// NODE_OPTIONS.
DESKTOP_BACKEND_MAX_OLD_SPACE_FLAG,
preflight.linuxEntryPath,
"--bootstrap-fd",
"0",
Expand Down
Loading
Loading