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
This commit is contained in:
Dallin Romney
2026-05-20 17:43:52 -07:00
committed by GitHub
parent d91ef6bb17
commit b79effefee
3 changed files with 13 additions and 127 deletions

View File

@@ -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

View File

@@ -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({

View File

@@ -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<TuiResult> {
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]