Skip to content

Commit 8b84e95

Browse files
authored
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
1 parent 52154ed commit 8b84e95

3 files changed

Lines changed: 82 additions & 1 deletion

File tree

src/tui/embedded-backend.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const createSessionGoalMock = vi.fn();
1111
const clearSessionGoalMock = vi.fn();
1212
const getSessionGoalMock = vi.fn();
1313
const updateSessionGoalStatusMock = vi.fn();
14+
const ensureRuntimePluginsLoadedMock = vi.fn();
1415
const listSessionsFromStoreAsyncMock = vi.fn(
1516
async (_options?: unknown): Promise<{ sessions: unknown[] }> => ({ sessions: [] }),
1617
);
@@ -87,13 +88,18 @@ vi.mock("../config/sessions.js", () => ({
8788
}));
8889

8990
vi.mock("../agents/agent-scope.js", () => ({
91+
resolveAgentWorkspaceDir: (_cfg: unknown, agentId: string) => `/tmp/openclaw-agent-${agentId}`,
9092
resolveDefaultAgentId: (cfg?: {
9193
agents?: { list?: Array<{ id?: string; default?: boolean }> };
9294
}) =>
9395
cfg?.agents?.list?.find((agent) => agent.default)?.id ?? cfg?.agents?.list?.[0]?.id ?? "main",
9496
resolveSessionAgentId: () => "main",
9597
}));
9698

99+
vi.mock("../agents/runtime-plugins.js", () => ({
100+
ensureRuntimePluginsLoaded: (...args: unknown[]) => ensureRuntimePluginsLoadedMock(...args),
101+
}));
102+
97103
vi.mock("../agents/defaults.js", () => ({
98104
DEFAULT_PROVIDER: "openai",
99105
}));
@@ -230,6 +236,7 @@ describe("EmbeddedTuiBackend", () => {
230236
status,
231237
tokensUsed: 0,
232238
}));
239+
ensureRuntimePluginsLoadedMock.mockReset();
233240
listSessionsFromStoreAsyncMock.mockReset();
234241
listSessionsFromStoreAsyncMock.mockResolvedValue({ sessions: [] });
235242
loadCombinedSessionStoreForGatewayMock.mockReset();
@@ -604,6 +611,48 @@ describe("EmbeddedTuiBackend", () => {
604611
expect(loadSessionEntryMock).toHaveBeenCalledWith("global", { agentId: "work" });
605612
});
606613

