From 955f0a692a38ee3f928ac835b2e3cc2eae34eb40 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 28 Apr 2026 02:04:57 +0100 Subject: [PATCH] perf: fast-path gateway foreground startup --- src/cli/program/register.subclis-core.ts | 33 ++++++++++++++- src/cli/program/register.subclis.test.ts | 44 ++++++++++++++++++++ src/cli/program/register.subclis.ts | 8 +++- src/cli/run-main.test.ts | 12 ++++++ src/cli/run-main.ts | 52 +++++++++++++++++++++++- 5 files changed, 145 insertions(+), 4 deletions(-) diff --git a/src/cli/program/register.subclis-core.ts b/src/cli/program/register.subclis-core.ts index 80176ced24f..0883e8288e5 100644 --- a/src/cli/program/register.subclis-core.ts +++ b/src/cli/program/register.subclis-core.ts @@ -10,6 +10,7 @@ import { defineImportedProgramCommandGroupSpecs, type CommandGroupDescriptorSpec, } from "./command-group-descriptors.js"; +import { removeCommandByName } from "./command-tree.js"; import { loadPrivateQaCliModule } from "./private-qa-cli.js"; import { registerCommandGroupByName, @@ -26,6 +27,28 @@ export { getSubCliCommandsWithSubcommands }; type SubCliRegistrar = (program: Command) => Promise | void; +function shouldRegisterGatewayRunOnly(name: string, argv: string[]): boolean { + if (name !== "gateway") { + return false; + } + const invocation = resolveCliArgvInvocation(argv); + if (invocation.hasHelpOrVersion || invocation.commandPath[0] !== "gateway") { + return false; + } + return invocation.commandPath.length === 1 || invocation.commandPath[1] === "run"; +} + +async function registerGatewayRunOnly(program: Command): Promise { + const { addGatewayRunCommand } = await import("../gateway-cli/run.js"); + removeCommandByName(program, "gateway"); + const gateway = addGatewayRunCommand( + program.command("gateway").description("Run, inspect, and query the WebSocket Gateway"), + ); + addGatewayRunCommand( + gateway.command("run").description("Run the WebSocket Gateway (foreground)"), + ); +} + async function registerSubCliWithPluginCommands( program: Command, registerSubCli: () => Promise, @@ -241,7 +264,15 @@ export function getSubCliEntries(): ReadonlyArray { return getSubCliEntryDescriptors(); } -export async function registerSubCliByName(program: Command, name: string): Promise { +export async function registerSubCliByName( + program: Command, + name: string, + argv: string[] = process.argv, +): Promise { + if (shouldRegisterGatewayRunOnly(name, argv)) { + await registerGatewayRunOnly(program); + return true; + } return registerCommandGroupByName(program, resolveSubCliCommandGroups(), name); } diff --git a/src/cli/program/register.subclis.test.ts b/src/cli/program/register.subclis.test.ts index f55ded1be1a..e0b4316e02e 100644 --- a/src/cli/program/register.subclis.test.ts +++ b/src/cli/program/register.subclis.test.ts @@ -47,8 +47,25 @@ const { registerPluginsCli, registerPluginCliCommandsFromValidatedConfig } = vi. }), registerPluginCliCommandsFromValidatedConfig: vi.fn(async () => null), })); +const { addGatewayRunCommand, gatewayRunAction, registerGatewayCli } = vi.hoisted(() => { + const runAction = vi.fn(); + return { + addGatewayRunCommand: vi.fn((command: Command) => + command.option("--force", "force", false).action(runAction), + ), + gatewayRunAction: runAction, + registerGatewayCli: vi.fn((program: Command) => { + program + .command("gateway") + .command("call") + .action(() => undefined); + }), + }; +}); vi.mock("../acp-cli.js", () => ({ registerAcpCli })); +vi.mock("../gateway-cli.js", () => ({ registerGatewayCli })); +vi.mock("../gateway-cli/run.js", () => ({ addGatewayRunCommand })); vi.mock("../nodes-cli.js", () => ({ registerNodesCli })); vi.mock("../capability-cli.js", () => ({ registerCapabilityCli })); vi.mock("../plugins-cli.js", () => ({ registerPluginsCli })); @@ -93,6 +110,9 @@ describe("registerSubCliCommands", () => { inferAction.mockClear(); registerPluginsCli.mockClear(); registerPluginCliCommandsFromValidatedConfig.mockClear(); + addGatewayRunCommand.mockClear(); + gatewayRunAction.mockClear(); + registerGatewayCli.mockClear(); }); afterEach(() => { @@ -174,6 +194,30 @@ describe("registerSubCliCommands", () => { expect(acpAction).toHaveBeenCalledTimes(1); }); + it("registers only the gateway run surface for gateway startup", async () => { + const argv = ["node", "openclaw", "gateway", "--force"]; + process.argv = argv; + const program = new Command().name("openclaw"); + + await registerSubCliByName(program, "gateway", argv); + + expect(addGatewayRunCommand).toHaveBeenCalledTimes(2); + expect(registerGatewayCli).not.toHaveBeenCalled(); + await program.parseAsync(["gateway", "--force"], { from: "user" }); + expect(gatewayRunAction).toHaveBeenCalledTimes(1); + }); + + it("keeps the full gateway CLI for non-run gateway subcommands", async () => { + const argv = ["node", "openclaw", "gateway", "call", "health"]; + process.argv = argv; + const program = new Command().name("openclaw"); + + await registerSubCliByName(program, "gateway", argv); + + expect(addGatewayRunCommand).not.toHaveBeenCalled(); + expect(registerGatewayCli).toHaveBeenCalledTimes(1); + }); + it.each([ ["plugins update", ["plugins", "update", "lossless-claw"]], ["plugins update --all", ["plugins", "update", "--all"]], diff --git a/src/cli/program/register.subclis.ts b/src/cli/program/register.subclis.ts index 7c5bb496fbc..dedd1fc3a9e 100644 --- a/src/cli/program/register.subclis.ts +++ b/src/cli/program/register.subclis.ts @@ -46,8 +46,12 @@ export function getSubCliEntries(): ReadonlyArray { return getSubCliEntryDescriptors(); } -export async function registerSubCliByName(program: Command, name: string): Promise { - if (await registerSubCliByNameCore(program, name)) { +export async function registerSubCliByName( + program: Command, + name: string, + argv: string[] = process.argv, +): Promise { + if (await registerSubCliByNameCore(program, name, argv)) { return true; } return registerCommandGroupByName(program, resolveSubCliCommandGroups(), name); diff --git a/src/cli/run-main.test.ts b/src/cli/run-main.test.ts index aa6ccee4ae1..a0976ee8227 100644 --- a/src/cli/run-main.test.ts +++ b/src/cli/run-main.test.ts @@ -9,6 +9,7 @@ import { shouldUseBrowserHelpFastPath, shouldUseRootHelpFastPath, } from "./run-main-policy.js"; +import { isGatewayRunFastPathArgv } from "./run-main.js"; const memoryWikiCommandAliasRegistry: PluginManifestCommandAliasRegistry = { plugins: [ @@ -28,6 +29,17 @@ const memoryCoreCommandAliasRegistry: PluginManifestCommandAliasRegistry = { ], }; +describe("isGatewayRunFastPathArgv", () => { + it("matches only plain gateway foreground starts without root options or help", () => { + expect(isGatewayRunFastPathArgv(["node", "openclaw", "gateway"])).toBe(true); + expect(isGatewayRunFastPathArgv(["node", "openclaw", "gateway", "--force"])).toBe(true); + expect(isGatewayRunFastPathArgv(["node", "openclaw", "gateway", "run"])).toBe(true); + expect(isGatewayRunFastPathArgv(["node", "openclaw", "gateway", "call", "health"])).toBe(false); + expect(isGatewayRunFastPathArgv(["node", "openclaw", "gateway", "--help"])).toBe(false); + expect(isGatewayRunFastPathArgv(["node", "openclaw", "--no-color", "gateway"])).toBe(false); + }); +}); + describe("rewriteUpdateFlagArgv", () => { it("leaves argv unchanged when --update is absent", () => { const argv = ["node", "entry.js", "status"]; diff --git a/src/cli/run-main.ts b/src/cli/run-main.ts index 054ba60561d..32af67862d5 100644 --- a/src/cli/run-main.ts +++ b/src/cli/run-main.ts @@ -72,6 +72,52 @@ function createGatewayCliMainStartupTrace(argv: string[]) { }; } +export function isGatewayRunFastPathArgv(argv: string[]): boolean { + if (argv[2] !== "gateway") { + return false; + } + const invocation = resolveCliArgvInvocation(argv); + if (invocation.hasHelpOrVersion || invocation.commandPath[0] !== "gateway") { + return false; + } + return invocation.commandPath.length === 1 || invocation.commandPath[1] === "run"; +} + +async function tryRunGatewayRunFastPath( + argv: string[], + startupTrace: ReturnType, +): Promise { + if (!isGatewayRunFastPathArgv(argv)) { + return false; + } + const [{ Command }, { addGatewayRunCommand }] = await startupTrace.measure( + "gateway-run-imports", + () => Promise.all([import("commander"), import("./gateway-cli/run.js")]), + ); + const program = new Command(); + program.name("openclaw"); + program.enablePositionalOptions(); + program.exitOverride((err) => { + process.exitCode = typeof err.exitCode === "number" ? err.exitCode : 1; + throw err; + }); + const gateway = addGatewayRunCommand( + program.command("gateway").description("Run, inspect, and query the WebSocket Gateway"), + ); + addGatewayRunCommand( + gateway.command("run").description("Run the WebSocket Gateway (foreground)"), + ); + try { + await startupTrace.measure("gateway-run-parse", () => program.parseAsync(argv)); + } catch (error) { + if (!isCommanderParseExit(error)) { + throw error; + } + process.exitCode = error.exitCode; + } + return true; +} + async function closeCliMemoryManagers(): Promise { const { hasMemoryRuntime } = await import("../plugins/memory-state.js"); if (!hasMemoryRuntime()) { @@ -255,6 +301,10 @@ export async function runCli(argv: string[] = process.argv) { await startupTrace.measure("proxy-dispatcher", () => ensureCliEnvProxyDispatcher()); maybeWarnAboutDebugProxyCoverage(); + if (await tryRunGatewayRunFastPath(normalizedArgv, startupTrace)) { + return; + } + const { tryRouteCli } = await startupTrace.measure("route-import", () => import("./route.js")); if (await startupTrace.measure("route", () => tryRouteCli(normalizedArgv))) { return; @@ -339,7 +389,7 @@ export async function runCli(argv: string[] = process.argv) { await registerCoreCliByName(program, ctx, primary, parseArgv); } const { registerSubCliByName } = await import("./program/register.subclis.js"); - await registerSubCliByName(program, primary); + await registerSubCliByName(program, primary, parseArgv); }); }