CLI: add root --help fast path and lazy channel option resolution (#30975)

* CLI argv: add strict root help invocation guard

* Entry: add root help fast-path bootstrap bypass

* CLI context: lazily resolve channel options

* CLI context tests: cover lazy channel option resolution

* CLI argv tests: cover root help invocation detection

* Changelog: note additional startup path optimizations

* Changelog: split startup follow-up into #30975 entry

* CLI channel options: load precomputed startup metadata

* CLI channel options tests: cover precomputed metadata path

* Build: generate CLI startup metadata during build

* Build script: invoke CLI startup metadata generator

* CLI routes: preload plugins for routed health

* CLI routes tests: assert health plugin preload

* CLI: add experimental bundled entry and snapshot helper

* Tools: compare CLI startup entries in benchmark script

* Docs: add startup tuning notes for Pi and VM hosts

* CLI: drop bundled entry runtime toggle

* Build: remove bundled and snapshot scripts

* Tools: remove bundled-entry benchmark shortcut

* Docs: remove bundled startup bench examples

* Docs: remove Pi bundled entry mention

* Docs: remove VM bundled entry mention

* Changelog: remove bundled startup follow-up claims

* Build: remove snapshot helper script

* Build: remove CLI bundle tsdown config

* Doctor: add low-power startup optimization hints

* Doctor: run startup optimization hint checks

* Doctor tests: cover startup optimization host targeting

* Doctor tests: mock startup optimization note export

* CLI argv: require strict root-only help fast path

* CLI argv tests: cover mixed root-help invocations

* CLI channel options: merge metadata with runtime catalog

* CLI channel options tests: assert dynamic catalog merge

* Changelog: align #30975 startup follow-up scope

* Docs tests: remove secondary-entry startup bench note

* Docs Pi: add systemd recovery reference link

* Docs VPS: add systemd recovery reference link
This commit is contained in:
Vincent Koc
2026-03-01 14:23:46 -08:00
committed by GitHub
parent dcd19da425
commit 38da2d076c
19 changed files with 667 additions and 31 deletions

View File

@@ -0,0 +1,81 @@
import { describe, expect, it, vi } from "vitest";
import { noteStartupOptimizationHints } from "./doctor-platform-notes.js";
describe("noteStartupOptimizationHints", () => {
it("does not warn when compile cache and no-respawn are configured", () => {
const noteFn = vi.fn();
noteStartupOptimizationHints(
{
NODE_COMPILE_CACHE: "/var/tmp/openclaw-compile-cache",
OPENCLAW_NO_RESPAWN: "1",
},
{ platform: "linux", arch: "arm64", totalMemBytes: 4 * 1024 ** 3, noteFn },
);
expect(noteFn).not.toHaveBeenCalled();
});
it("warns when compile cache is under /tmp and no-respawn is not set", () => {
const noteFn = vi.fn();
noteStartupOptimizationHints(
{
NODE_COMPILE_CACHE: "/tmp/openclaw-compile-cache",
},
{ platform: "linux", arch: "arm64", totalMemBytes: 4 * 1024 ** 3, noteFn },
);
expect(noteFn).toHaveBeenCalledTimes(1);
const [message, title] = noteFn.mock.calls[0] ?? [];
expect(title).toBe("Startup optimization");
expect(message).toContain("NODE_COMPILE_CACHE points to /tmp");
expect(message).toContain("OPENCLAW_NO_RESPAWN is not set to 1");
expect(message).toContain("export NODE_COMPILE_CACHE=/var/tmp/openclaw-compile-cache");
expect(message).toContain("export OPENCLAW_NO_RESPAWN=1");
});
it("warns when compile cache is disabled via env override", () => {
const noteFn = vi.fn();
noteStartupOptimizationHints(
{
NODE_COMPILE_CACHE: "/var/tmp/openclaw-compile-cache",
OPENCLAW_NO_RESPAWN: "1",
NODE_DISABLE_COMPILE_CACHE: "1",
},
{ platform: "linux", arch: "arm64", totalMemBytes: 4 * 1024 ** 3, noteFn },
);
expect(noteFn).toHaveBeenCalledTimes(1);
const [message] = noteFn.mock.calls[0] ?? [];
expect(message).toContain("NODE_DISABLE_COMPILE_CACHE is set");
expect(message).toContain("unset NODE_DISABLE_COMPILE_CACHE");
});
it("skips startup optimization note on win32", () => {
const noteFn = vi.fn();
noteStartupOptimizationHints(
{
NODE_COMPILE_CACHE: "/tmp/openclaw-compile-cache",
},
{ platform: "win32", arch: "arm64", totalMemBytes: 4 * 1024 ** 3, noteFn },
);
expect(noteFn).not.toHaveBeenCalled();
});
it("skips startup optimization note on non-target linux hosts", () => {
const noteFn = vi.fn();
noteStartupOptimizationHints(
{
NODE_COMPILE_CACHE: "/tmp/openclaw-compile-cache",
},
{ platform: "linux", arch: "x64", totalMemBytes: 32 * 1024 ** 3, noteFn },
);
expect(noteFn).not.toHaveBeenCalled();
});
});

View File

@@ -140,3 +140,81 @@ export function noteDeprecatedLegacyEnvVars(
];
(deps?.noteFn ?? note)(lines.join("\n"), "Environment");
}
function isTruthyEnvValue(value: string | undefined): boolean {
return typeof value === "string" && value.trim().length > 0;
}
function isTmpCompileCachePath(cachePath: string): boolean {
const normalized = cachePath.trim().replace(/\/+$/, "");
return (
normalized === "/tmp" ||
normalized.startsWith("/tmp/") ||
normalized === "/private/tmp" ||
normalized.startsWith("/private/tmp/")
);
}
export function noteStartupOptimizationHints(
env: NodeJS.ProcessEnv = process.env,
deps?: {
platform?: NodeJS.Platform;
arch?: string;
totalMemBytes?: number;
noteFn?: typeof note;
},
) {
const platform = deps?.platform ?? process.platform;
if (platform === "win32") {
return;
}
const arch = deps?.arch ?? os.arch();
const totalMemBytes = deps?.totalMemBytes ?? os.totalmem();
const isArmHost = arch === "arm" || arch === "arm64";
const isLowMemoryLinux =
platform === "linux" && totalMemBytes > 0 && totalMemBytes <= 8 * 1024 ** 3;
const isStartupTuneTarget = platform === "linux" && (isArmHost || isLowMemoryLinux);
if (!isStartupTuneTarget) {
return;
}
const noteFn = deps?.noteFn ?? note;
const compileCache = env.NODE_COMPILE_CACHE?.trim() ?? "";
const disableCompileCache = env.NODE_DISABLE_COMPILE_CACHE?.trim() ?? "";
const noRespawn = env.OPENCLAW_NO_RESPAWN?.trim() ?? "";
const lines: string[] = [];
if (!compileCache) {
lines.push(
"- NODE_COMPILE_CACHE is not set; repeated CLI runs can be slower on small hosts (Pi/VM).",
);
} else if (isTmpCompileCachePath(compileCache)) {
lines.push(
"- NODE_COMPILE_CACHE points to /tmp; use /var/tmp so cache survives reboots and warms startup reliably.",
);
}
if (isTruthyEnvValue(disableCompileCache)) {
lines.push("- NODE_DISABLE_COMPILE_CACHE is set; startup compile cache is disabled.");
}
if (noRespawn !== "1") {
lines.push(
"- OPENCLAW_NO_RESPAWN is not set to 1; set it to avoid extra startup overhead from self-respawn.",
);
}
if (lines.length === 0) {
return;
}
const suggestions = [
"- Suggested env for low-power hosts:",
" export NODE_COMPILE_CACHE=/var/tmp/openclaw-compile-cache",
" mkdir -p /var/tmp/openclaw-compile-cache",
" export OPENCLAW_NO_RESPAWN=1",
isTruthyEnvValue(disableCompileCache) ? " unset NODE_DISABLE_COMPILE_CACHE" : undefined,
].filter((line): line is string => Boolean(line));
noteFn([...lines, ...suggestions].join("\n"), "Startup optimization");
}

