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
This commit is contained in:
Dallin Romney
2026-06-09 11:30:28 -07:00
committed by GitHub
parent 52154eda0d
commit 8b84e951e5
3 changed files with 82 additions and 1 deletions
+49
View File
@@ -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" }],
+27 -1
View File
@@ -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,
};
}
+6
View File
@@ -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)}`);