614+
it("loads runtime plugins for the send-path workspace before returning embedded history", async () => {
615+
const cfg = { agents: { list: [{ id: "main" }] } };
616+
loadSessionEntryMock.mockReturnValue({
617+
cfg,
618+
canonicalKey: "agent:main:main",
619+
storePath: "/tmp/openclaw-sessions.json",
620+
entry: { spawnedWorkspaceDir: "/tmp/openclaw-custom-workspace" },
621+
});
622+
623+
const { EmbeddedTuiBackend } = await import("./embedded-backend.js");
624+
const backend = new EmbeddedTuiBackend();
625+
626+
await expect(backend.loadHistory({ sessionKey: "agent:main:main" })).resolves.toMatchObject({
627+
runtimePluginsPrewarm: { status: "warmed" },
628+
});
629+
expect(ensureRuntimePluginsLoadedMock).toHaveBeenCalledWith({
630+
config: cfg,
631+
workspaceDir: "/tmp/openclaw-agent-main",
632+
});
633+
});
634+
635+
it("returns embedded history when runtime plugin loading fails", async () => {
636+
ensureRuntimePluginsLoadedMock.mockImplementationOnce(() => {
637+
throw new Error("runtime unavailable");
638+
});
639+
loadSessionEntryMock.mockReturnValue({
640+
cfg: {},
641+
canonicalKey: "agent:main:main",
642+
storePath: "/tmp/openclaw-sessions.json",
643+
entry: {},
644+
});
645+
646+
const { EmbeddedTuiBackend } = await import("./embedded-backend.js");
647+
const backend = new EmbeddedTuiBackend();
648+
649+
await expect(backend.loadHistory({ sessionKey: "agent:main:main" })).resolves.toMatchObject({
650+
sessionKey: "agent:main:main",
651+
messages: [],
652+
runtimePluginsPrewarm: { status: "failed", error: "Error: runtime unavailable" },
653+
});
654+
});
655+
607656
it("passes selected-agent global scope into local chat turns", async () => {
608657
agentCommandFromIngressMock.mockResolvedValueOnce({
609658
payloads: [{ text: "done" }],

src/tui/embedded-backend.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,19 @@
22
import { randomUUID } from "node:crypto";
33
import type { SessionsPatchResult } from "../../packages/gateway-protocol/src/index.js";
44
import { agentCommandFromIngress } from "../agents/agent-command.js";
5-
import { resolveDefaultAgentId, resolveSessionAgentId } from "../agents/agent-scope.js";
5+
import {
6+
resolveAgentWorkspaceDir,
7+
resolveDefaultAgentId,
8+
resolveSessionAgentId,
9+
} from "../agents/agent-scope.js";
610
import { ensureContextWindowCacheLoaded } from "../agents/context.js";
711
import { DEFAULT_PROVIDER } from "../agents/defaults.js";
812
import {
913
buildAllowedModelSet,
1014
buildConfiguredModelCatalog,
1115
resolveThinkingDefault,
1216
} from "../agents/model-selection.js";
17+
import { ensureRuntimePluginsLoaded } from "../agents/runtime-plugins.js";
1318
import { parseGoalCommand } from "../auto-reply/reply/commands-goal.js";
1419
import { createDefaultDeps } from "../cli/deps.js";
1520
import { getRuntimeConfig } from "../config/config.js";
@@ -128,6 +133,22 @@ function shouldLoadFullGatewayCatalogForReplaceMode(cfg: OpenClawConfig) {
128133
return cfg.models?.mode === "replace" && hasProviderWildcardModelAllowlist(cfg);
129134
}
130135

136+
function ensureEmbeddedHistoryRuntimePluginsLoaded(params: {
137+
cfg: OpenClawConfig;
138+
sessionAgentId: string;
139+
}): { status: "warmed" } | { status: "failed"; error: string } {
140+
try {
141+
const workspaceDir = resolveAgentWorkspaceDir(params.cfg, params.sessionAgentId);
142+
ensureRuntimePluginsLoaded({
143+
config: params.cfg,
144+
workspaceDir,
145+
});
146+
return { status: "warmed" };
147+
} catch (err) {
148+
return { status: "failed", error: String(err) };
149+
}
150+
}
151+
131152
async function loadEmbeddedTuiModelCatalog(cfg: OpenClawConfig) {
132153
const configuredCatalog = resolveConfiguredReplaceModeCatalog(cfg);
133154
if (configuredCatalog !== undefined) {
@@ -414,6 +435,10 @@ export class EmbeddedTuiBackend implements TuiBackend {
414435
config: cfg,
415436
agentId: opts.agentId,
416437
});
438+
const runtimePluginsPrewarm = ensureEmbeddedHistoryRuntimePluginsLoaded({
439+
cfg,
440+
sessionAgentId,
441+
});
417442
const resolvedSessionModel = resolveSessionModelRef(cfg, entry, sessionAgentId);
418443
const max = Math.min(1000, typeof opts.limit === "number" ? opts.limit : 200);
419444
const maxHistoryBytes = getMaxChatHistoryMessagesBytes();
@@ -478,6 +503,7 @@ export class EmbeddedTuiBackend implements TuiBackend {
478503
thinkingLevel,
479504
fastMode: entry?.fastMode,
480505
verboseLevel: sessionInfo.verboseLevel,
506+
runtimePluginsPrewarm,
481507
};
482508
}
483509

src/tui/tui-session-actions.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,7 @@ export function createSessionActions(context: SessionActionContext) {
423423
verboseLevel?: string;
424424
traceLevel?: string;
425425
inFlightRun?: { runId?: unknown; text?: unknown };
426+
runtimePluginsPrewarm?: { status?: string; error?: string };
426427
};
427428
const sessionInfo = record.sessionInfo;
428429
if (sessionInfo?.key && sessionInfo.key !== state.currentSessionKey) {
@@ -536,6 +537,11 @@ export function createSessionActions(context: SessionActionContext) {
536537
setActivityStatus("streaming");
537538
}
538539
state.historyLoaded = true;
540+
if (record.runtimePluginsPrewarm?.status === "failed") {
541+
chatLog.addSystem(
542+
`runtime prewarm failed: ${record.runtimePluginsPrewarm.error ?? "unknown"}`,
543+
);
544+
}
539545
void rememberSessionKey?.(state.currentSessionKey);
540546
} catch (err) {
541547
chatLog.addSystem(`history failed: ${String(err)}`);

0 commit comments

Comments
 (0)