View File

@@ -19,6 +19,7 @@ vi.mock("./doctor-memory-search.js", () => ({
vi.mock("./doctor-platform-notes.js", () => ({
noteDeprecatedLegacyEnvVars: vi.fn(),
noteStartupOptimizationHints: vi.fn(),
noteMacLaunchAgentOverrides: vi.fn().mockResolvedValue(undefined),
noteMacLaunchctlGatewayEnvOverrides: vi.fn().mockResolvedValue(undefined),
}));

View File

@@ -40,6 +40,7 @@ import {
noteMacLaunchAgentOverrides,
noteMacLaunchctlGatewayEnvOverrides,
noteDeprecatedLegacyEnvVars,
noteStartupOptimizationHints,
} from "./doctor-platform-notes.js";
import { createDoctorPrompter, type DoctorOptions } from "./doctor-prompter.js";
import { maybeRepairSandboxImages, noteSandboxScopeWarnings } from "./doctor-sandbox.js";
@@ -92,6 +93,7 @@ export async function doctorCommand(
await maybeRepairUiProtocolFreshness(runtime, prompter);
noteSourceInstallIssues(root);
noteDeprecatedLegacyEnvVars();
noteStartupOptimizationHints();
const configResult = await loadAndMaybeMigrateDoctorConfig({
options,