Skip to content

Commit b79effe

Browse files
authored
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
1 parent d91ef6b commit b79effe

3 files changed

Lines changed: 13 additions & 127 deletions

File tree

src/agents/context.lookup.test.ts

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -201,44 +201,6 @@ describe("lookupContextTokens", () => {
201201
expect(secondLoadConfigMock).not.toHaveBeenCalled();
202202
});
203203

204-
it("only warms eagerly for real openclaw startup commands that need model metadata", async () => {
205-
const { shouldEagerWarmContextWindowCache } = await importContextModule();
206-
207-
expect(shouldEagerWarmContextWindowCache(["node", "openclaw", "chat"])).toBe(true);
208-
expect(shouldEagerWarmContextWindowCache(["node", "openclaw", "chat", "--help"])).toBe(false);
209-
expect(
210-
shouldEagerWarmContextWindowCache(["node", "openclaw", "matrix", "encryption", "help"]),
211-
).toBe(false);
212-
expect(shouldEagerWarmContextWindowCache(["node", "openclaw", "help", "matrix"])).toBe(false);
213-
expect(
214-
shouldEagerWarmContextWindowCache(["node", "openclaw", "browser", "status", "--help"]),
215-
).toBe(false);
216-
expect(
217-
shouldEagerWarmContextWindowCache([
218-
"node",
219-
"openclaw",
220-
"--profile",
221-
"--",
222-
"config",
223-
"validate",
224-
]),
225-
).toBe(false);
226-
expect(shouldEagerWarmContextWindowCache(["node", "openclaw", "logs", "--limit", "5"])).toBe(
227-
false,
228-
);
229-
expect(
230-
shouldEagerWarmContextWindowCache(["node", "openclaw", "memory", "search", "--json"]),
231-
).toBe(false);
232-
expect(shouldEagerWarmContextWindowCache(["node", "openclaw", "message", "read"])).toBe(false);
233-
expect(shouldEagerWarmContextWindowCache(["node", "openclaw", "status", "--json"])).toBe(false);
234-
expect(shouldEagerWarmContextWindowCache(["node", "openclaw", "sessions", "--json"])).toBe(
235-
false,
236-
);
237-
expect(
238-
shouldEagerWarmContextWindowCache(["node", "scripts/test-built-plugin-singleton.mjs"]),
239-
).toBe(false);
240-
});
241-
242204
it("retries config loading after backoff when an initial load fails", async () => {
243205
vi.useFakeTimers();
244206
const loadConfigMock = vi

src/agents/context.ts

Lines changed: 0 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
// Lazy-load pi-coding-agent model metadata so we can infer context windows when
22
// the agent reports a model id. This includes custom models.json entries.
33

4-
import path from "node:path";
5-
import { isHelpOrVersionInvocation } from "../cli/argv.js";
64
import { getRuntimeConfig } from "../config/config.js";
75
import type { OpenClawConfig } from "../config/types.openclaw.js";
86
import { computeBackoff, type BackoffPolicy } from "../infra/backoff.js";
9-
import { consumeRootOptionToken, FLAG_TERMINATOR } from "../infra/cli-root-options.js";
107
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
118
import { resolveDefaultAgentDir } from "./agent-scope.js";
129
import { lookupCachedContextTokens, MODEL_CONTEXT_TOKEN_CACHE } from "./context-cache.js";
@@ -108,82 +105,6 @@ function loadModelsConfigRuntime() {
108105
return CONTEXT_WINDOW_RUNTIME_STATE.modelsConfigRuntimeLoader.load();
109106
}
110107

111-
function isLikelyOpenClawCliProcess(argv: string[] = process.argv): boolean {
112-
const entryBasename = normalizeLowercaseStringOrEmpty(path.basename(argv[1] ?? ""));
113-
return (
114-
entryBasename === "openclaw" ||
115-
entryBasename === "openclaw.mjs" ||
116-
entryBasename === "entry.js" ||
117-
entryBasename === "entry.mjs"
118-
);
119-
}
120-
121-
function getCommandPathFromArgv(argv: string[]): string[] {
122-
const args = argv.slice(2);
123-
const tokens: string[] = [];
124-
for (let i = 0; i < args.length; i += 1) {
125-
const arg = args[i];
126-
if (!arg || arg === FLAG_TERMINATOR) {
127-
break;
128-
}
129-
const consumed = consumeRootOptionToken(args, i);
130-
if (consumed > 0) {
131-
i += consumed - 1;
132-
continue;
133-
}
134-
if (arg.startsWith("-")) {
135-
continue;
136-
}
137-
tokens.push(arg);
138-
if (tokens.length >= 2) {
139-
break;
140-
}
141-
}
142-
return tokens;
143-
}
144-
145-
const SKIP_EAGER_WARMUP_PRIMARY_COMMANDS = new Set([
146-
"agent",
147-
"backup",
148-
"browser",
149-
"completion",
150-
"config",
151-
"directory",
152-
"doctor",
153-
"gateway",
154-
"health",
155-
"hooks",
156-
"logs",
157-
"memory",
158-
"message",
159-
"models",
160-
"pairing",
161-
"plugins",
162-
"secrets",
163-
"sessions",
164-
"status",
165-
"update",
166-
"webhooks",
167-
]);
168-
169-
export function shouldEagerWarmContextWindowCache(argv: string[] = process.argv): boolean {
170-
// Keep this gate tied to the real OpenClaw CLI entrypoints.
171-
//
172-
// This module can also land inside shared dist chunks that are imported from
173-
// plugin-sdk/library surfaces during smoke tests and plugin loading. If we do
174-
// eager warmup for those generic Node script imports, merely importing the
175-
// built plugin-sdk can call ensureOpenClawModelsJson(), which cascades into
176-
// plugin discovery and breaks dist/source singleton assumptions.
177-
if (!isLikelyOpenClawCliProcess(argv)) {
178-
return false;
179-
}
180-
if (isHelpOrVersionInvocation(argv)) {
181-
return false;
182-
}
183-
const [primary] = getCommandPathFromArgv(argv);
184-
return Boolean(primary) && !SKIP_EAGER_WARMUP_PRIMARY_COMMANDS.has(primary);
185-
}
186-
187108
function primeConfiguredContextWindows(): OpenClawConfig | undefined {
188109
if (CONTEXT_WINDOW_RUNTIME_STATE.configuredConfig) {
189110
applyConfiguredContextWindows({

src/tui/tui.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
2828
import { getSlashCommands } from "./commands.js";
2929
import { ChatLog } from "./components/chat-log.js";
3030
import { CustomEditor } from "./components/custom-editor.js";
31-
import { EmbeddedTuiBackend } from "./embedded-backend.js";
3231
import { GatewayChatClient } from "./gateway-chat.js";
3332
import { editorTheme, theme } from "./theme/theme.js";
3433
import type { TuiBackend } from "./tui-backend.js";
@@ -665,15 +664,19 @@ export async function runTui(opts: RunTuiOptions): Promise<TuiResult> {
665664
localBtwRunIds.clear();
666665
};
667666

668-
const client: TuiBackend = opts.backend
669-
? opts.backend
670-
: opts.local
671-
? new EmbeddedTuiBackend()
672-
: await GatewayChatClient.connect({
673-
url: opts.url,
674-
token: opts.token,
675-
password: opts.password,
676-
});
667+
let client: TuiBackend;
668+
if (opts.backend) {
669+
client = opts.backend;
670+
} else if (opts.local) {
671+
const { EmbeddedTuiBackend } = await import("./embedded-backend.js");
672+
client = new EmbeddedTuiBackend();
673+
} else {
674+
client = await GatewayChatClient.connect({
675+
url: opts.url,
676+
token: opts.token,
677+
password: opts.password,
678+
});
679+
}
677680
const previousConsoleSubsystemFilter = isLocalMode
678681
? loggingState.consoleSubsystemFilter
679682
? [...loggingState.consoleSubsystemFilter]

0 commit comments

Comments
 (0)