From c205577f2cf1d8098393a0d7f4c96f848431710e Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Mon, 27 Apr 2026 20:22:24 -0700 Subject: [PATCH] fix(cli): keep gateway run on fast path --- CHANGELOG.md | 1 + src/cli/run-main.test.ts | 11 ++- src/cli/run-main.ts | 169 ++++++++++++++++++++++++++++++++++----- 3 files changed, 162 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2126f9bdea5..d2c019cf05f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Gateway/startup: keep value-option foreground starts on the gateway fast path and skip proxy bootstrap unless proxy env is configured, reducing normal gateway startup RSS and avoiding full CLI graph loading. Thanks @vincentkoc. - Backup: skip installed plugin `extensions/*/node_modules` dependency trees while keeping plugin manifests and source files in archives, so local backups avoid rebuildable npm payload bloat. Fixes #64144. Thanks @BrilliantWang. - Cron/models: fail isolated cron runs closed when an explicit `payload.model` is not allowed or cannot be resolved, so scheduled jobs do not silently fall back to an unrelated agent default or paid route before configured provider proxies such as LiteLLM can run. Fixes #73146. Thanks @oneandrewwang. - Memory/QMD: back off repeated chat-turn QMD open failures while still letting memory status and CLI probes recheck immediately, so a broken sidecar dependency cannot trigger active-memory or cron retry storms. Fixes #73188 and #73176. Thanks @leonlushgit and @w3i-William. diff --git a/src/cli/run-main.test.ts b/src/cli/run-main.test.ts index a0976ee8227..a231b0d8e91 100644 --- a/src/cli/run-main.test.ts +++ b/src/cli/run-main.test.ts @@ -33,10 +33,19 @@ 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", "--port", "18789"])).toBe(true); + expect(isGatewayRunFastPathArgv(["node", "openclaw", "gateway", "--auth=none"])).toBe(true); + expect( + isGatewayRunFastPathArgv(["node", "openclaw", "--no-color", "gateway", "--bind", "loopback"]), + ).toBe(true); expect(isGatewayRunFastPathArgv(["node", "openclaw", "gateway", "run"])).toBe(true); + expect( + isGatewayRunFastPathArgv(["node", "openclaw", "gateway", "run", "--raw-stream-path", "x"]), + ).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); + expect(isGatewayRunFastPathArgv(["node", "openclaw", "gateway", "--port"])).toBe(false); + expect(isGatewayRunFastPathArgv(["node", "openclaw", "gateway", "--unknown"])).toBe(false); }); }); diff --git a/src/cli/run-main.ts b/src/cli/run-main.ts index 419bb4bcf4a..88203b46c4b 100644 --- a/src/cli/run-main.ts +++ b/src/cli/run-main.ts @@ -4,6 +4,7 @@ import process from "node:process"; import { fileURLToPath } from "node:url"; import { resolveStateDir } from "../config/paths.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { isValueToken } from "../infra/cli-root-options.js"; import { isTruthyEnvValue, normalizeEnv } from "../infra/env.js"; import { isMainModule } from "../infra/is-main.js"; import { ensureOpenClawCliOnPath } from "../infra/path-env.js"; @@ -39,6 +40,40 @@ export { type Awaitable = T | Promise; +const GATEWAY_RUN_VALUE_FLAGS = new Set([ + "--port", + "--bind", + "--token", + "--auth", + "--password", + "--password-file", + "--tailscale", + "--ws-log", + "--raw-stream-path", +]); + +const GATEWAY_RUN_BOOLEAN_FLAGS = new Set([ + "--tailscale-reset-on-exit", + "--allow-unconfigured", + "--dev", + "--reset", + "--force", + "--verbose", + "--cli-backend-logs", + "--claude-cli-logs", + "--compact", + "--raw-stream", +]); + +const CLI_PROXY_ENV_KEYS = [ + "HTTP_PROXY", + "HTTPS_PROXY", + "ALL_PROXY", + "http_proxy", + "https_proxy", + "all_proxy", +] as const; + function createGatewayCliMainStartupTrace(argv: string[]) { const enabled = isTruthyEnvValue(process.env.OPENCLAW_GATEWAY_STARTUP_TRACE) && @@ -73,14 +108,45 @@ 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") { + if (invocation.hasHelpOrVersion) { return false; } - return invocation.commandPath.length === 1 || invocation.commandPath[1] === "run"; + const args = argv.slice(2); + let sawGateway = false; + let sawRun = false; + + for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + if (!arg || arg === "--") { + return false; + } + if (!sawGateway) { + const consumed = consumeGatewayFastPathRootOptionToken(args, index); + if (consumed > 0) { + index += consumed - 1; + continue; + } + if (arg !== "gateway") { + return false; + } + sawGateway = true; + continue; + } + + const consumed = consumeGatewayRunOptionToken(args, index); + if (consumed > 0) { + index += consumed - 1; + continue; + } + if (!sawRun && arg === "run") { + sawRun = true; + continue; + } + return false; + } + + return sawGateway; } function hasJsonOutputFlag(argv: string[]): boolean { @@ -121,6 +187,7 @@ async function tryRunGatewayRunFastPath( const program = new Command(); program.name("openclaw"); program.enablePositionalOptions(); + program.option("--no-color", "Disable ANSI colors", false); program.exitOverride((err) => { process.exitCode = typeof err.exitCode === "number" ? err.exitCode : 1; throw err; @@ -201,6 +268,72 @@ async function ensureCliEnvProxyDispatcher(): Promise { } } +function consumeGatewayRunOptionToken(args: ReadonlyArray, index: number): number { + const arg = args[index]; + if (!arg || arg === "--" || !arg.startsWith("-")) { + return 0; + } + const equalsIndex = arg.indexOf("="); + const flag = equalsIndex === -1 ? arg : arg.slice(0, equalsIndex); + if (GATEWAY_RUN_BOOLEAN_FLAGS.has(flag)) { + return equalsIndex === -1 ? 1 : 0; + } + if (!GATEWAY_RUN_VALUE_FLAGS.has(flag)) { + return 0; + } + if (equalsIndex !== -1) { + return arg.slice(equalsIndex + 1).trim() ? 1 : 0; + } + return isValueToken(args[index + 1]) ? 2 : 0; +} + +function consumeGatewayFastPathRootOptionToken(args: ReadonlyArray, index: number): number { + const arg = args[index]; + if (!arg || arg === "--") { + return 0; + } + if (arg === "--no-color") { + return 1; + } + if (arg.startsWith("--profile=")) { + return arg.slice("--profile=".length).trim() ? 1 : 0; + } + if (arg === "--profile") { + return isValueToken(args[index + 1]) ? 2 : 0; + } + return 0; +} + +function shouldBootstrapCliProxyBeforeFastPath(env: NodeJS.ProcessEnv = process.env): boolean { + if ( + isTruthyEnvValue(env.OPENCLAW_DEBUG_PROXY_ENABLED) || + isTruthyEnvValue(env.OPENCLAW_DEBUG_PROXY_REQUIRE) + ) { + return true; + } + return CLI_PROXY_ENV_KEYS.some((key) => { + const value = env[key]; + return typeof value === "string" && value.trim().length > 0; + }); +} + +async function bootstrapCliProxyCaptureAndDispatcher( + startupTrace: ReturnType, +): Promise { + const [ + { initializeDebugProxyCapture, finalizeDebugProxyCapture }, + { maybeWarnAboutDebugProxyCoverage }, + ] = await startupTrace.measure("proxy-imports", () => + Promise.all([import("../proxy-capture/runtime.js"), import("../proxy-capture/coverage.js")]), + ); + initializeDebugProxyCapture("cli"); + process.once("exit", () => { + finalizeDebugProxyCapture(); + }); + await startupTrace.measure("proxy-dispatcher", () => ensureCliEnvProxyDispatcher()); + maybeWarnAboutDebugProxyCoverage(); +} + export async function runCli(argv: string[] = process.argv) { const originalArgv = normalizeWindowsArgv(argv); const startupTrace = createGatewayCliMainStartupTrace(originalArgv); @@ -312,20 +445,20 @@ export async function runCli(argv: string[] = process.argv) { return; } - const [ - { initializeDebugProxyCapture, finalizeDebugProxyCapture }, - { maybeWarnAboutDebugProxyCoverage }, - ] = await startupTrace.measure("proxy-imports", () => - Promise.all([import("../proxy-capture/runtime.js"), import("../proxy-capture/coverage.js")]), - ); - initializeDebugProxyCapture("cli"); - process.once("exit", () => { - finalizeDebugProxyCapture(); - }); - await startupTrace.measure("proxy-dispatcher", () => ensureCliEnvProxyDispatcher()); - maybeWarnAboutDebugProxyCoverage(); + const bootstrapProxyBeforeFastPath = shouldBootstrapCliProxyBeforeFastPath(); + if ( + !bootstrapProxyBeforeFastPath && + (await tryRunGatewayRunFastPath(normalizedArgv, startupTrace)) + ) { + return; + } - if (await tryRunGatewayRunFastPath(normalizedArgv, startupTrace)) { + await bootstrapCliProxyCaptureAndDispatcher(startupTrace); + + if ( + bootstrapProxyBeforeFastPath && + (await tryRunGatewayRunFastPath(normalizedArgv, startupTrace)) + ) { return; }