mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 04:47:03 -06:00
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:
@@ -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" }],
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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)}`);
|
||||
|
||||
Reference in New Issue
Block a user