From 8b84e951e5a613464d6b5d78747016c44ced1bfb Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Tue, 9 Jun 2026 11:30:28 -0700 Subject: [PATCH] perf(tui): prewarm runtime plugins before first send (#90782) * perf: prewarm TUI runtime plugins before first send * fix: satisfy TUI prewarm lint * fix(tui): clarify runtime warmup submit block * refactor(tui): warm embedded runtime during history load * fix(tui): align runtime prewarm workspace --- src/tui/embedded-backend.test.ts | 49 ++++++++++++++++++++++++++++++++ src/tui/embedded-backend.ts | 28 +++++++++++++++++- src/tui/tui-session-actions.ts | 6 ++++ 3 files changed, 82 insertions(+), 1 deletion(-) diff --git a/src/tui/embedded-backend.test.ts b/src/tui/embedded-backend.test.ts index 33a8c5ee0bda..c3c2603b80f6 100644 --- a/src/tui/embedded-backend.test.ts +++ b/src/tui/embedded-backend.test.ts @@ -11,6 +11,7 @@ const createSessionGoalMock = vi.fn(); const clearSessionGoalMock = vi.fn(); const getSessionGoalMock = vi.fn(); const updateSessionGoalStatusMock = vi.fn(); +const ensureRuntimePluginsLoadedMock = vi.fn(); const listSessionsFromStoreAsyncMock = vi.fn( async (_options?: unknown): Promise<{ sessions: unknown[] }> => ({ sessions: [] }), ); @@ -87,6 +88,7 @@ vi.mock("../config/sessions.js", () => ({ })); vi.mock("../agents/agent-scope.js", () => ({ + resolveAgentWorkspaceDir: (_cfg: unknown, agentId: string) => `/tmp/openclaw-agent-${agentId}`, resolveDefaultAgentId: (cfg?: { agents?: { list?: Array<{ id?: string; default?: boolean }> }; }) => @@ -94,6 +96,10 @@ vi.mock("../agents/agent-scope.js", () => ({ resolveSessionAgentId: () => "main", })); +vi.mock("../agents/runtime-plugins.js", () => ({ + ensureRuntimePluginsLoaded: (...args: unknown[]) => ensureRuntimePluginsLoadedMock(...args), +})); + vi.mock("../agents/defaults.js", () => ({ DEFAULT_PROVIDER: "openai", })); @@ -230,6 +236,7 @@ describe("EmbeddedTuiBackend", () => { status, tokensUsed: 0, })); + ensureRuntimePluginsLoadedMock.mockReset(); listSessionsFromStoreAsyncMock.mockReset(); listSessionsFromStoreAsyncMock.mockResolvedValue({ sessions: [] }); loadCombinedSessionStoreForGatewayMock.mockReset(); @@ -604,6 +611,48 @@ describe("EmbeddedTuiBackend", () => { expect(loadSessionEntryMock).toHaveBeenCalledWith("global", { agentId: "work" }); }); + it("loads runtime plugins for the send-path workspace before returning embedded history", async () => { + const cfg = { agents: { list: [{ id: "main" }] } }; + loadSessionEntryMock.mockReturnValue({ + cfg, + canonicalKey: "agent:main:main", + storePath: "/tmp/openclaw-sessions.json", + entry: { spawnedWorkspaceDir: "/tmp/openclaw-custom-workspace" }, + }); + + const { EmbeddedTuiBackend } = await import("./embedded-backend.js"); + const backend = new EmbeddedTuiBackend(); + + await expect(backend.loadHistory({ sessionKey: "agent:main:main" })).resolves.toMatchObject({ + runtimePluginsPrewarm: { status: "warmed" }, + }); + expect(ensureRuntimePluginsLoadedMock).toHaveBeenCalledWith({ + config: cfg, + workspaceDir: "/tmp/openclaw-agent-main", + }); + }); + + it("returns embedded history when runtime plugin loading fails", async () => { + ensureRuntimePluginsLoadedMock.mockImplementationOnce(() => { + throw new Error("runtime unavailable"); + }); + loadSessionEntryMock.mockReturnValue({ + cfg: {}, + canonicalKey: "agent:main:main", + storePath: "/tmp/openclaw-sessions.json", + entry: {}, + }); + + const { EmbeddedTuiBackend } = await import("./embedded-backend.js"); + const backend = new EmbeddedTuiBackend(); + + await expect(backend.loadHistory({ sessionKey: "agent:main:main" })).resolves.toMatchObject({ + sessionKey: "agent:main:main", + messages: [], + runtimePluginsPrewarm: { status: "failed", error: "Error: runtime unavailable" }, + }); + }); + it("passes selected-agent global scope into local chat turns", async () => { agentCommandFromIngressMock.mockResolvedValueOnce({ payloads: [{ text: "done" }], diff --git a/src/tui/embedded-backend.ts b/src/tui/embedded-backend.ts index ff0c4ab74734..6993a7f9eaec 100644 --- a/src/tui/embedded-backend.ts +++ b/src/tui/embedded-backend.ts @@ -2,7 +2,11 @@ import { randomUUID } from "node:crypto"; import type { SessionsPatchResult } from "../../packages/gateway-protocol/src/index.js"; import { agentCommandFromIngress } from "../agents/agent-command.js"; -import { resolveDefaultAgentId, resolveSessionAgentId } from "../agents/agent-scope.js"; +import { + resolveAgentWorkspaceDir, + resolveDefaultAgentId, + resolveSessionAgentId, +} from "../agents/agent-scope.js"; import { ensureContextWindowCacheLoaded } from "../agents/context.js"; import { DEFAULT_PROVIDER } from "../agents/defaults.js"; import { @@ -10,6 +14,7 @@ import { buildConfiguredModelCatalog, resolveThinkingDefault, } from "../agents/model-selection.js"; +import { ensureRuntimePluginsLoaded } from "../agents/runtime-plugins.js"; import { parseGoalCommand } from "../auto-reply/reply/commands-goal.js"; import { createDefaultDeps } from "../cli/deps.js"; import { getRuntimeConfig } from "../config/config.js"; @@ -128,6 +133,22 @@ function shouldLoadFullGatewayCatalogForReplaceMode(cfg: OpenClawConfig) { return cfg.models?.mode === "replace" && hasProviderWildcardModelAllowlist(cfg); } +function ensureEmbeddedHistoryRuntimePluginsLoaded(params: { + cfg: OpenClawConfig; + sessionAgentId: string; +}): { status: "warmed" } | { status: "failed"; error: string } { + try { + const workspaceDir = resolveAgentWorkspaceDir(params.cfg, params.sessionAgentId); + ensureRuntimePluginsLoaded({ + config: params.cfg, + workspaceDir, + }); + return { status: "warmed" }; + } catch (err) { + return { status: "failed", error: String(err) }; + } +} + async function loadEmbeddedTuiModelCatalog(cfg: OpenClawConfig) { const configuredCatalog = resolveConfiguredReplaceModeCatalog(cfg); if (configuredCatalog !== undefined) { @@ -414,6 +435,10 @@ export class EmbeddedTuiBackend implements TuiBackend { config: cfg, agentId: opts.agentId, }); + const runtimePluginsPrewarm = ensureEmbeddedHistoryRuntimePluginsLoaded({ + cfg, + sessionAgentId, + }); const resolvedSessionModel = resolveSessionModelRef(cfg, entry, sessionAgentId); const max = Math.min(1000, typeof opts.limit === "number" ? opts.limit : 200); const maxHistoryBytes = getMaxChatHistoryMessagesBytes(); @@ -478,6 +503,7 @@ export class EmbeddedTuiBackend implements TuiBackend { thinkingLevel, fastMode: entry?.fastMode, verboseLevel: sessionInfo.verboseLevel, + runtimePluginsPrewarm, }; } diff --git a/src/tui/tui-session-actions.ts b/src/tui/tui-session-actions.ts index 6765b6fe263f..3bf19ecca684 100644 --- a/src/tui/tui-session-actions.ts +++ b/src/tui/tui-session-actions.ts @@ -423,6 +423,7 @@ export function createSessionActions(context: SessionActionContext) { verboseLevel?: string; traceLevel?: string; inFlightRun?: { runId?: unknown; text?: unknown }; + runtimePluginsPrewarm?: { status?: string; error?: string }; }; const sessionInfo = record.sessionInfo; if (sessionInfo?.key && sessionInfo.key !== state.currentSessionKey) { @@ -536,6 +537,11 @@ export function createSessionActions(context: SessionActionContext) { setActivityStatus("streaming"); } state.historyLoaded = true; + if (record.runtimePluginsPrewarm?.status === "failed") { + chatLog.addSystem( + `runtime prewarm failed: ${record.runtimePluginsPrewarm.error ?? "unknown"}`, + ); + } void rememberSessionKey?.(state.currentSessionKey); } catch (err) { chatLog.addSystem(`history failed: ${String(err)}`);