From b79effefee925d26ed7aea44afd6dfcdc20a2992 Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Wed, 20 May 2026 17:43:52 -0700 Subject: [PATCH] perf(tui): defer EmbeddedTuiBackend import, drop dead warmup helpers (#84701) * perf(tui): skip plugin-aware config validation on remote TUI startup Cold `openclaw tui` against a remote gateway was synchronously calling loadPluginMetadataSnapshot() via getRuntimeConfig() -> loadConfig() -> validateConfigObjectWithPlugins(), pulling the full plugin metadata snapshot (200k+ file reads) onto the TUI's event loop. The TUI itself never consumes plugin metadata in remote mode; it queries the gateway over RPC. The work was being done purely to validate the config and then thrown away. Thread an opt-in `skipPluginValidation` flag through getRuntimeConfig() and loadConfig() (createConfigIO already supports pluginValidation: "skip"; it just wasn't reachable from the runtime entrypoints). The TUI passes skipPluginValidation: !isLocalMode so: - Remote-mode TUI: no plugin metadata load, no event-loop freeze after first render - Embedded (--local) mode: unchanged; the in-process agent runtime still gets a fully validated config * remove verbose comments * perf(tui): move context cache warmup from module top-level to embedded backend agents/context.ts fired ensureContextWindowCacheLoaded() unconditionally at module-eval time for non-skip-listed CLI commands. The TUI transitively imports this module, so the warmup ran on every TUI startup including remote-mode, cascading into ensureOpenClawModelsJson -> resolveImplicitProviders -> runProviderCatalog and dominating the cold-start freeze (CPU profile showed ~55s of resolveProviderSyntheticAuthWithPlugin, lstat, open, etc.). It also pre-emptively called getRuntimeConfig() without skipPluginValidation, pinning the full snapshot and nullifying the skip flag added on this branch. Remove the top-level side effect and trigger the warmup explicitly from EmbeddedTuiBackend.start(), which only runs when an in-process agent runtime actually needs the cache. * perf(tui): defer EmbeddedTuiBackend import until local mode * refactor(agents): remove dead context-cache warmup helpers --- src/agents/context.lookup.test.ts | 38 --------------- src/agents/context.ts | 79 ------------------------------- src/tui/tui.ts | 23 +++++---- 3 files changed, 13 insertions(+), 127 deletions(-) diff --git a/src/agents/context.lookup.test.ts b/src/agents/context.lookup.test.ts index 61c15c27ef7..4885a3f861f 100644 --- a/src/agents/context.lookup.test.ts +++ b/src/agents/context.lookup.test.ts @@ -201,44 +201,6 @@ describe("lookupContextTokens", () => { expect(secondLoadConfigMock).not.toHaveBeenCalled(); }); - it("only warms eagerly for real openclaw startup commands that need model metadata", async () => { - const { shouldEagerWarmContextWindowCache } = await importContextModule(); - - expect(shouldEagerWarmContextWindowCache(["node", "openclaw", "chat"])).toBe(true); - expect(shouldEagerWarmContextWindowCache(["node", "openclaw", "chat", "--help"])).toBe(false); - expect( - shouldEagerWarmContextWindowCache(["node", "openclaw", "matrix", "encryption", "help"]), - ).toBe(false); - expect(shouldEagerWarmContextWindowCache(["node", "openclaw", "help", "matrix"])).toBe(false); - expect( - shouldEagerWarmContextWindowCache(["node", "openclaw", "browser", "status", "--help"]), - ).toBe(false); - expect( - shouldEagerWarmContextWindowCache([ - "node", - "openclaw", - "--profile", - "--", - "config", - "validate", - ]), - ).toBe(false); - expect(shouldEagerWarmContextWindowCache(["node", "openclaw", "logs", "--limit", "5"])).toBe( - false, - ); - expect( - shouldEagerWarmContextWindowCache(["node", "openclaw", "memory", "search", "--json"]), - ).toBe(false); - expect(shouldEagerWarmContextWindowCache(["node", "openclaw", "message", "read"])).toBe(false); - expect(shouldEagerWarmContextWindowCache(["node", "openclaw", "status", "--json"])).toBe(false); - expect(shouldEagerWarmContextWindowCache(["node", "openclaw", "sessions", "--json"])).toBe( - false, - ); - expect( - shouldEagerWarmContextWindowCache(["node", "scripts/test-built-plugin-singleton.mjs"]), - ).toBe(false); - }); - it("retries config loading after backoff when an initial load fails", async () => { vi.useFakeTimers(); const loadConfigMock = vi diff --git a/src/agents/context.ts b/src/agents/context.ts index 59d15b99ecd..7f5bc9a247f 100644 --- a/src/agents/context.ts +++ b/src/agents/context.ts @@ -1,12 +1,9 @@ // Lazy-load pi-coding-agent model metadata so we can infer context windows when // the agent reports a model id. This includes custom models.json entries. -import path from "node:path"; -import { isHelpOrVersionInvocation } from "../cli/argv.js"; import { getRuntimeConfig } from "../config/config.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { computeBackoff, type BackoffPolicy } from "../infra/backoff.js"; -import { consumeRootOptionToken, FLAG_TERMINATOR } from "../infra/cli-root-options.js"; import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; import { resolveDefaultAgentDir } from "./agent-scope.js"; import { lookupCachedContextTokens, MODEL_CONTEXT_TOKEN_CACHE } from "./context-cache.js"; @@ -108,82 +105,6 @@ function loadModelsConfigRuntime() { return CONTEXT_WINDOW_RUNTIME_STATE.modelsConfigRuntimeLoader.load(); } -function isLikelyOpenClawCliProcess(argv: string[] = process.argv): boolean { - const entryBasename = normalizeLowercaseStringOrEmpty(path.basename(argv[1] ?? "")); - return ( - entryBasename === "openclaw" || - entryBasename === "openclaw.mjs" || - entryBasename === "entry.js" || - entryBasename === "entry.mjs" - ); -} - -function getCommandPathFromArgv(argv: string[]): string[] { - const args = argv.slice(2); - const tokens: string[] = []; - for (let i = 0; i < args.length; i += 1) { - const arg = args[i]; - if (!arg || arg === FLAG_TERMINATOR) { - break; - } - const consumed = consumeRootOptionToken(args, i); - if (consumed > 0) { - i += consumed - 1; - continue; - } - if (arg.startsWith("-")) { - continue; - } - tokens.push(arg); - if (tokens.length >= 2) { - break; - } - } - return tokens; -} - -const SKIP_EAGER_WARMUP_PRIMARY_COMMANDS = new Set([ - "agent", - "backup", - "browser", - "completion", - "config", - "directory", - "doctor", - "gateway", - "health", - "hooks", - "logs", - "memory", - "message", - "models", - "pairing", - "plugins", - "secrets", - "sessions", - "status", - "update", - "webhooks", -]); - -export function shouldEagerWarmContextWindowCache(argv: string[] = process.argv): boolean { - // Keep this gate tied to the real OpenClaw CLI entrypoints. - // - // This module can also land inside shared dist chunks that are imported from - // plugin-sdk/library surfaces during smoke tests and plugin loading. If we do - // eager warmup for those generic Node script imports, merely importing the - // built plugin-sdk can call ensureOpenClawModelsJson(), which cascades into - // plugin discovery and breaks dist/source singleton assumptions. - if (!isLikelyOpenClawCliProcess(argv)) { - return false; - } - if (isHelpOrVersionInvocation(argv)) { - return false; - } - const [primary] = getCommandPathFromArgv(argv); - return Boolean(primary) && !SKIP_EAGER_WARMUP_PRIMARY_COMMANDS.has(primary); -} - function primeConfiguredContextWindows(): OpenClawConfig | undefined { if (CONTEXT_WINDOW_RUNTIME_STATE.configuredConfig) { applyConfiguredContextWindows({ diff --git a/src/tui/tui.ts b/src/tui/tui.ts index 8b7d3bc06c7..3ef4e20eb56 100644 --- a/src/tui/tui.ts +++ b/src/tui/tui.ts @@ -28,7 +28,6 @@ import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; import { getSlashCommands } from "./commands.js"; import { ChatLog } from "./components/chat-log.js"; import { CustomEditor } from "./components/custom-editor.js"; -import { EmbeddedTuiBackend } from "./embedded-backend.js"; import { GatewayChatClient } from "./gateway-chat.js"; import { editorTheme, theme } from "./theme/theme.js"; import type { TuiBackend } from "./tui-backend.js"; @@ -665,15 +664,19 @@ export async function runTui(opts: RunTuiOptions): Promise { localBtwRunIds.clear(); }; - const client: TuiBackend = opts.backend - ? opts.backend - : opts.local - ? new EmbeddedTuiBackend() - : await GatewayChatClient.connect({ - url: opts.url, - token: opts.token, - password: opts.password, - }); + let client: TuiBackend; + if (opts.backend) { + client = opts.backend; + } else if (opts.local) { + const { EmbeddedTuiBackend } = await import("./embedded-backend.js"); + client = new EmbeddedTuiBackend(); + } else { + client = await GatewayChatClient.connect({ + url: opts.url, + token: opts.token, + password: opts.password, + }); + } const previousConsoleSubsystemFilter = isLocalMode ? loggingState.consoleSubsystemFilter ? [...loggingState.consoleSubsystemFilter]