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
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"typescript": "^5.9.2"
},
"engines": {
"node": ">=20.0.0"
"node": ">=22.8.0"
},
"version": "0.3.1",
"dependencies": {
Expand Down
1 change: 1 addition & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## [Unreleased]

- Fixed unsupported Node versions crashing before startup by requiring Node 22.8.0 or newer and showing upgrade guidance before loading the CLI ([ENG-4260](https://linear.app/primeintellect/issue/ENG-4260/incorrect-node-version-breaks-first-launch)).
- Added `@` file-path autocomplete to new-agent and reply prompts in the Agents View.

## [0.3.1] - 2026-07-15
Expand Down
2 changes: 1 addition & 1 deletion packages/coding-agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,6 @@
"directory": "packages/coding-agent"
},
"engines": {
"node": ">=20.6.0"
"node": ">=22.8.0"
}
}
43 changes: 43 additions & 0 deletions packages/coding-agent/src/cli-main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { enableCompileCache } from "node:module";
import { maybeStartInteractiveDaemonEarly } from "./cli/daemon-launch.js";
import {
closeOwnedSessionWorkerOwnerWatch,
installOwnedSessionWorkerOwnerWatch,
maybeRunOwnedSessionWorkerFrontend,
} from "./cli/owned-session-worker.js";
import { APP_NAME } from "./config.js";

export async function runCli(): Promise<void> {
try {
enableCompileCache?.();
} catch {
// Read-only cache dir; startup just skips the cache.
}

process.title = APP_NAME;
process.env.PI_CODING_AGENT = "true";
process.emitWarning = (() => {}) as typeof process.emitWarning;

// Boot a cold daemon concurrently with this process's heavy imports.
maybeStartInteractiveDaemonEarly(process.argv.slice(2));

installOwnedSessionWorkerOwnerWatch();

const handledByOwnedWorker = await maybeRunOwnedSessionWorkerFrontend(process.argv.slice(2));
if (!handledByOwnedWorker) {
const [{ EnvHttpProxyAgent, setGlobalDispatcher }, { main }] = await Promise.all([
import("undici"),
import("./main.js"),
]);

// undici's 300s body/headers timeouts abort long local-LLM SSE stalls; provider
// SDKs enforce their own deadlines via retry.provider.timeoutMs.
setGlobalDispatcher(new EnvHttpProxyAgent({ bodyTimeout: 0, headersTimeout: 0 }));

try {
await main(process.argv.slice(2));
} finally {
closeOwnedSessionWorkerOwnerWatch();
}
}
}
64 changes: 13 additions & 51 deletions packages/coding-agent/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,15 @@
#!/usr/bin/env node
/**
* CLI entry point for the refactored coding agent.
* Uses main.ts with AgentSession and new mode modules.
*
* Test with: npx tsx src/cli-new.ts [args...]
*/
import { enableCompileCache } from "node:module";
import { maybeStartInteractiveDaemonEarly } from "./cli/daemon-launch.js";
import {
closeOwnedSessionWorkerOwnerWatch,
installOwnedSessionWorkerOwnerWatch,
maybeRunOwnedSessionWorkerFrontend,
} from "./cli/owned-session-worker.js";
import { APP_NAME } from "./config.js";

// Persist V8 compile caches across runs (~10-15% off module-graph load time).
try {
enableCompileCache?.();
} catch {
// Unsupported Node version or read-only cache dir; startup just skips the cache.
}

process.title = APP_NAME;
process.env.PI_CODING_AGENT = "true";
process.emitWarning = (() => {}) as typeof process.emitWarning;

// Kick off the interactive daemon spawn/probe before importing the heavy main
// module graph (~1.5s), so a cold daemon boots concurrently with this
// process's own imports instead of serially after them.
maybeStartInteractiveDaemonEarly(process.argv.slice(2));

installOwnedSessionWorkerOwnerWatch();

const handledByOwnedWorker = await maybeRunOwnedSessionWorkerFrontend(process.argv.slice(2));
if (!handledByOwnedWorker) {
const [{ EnvHttpProxyAgent, setGlobalDispatcher }, { main }] = await Promise.all([
import("undici"),
import("./main.js"),
]);

// bodyTimeout/headersTimeout default to 300s in undici; long local-LLM stalls
// (e.g. vLLM buffering a large tool call) exceed that and abort the SSE stream
// with UND_ERR_BODY_TIMEOUT. Disable both — provider SDKs enforce their own
// AbortController-based deadlines via retry.provider.timeoutMs.
setGlobalDispatcher(new EnvHttpProxyAgent({ bodyTimeout: 0, headersTimeout: 0 }));

try {
await main(process.argv.slice(2));
} finally {
closeOwnedSessionWorkerOwnerWatch();
}
// The Node 22+ module graph fails at link time on older Node, so it must load
// behind the dynamic import, after the dependency-free guard runs.
import { assertNodeVersion } from "./cli/node-version-check.js";

const supported = assertNodeVersion({
version: process.versions.node,
log: console.error,
exit: (code) => process.exit(code),
});

if (supported) {
const { runCli } = await import("./cli-main.js");
await runCli();
}
58 changes: 58 additions & 0 deletions packages/coding-agent/src/cli/node-version-check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Dependency-free and Node-20-safe so it can never crash on the versions it rejects.

const MIN_NODE_VERSION_PARTS = [22, 8, 0] as const;
export const MIN_NODE_VERSION = MIN_NODE_VERSION_PARTS.join(".");

export interface NodeVersionGuardIO {
version: string;
log: (message: string) => void;
exit: (code: number) => void;
}

interface ParsedNodeVersion {
parts: readonly [number, number, number];
prerelease: boolean;
}

function parseVersion(version: string): ParsedNodeVersion | undefined {
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(version);
if (!match) {
return undefined;
}

return {
parts: [Number(match[1]), Number(match[2]), Number(match[3])],
prerelease: match[4] !== undefined,
};
}

function isSupportedNodeVersion(version: ParsedNodeVersion): boolean {
for (let index = 0; index < MIN_NODE_VERSION_PARTS.length; index++) {
const part = version.parts[index]!;
const minimumPart = MIN_NODE_VERSION_PARTS[index]!;
if (part !== minimumPart) {
return part > minimumPart;
}
}
return !version.prerelease;
}

export function assertNodeVersion(io: NodeVersionGuardIO): boolean {
// Bun ships its own runtime; its node-compat version is unrelated to the user's Node.
if (process.versions.bun) {
return true;
}

const version = parseVersion(io.version);
if (!version || isSupportedNodeVersion(version)) {
return true;
}

io.log(`prime-agent requires Node ${MIN_NODE_VERSION} or newer, but the active Node is v${io.version}.`);
io.log("");
io.log(` 1. Install Node ${MIN_NODE_VERSION}+ (e.g. "nvm install 22 && nvm use 22", or from https://nodejs.org)`);
io.log(" 2. Reinstall prime-agent under that Node so the command resolves to it:");
io.log(" https://github.com/PrimeIntellect-ai/prime-agent/releases/latest");
io.exit(1);
return false;
}
83 changes: 83 additions & 0 deletions packages/coding-agent/test/node-version-check.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { describe, expect, test } from "vitest";
import { assertNodeVersion, MIN_NODE_VERSION } from "../src/cli/node-version-check.js";

function run(version: string) {
const logs: string[] = [];
let exitCode: number | null = null;
const ok = assertNodeVersion({
version,
log: (m) => logs.push(m),
exit: (code) => {
exitCode = code;
},
});
return { ok, logs, exitCode };
}

describe("assertNodeVersion", () => {
test("passes on the minimum supported version", () => {
const { ok, logs, exitCode } = run(MIN_NODE_VERSION);
expect(ok).toBe(true);
expect(exitCode).toBeNull();
expect(logs).toHaveLength(0);
});

test("passes on a newer minor", () => {
const { ok, exitCode } = run("22.9.0");
expect(ok).toBe(true);
expect(exitCode).toBeNull();
});

test("passes on a newer patch", () => {
const { ok, exitCode } = run("22.8.1");
expect(ok).toBe(true);
expect(exitCode).toBeNull();
});

test("passes on a newer major", () => {
const { ok, exitCode } = run("25.9.0");
expect(ok).toBe(true);
expect(exitCode).toBeNull();
});

test("rejects a Node 22 release below the minimum", () => {
const { ok, logs, exitCode } = run("22.7.0");
expect(ok).toBe(false);
expect(exitCode).toBe(1);
expect(logs.join("\n")).toContain(`Node ${MIN_NODE_VERSION}`);
});

test("rejects an outdated major with guidance and exit 1", () => {
const { ok, logs, exitCode } = run("20.18.1");
expect(ok).toBe(false);
expect(exitCode).toBe(1);
const text = logs.join("\n");
expect(text).toContain(`Node ${MIN_NODE_VERSION}`);
expect(text).toContain("20.18.1");
expect(text).toContain("github.com/PrimeIntellect-ai/prime-agent/releases/latest");
});

test("accepts the v prefix used by process.version", () => {
const { ok, exitCode } = run(`v${MIN_NODE_VERSION}`);
expect(ok).toBe(true);
expect(exitCode).toBeNull();
});

test("accepts build metadata at the minimum version", () => {
const { ok, exitCode } = run(`${MIN_NODE_VERSION}+build.1`);
expect(ok).toBe(true);
expect(exitCode).toBeNull();
});

test("rejects a prerelease of the minimum version", () => {
const { ok, exitCode } = run(`${MIN_NODE_VERSION}-rc.1`);
expect(ok).toBe(false);
expect(exitCode).toBe(1);
});

test("lets an unparseable version through rather than blocking", () => {
const { ok, exitCode } = run("not-a-version");
expect(ok).toBe(true);
expect(exitCode).toBeNull();
});
});