perf(auto-reply): fast-path getReply test bootstrap

This commit is contained in:
Peter Steinberger
2026-04-06 18:46:21 +01:00
parent a211f09259
commit 95fe63e63f
6 changed files with 475 additions and 41 deletions

View File

@@ -13,6 +13,7 @@ import {
loadModelCatalogMock,
runEmbeddedPiAgentMock,
} from "./reply.directive.directive-behavior.e2e-mocks.js";
import { markCompleteReplyConfig } from "./reply/get-reply-fast-path.js";
export const MAIN_SESSION_KEY = "agent:main:main";
type RunPreparedReply = typeof import("./reply/get-reply-run.js").runPreparedReply;
@@ -136,7 +137,7 @@ export function makeWhatsAppDirectiveConfig(
defaults: Record<string, unknown>,
extra: Record<string, unknown> = {},
) {
return {
return markCompleteReplyConfig({
agents: {
defaults: {
workspace: path.join(home, "openclaw"),
@@ -146,7 +147,7 @@ export function makeWhatsAppDirectiveConfig(
channels: { whatsapp: { allowFrom: ["*"] } },
session: { store: sessionStorePath(home) },
...extra,
};
});
}
export const AUTHORIZED_WHATSAPP_COMMAND = {

View File

@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterAll, beforeAll, vi, type Mock } from "vitest";
import { markCompleteReplyConfig } from "./reply/get-reply-fast-path.js";
export type ReplyRuntimeMocks = {
runEmbeddedPiAgent: Mock;
@@ -63,6 +64,62 @@ vi.mock("../agents/pi-embedded.runtime.js", () => ({
waitForEmbeddedPiRunEnd: vi.fn(async () => undefined),
}));
vi.mock("./reply/agent-runner.runtime.js", () => ({
runReplyAgent: async (params: {
commandBody: string;
followupRun: {
prompt: string;
run: {
agentDir: string;
agentId: string;
config: unknown;
execOverrides?: unknown;
inputProvenance?: unknown;
messageProvider?: string;
model: string;
ownerNumbers?: string[];
provider: string;
reasoningLevel?: unknown;
senderIsOwner?: boolean;
sessionFile: string;
sessionId: string;
sessionKey: string;
skillsSnapshot?: unknown;
thinkLevel?: unknown;
timeoutMs?: number;
verboseLevel?: unknown;
workspaceDir: string;
bashElevated?: unknown;
};
};
}) => {
const result = await replyRuntimeMockState.mocks.runEmbeddedPiAgent({
prompt: params.followupRun.prompt || params.commandBody,
agentDir: params.followupRun.run.agentDir,
agentId: params.followupRun.run.agentId,
config: params.followupRun.run.config,
execOverrides: params.followupRun.run.execOverrides,
inputProvenance: params.followupRun.run.inputProvenance,
messageProvider: params.followupRun.run.messageProvider,
model: params.followupRun.run.model,
ownerNumbers: params.followupRun.run.ownerNumbers,
provider: params.followupRun.run.provider,
reasoningLevel: params.followupRun.run.reasoningLevel,
senderIsOwner: params.followupRun.run.senderIsOwner,
sessionFile: params.followupRun.run.sessionFile,
sessionId: params.followupRun.run.sessionId,
sessionKey: params.followupRun.run.sessionKey,
skillsSnapshot: params.followupRun.run.skillsSnapshot,
thinkLevel: params.followupRun.run.thinkLevel,
timeoutMs: params.followupRun.run.timeoutMs,
verboseLevel: params.followupRun.run.verboseLevel,
workspaceDir: params.followupRun.run.workspaceDir,
bashElevated: params.followupRun.run.bashElevated,
});
return result?.payloads?.[0];
},
}));
type HomeEnvSnapshot = {
HOME: string | undefined;
USERPROFILE: string | undefined;
@@ -140,7 +197,7 @@ export function createTempHomeHarness(options: { prefix: string; beforeEachCase?
}
export function makeReplyConfig(home: string) {
return {
return markCompleteReplyConfig({
agents: {
defaults: {
model: "anthropic/claude-opus-4-6",
@@ -153,7 +210,7 @@ export function makeReplyConfig(home: string) {
},
},
session: { store: path.join(home, "sessions.json") },
};
});
}
export function createReplyRuntimeMocks(): ReplyRuntimeMocks {

View File

@@ -0,0 +1,126 @@
import crypto from "node:crypto";
import path from "node:path";
import { normalizeChatType } from "../../channels/chat-type.js";
import type { OpenClawConfig } from "../../config/config.js";
import { applyMergePatch } from "../../config/merge-patch.js";
import type { SessionEntry } from "../../config/sessions/types.js";
import type { MsgContext, TemplateContext } from "../templating.js";
import { stripMentions, stripStructuralPrefixes } from "./mentions.js";
import type { SessionInitResult } from "./session.js";
const COMPLETE_REPLY_CONFIG_SYMBOL = Symbol.for("openclaw.reply.complete-config");
type ReplyConfigWithMarker = OpenClawConfig & {
[COMPLETE_REPLY_CONFIG_SYMBOL]?: true;
};
function resolveFastSessionKey(ctx: MsgContext): string {
const existing = ctx.SessionKey?.trim();
if (existing) {
return existing;
}
const provider = ctx.Provider?.trim() || ctx.Surface?.trim() || "main";
const destination = ctx.To?.trim() || ctx.From?.trim() || "default";
return `agent:main:${provider}:${destination}`;
}
export function markCompleteReplyConfig<T extends OpenClawConfig>(config: T): T {
Object.defineProperty(config as ReplyConfigWithMarker, COMPLETE_REPLY_CONFIG_SYMBOL, {
value: true,
configurable: true,
enumerable: false,
});
return config;
}
export function isCompleteReplyConfig(config: unknown): config is OpenClawConfig {
return Boolean(
config &&
typeof config === "object" &&
(config as ReplyConfigWithMarker)[COMPLETE_REPLY_CONFIG_SYMBOL] === true,
);
}
export function resolveGetReplyConfig(params: {
loadConfig: () => OpenClawConfig;
isFastTestEnv: boolean;
configOverride?: OpenClawConfig;
}): OpenClawConfig {
const { configOverride } = params;
if (configOverride == null) {
return params.loadConfig();
}
if (params.isFastTestEnv && isCompleteReplyConfig(configOverride)) {
return configOverride;
}
return applyMergePatch(params.loadConfig(), configOverride) as OpenClawConfig;
}
export function shouldUseReplyFastTestBootstrap(params: {
isFastTestEnv: boolean;
configOverride?: OpenClawConfig;
}): boolean {
return params.isFastTestEnv && isCompleteReplyConfig(params.configOverride);
}
export function initFastReplySessionState(params: {
ctx: MsgContext;
cfg: OpenClawConfig;
agentId: string;
commandAuthorized: boolean;
workspaceDir: string;
}): SessionInitResult {
const { ctx, cfg, agentId, commandAuthorized, workspaceDir } = params;
const sessionScope = cfg.session?.scope ?? "per-sender";
const sessionKey = resolveFastSessionKey(ctx);
const sessionId = crypto.randomUUID();
const commandSource = ctx.BodyForCommands ?? ctx.CommandBody ?? ctx.RawBody ?? ctx.Body ?? "";
const triggerBodyNormalized = stripStructuralPrefixes(commandSource).trim();
const normalizedChatType = normalizeChatType(ctx.ChatType);
const isGroup = normalizedChatType != null && normalizedChatType !== "direct";
const strippedForReset = isGroup
? stripMentions(triggerBodyNormalized, ctx, cfg, agentId)
: triggerBodyNormalized;
const resetMatch = strippedForReset.match(/^\/(new|reset)(?:\s|$)/i);
const resetTriggered = Boolean(resetMatch);
const bodyStripped = resetTriggered
? strippedForReset.slice(resetMatch?.[0].length ?? 0).trimStart()
: (ctx.BodyForAgent ?? ctx.Body ?? "");
const now = Date.now();
const sessionFile = path.join(workspaceDir, ".openclaw", "sessions", `${sessionId}.jsonl`);
const sessionEntry: SessionEntry = {
sessionId,
sessionFile,
updatedAt: now,
...(normalizedChatType ? { chatType: normalizedChatType } : {}),
...(ctx.Provider?.trim() ? { channel: ctx.Provider.trim() } : {}),
...(ctx.GroupSubject?.trim() ? { subject: ctx.GroupSubject.trim() } : {}),
...(ctx.GroupChannel?.trim() ? { groupChannel: ctx.GroupChannel.trim() } : {}),
};
const sessionStore: Record<string, SessionEntry> = { [sessionKey]: sessionEntry };
const sessionCtx: TemplateContext = {
...ctx,
SessionKey: sessionKey,
CommandAuthorized: commandAuthorized,
BodyStripped: bodyStripped,
...(normalizedChatType ? { ChatType: normalizedChatType } : {}),
};
return {
sessionCtx,
sessionEntry,
previousSessionEntry: undefined,
sessionStore,
sessionKey,
sessionId,
isNewSession: resetTriggered || !ctx.SessionKey,
resetTriggered,
systemSent: false,
abortedLastRun: false,
storePath: cfg.session?.store?.trim() ?? "",
sessionScope,
groupResolution: undefined,
isGroup,
bodyStripped,
triggerBodyNormalized,
};
}

View File

@@ -0,0 +1,64 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import {
createReplyRuntimeMocks,
createTempHomeHarness,
installReplyRuntimeMocks,
makeEmbeddedTextResult,
makeReplyConfig,
resetReplyRuntimeMocks,
} from "../reply.test-harness.js";
let getReplyFromConfig: typeof import("../reply.js").getReplyFromConfig;
const agentMocks = createReplyRuntimeMocks();
const { withTempHome } = createTempHomeHarness({ prefix: "openclaw-getreply-fast-" });
installReplyRuntimeMocks(agentMocks);
describe("getReplyFromConfig fast-path runtime", () => {
beforeEach(async () => {
vi.resetModules();
vi.stubEnv("OPENCLAW_TEST_FAST", "1");
resetReplyRuntimeMocks(agentMocks);
({ getReplyFromConfig } = await import("../reply.js"));
});
afterEach(() => {
vi.clearAllMocks();
vi.unstubAllEnvs();
});
it("keeps old-style runtime tests fast with marked temp-home configs", async () => {
await withTempHome(async (home) => {
let seenPrompt: string | undefined;
agentMocks.runEmbeddedPiAgent.mockImplementation(async (params) => {
seenPrompt = params.prompt;
return makeEmbeddedTextResult("ok");
});
const res = await getReplyFromConfig(
{
Body: "hello",
BodyForAgent: "hello",
RawBody: "hello",
CommandBody: "hello",
From: "+1001",
To: "+2000",
MediaPaths: ["/tmp/a.png", "/tmp/b.png"],
MediaUrls: ["/tmp/a.png", "/tmp/b.png"],
SessionKey: "agent:main:whatsapp:+2000",
Provider: "whatsapp",
Surface: "whatsapp",
ChatType: "direct",
},
{},
makeReplyConfig(home) as OpenClawConfig,
);
const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toBe("ok");
expect(seenPrompt).toContain("[media attached: 2 files]");
expect(seenPrompt).toContain("hello");
});
});
});

View File

@@ -0,0 +1,164 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import type { MsgContext } from "../templating.js";
import { markCompleteReplyConfig } from "./get-reply-fast-path.js";
import "./get-reply.test-runtime-mocks.js";
const mocks = vi.hoisted(() => ({
ensureAgentWorkspace: vi.fn(),
initSessionState: vi.fn(),
resolveReplyDirectives: vi.fn(),
}));
vi.mock("../../agents/workspace.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../agents/workspace.js")>();
return {
...actual,
ensureAgentWorkspace: (...args: unknown[]) => mocks.ensureAgentWorkspace(...args),
};
});
vi.mock("./directive-handling.defaults.js", () => ({
resolveDefaultModel: vi.fn(() => ({
defaultProvider: "openai",
defaultModel: "gpt-4o-mini",
aliasIndex: new Map(),
})),
}));
vi.mock("./get-reply-directives.js", () => ({
resolveReplyDirectives: (...args: unknown[]) => mocks.resolveReplyDirectives(...args),
}));
vi.mock("./get-reply-inline-actions.js", () => ({
handleInlineActions: vi.fn(async () => ({ kind: "reply", reply: { text: "ok" } })),
}));
vi.mock("./session.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./session.js")>();
return {
...actual,
initSessionState: (...args: unknown[]) => mocks.initSessionState(...args),
};
});
let getReplyFromConfig: typeof import("./get-reply.js").getReplyFromConfig;
let loadConfigMock: typeof import("../../config/config.js").loadConfig;
async function loadFreshGetReplyModuleForTest() {
vi.resetModules();
({ getReplyFromConfig } = await import("./get-reply.js"));
({ loadConfig: loadConfigMock } = await import("../../config/config.js"));
}
function buildCtx(overrides: Partial<MsgContext> = {}): MsgContext {
return {
Provider: "telegram",
Surface: "telegram",
ChatType: "direct",
Body: "hello",
BodyForAgent: "hello",
RawBody: "hello",
CommandBody: "hello",
SessionKey: "agent:main:telegram:123",
From: "telegram:user:42",
To: "telegram:123",
Timestamp: 1710000000000,
...overrides,
};
}
describe("getReplyFromConfig fast test bootstrap", () => {
beforeEach(async () => {
await loadFreshGetReplyModuleForTest();
vi.stubEnv("OPENCLAW_TEST_FAST", "1");
mocks.ensureAgentWorkspace.mockReset();
mocks.initSessionState.mockReset();
mocks.resolveReplyDirectives.mockReset();
vi.mocked(loadConfigMock).mockReset();
vi.mocked(loadConfigMock).mockReturnValue({});
mocks.resolveReplyDirectives.mockResolvedValue({ kind: "reply", reply: { text: "ok" } });
mocks.initSessionState.mockResolvedValue({
sessionCtx: {},
sessionEntry: {},
previousSessionEntry: {},
sessionStore: {},
sessionKey: "agent:main:telegram:123",
sessionId: "session-1",
isNewSession: false,
resetTriggered: false,
systemSent: false,
abortedLastRun: false,
storePath: "/tmp/sessions.json",
sessionScope: "per-chat",
groupResolution: undefined,
isGroup: false,
triggerBodyNormalized: "",
bodyStripped: "",
});
});
afterEach(() => {
vi.unstubAllEnvs();
});
it("skips loadConfig, workspace bootstrap, and session bootstrap for marked test configs", async () => {
const home = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-fast-reply-"));
const cfg = markCompleteReplyConfig({
agents: {
defaults: {
model: "anthropic/claude-opus-4-6",
workspace: path.join(home, "openclaw"),
},
},
channels: { telegram: { allowFrom: ["*"] } },
session: { store: path.join(home, "sessions.json") },
} as OpenClawConfig);
await expect(getReplyFromConfig(buildCtx(), undefined, cfg)).resolves.toEqual({ text: "ok" });
expect(vi.mocked(loadConfigMock)).not.toHaveBeenCalled();
expect(mocks.ensureAgentWorkspace).not.toHaveBeenCalled();
expect(mocks.initSessionState).not.toHaveBeenCalled();
expect(mocks.resolveReplyDirectives).toHaveBeenCalledWith(
expect.objectContaining({
cfg,
}),
);
});
it("still merges partial config overrides against loadConfig()", async () => {
vi.mocked(loadConfigMock).mockReturnValue({
channels: {
telegram: {
botToken: "resolved-telegram-token",
},
},
} satisfies OpenClawConfig);
await getReplyFromConfig(buildCtx(), undefined, {
agents: {
defaults: {
userTimezone: "America/New_York",
},
},
} as OpenClawConfig);
expect(vi.mocked(loadConfigMock)).toHaveBeenCalledOnce();
expect(mocks.initSessionState).toHaveBeenCalledOnce();
expect(mocks.resolveReplyDirectives).toHaveBeenCalledWith(
expect.objectContaining({
cfg: expect.objectContaining({
channels: expect.objectContaining({
telegram: expect.objectContaining({
botToken: "resolved-telegram-token",
}),
}),
agents: expect.objectContaining({
defaults: expect.objectContaining({
userTimezone: "America/New_York",
}),
}),
}),
}),
);
});
});

View File

@@ -1,3 +1,4 @@
import fs from "node:fs/promises";
import {
resolveAgentDir,
resolveAgentWorkspaceDir,
@@ -9,7 +10,6 @@ import { resolveAgentTimeoutMs } from "../../agents/timeout.js";
import { DEFAULT_AGENT_WORKSPACE_DIR, ensureAgentWorkspace } from "../../agents/workspace.js";
import { resolveChannelModelOverride } from "../../channels/model-overrides.js";
import { type OpenClawConfig, loadConfig } from "../../config/config.js";
import { applyMergePatch } from "../../config/merge-patch.js";
import { defaultRuntime } from "../../runtime.js";
import { normalizeStringEntries } from "../../shared/string-normalization.js";
import { resolveCommandAuthorization } from "../command-auth.js";
@@ -18,6 +18,11 @@ import { SILENT_REPLY_TOKEN } from "../tokens.js";
import type { GetReplyOptions, ReplyPayload } from "../types.js";
import { resolveDefaultModel } from "./directive-handling.defaults.js";
import { resolveReplyDirectives } from "./get-reply-directives.js";
import {
initFastReplySessionState,
resolveGetReplyConfig,
shouldUseReplyFastTestBootstrap,
} from "./get-reply-fast-path.js";
import { handleInlineActions } from "./get-reply-inline-actions.js";
import { runPreparedReply } from "./get-reply-run.js";
import { finalizeInboundContext } from "./inbound-context.js";
@@ -135,10 +140,15 @@ export async function getReplyFromConfig(
configOverride?: OpenClawConfig,
): Promise<ReplyPayload | ReplyPayload[] | undefined> {
const isFastTestEnv = process.env.OPENCLAW_TEST_FAST === "1";
const cfg =
configOverride == null
? loadConfig()
: (applyMergePatch(loadConfig(), configOverride) as OpenClawConfig);
const cfg = resolveGetReplyConfig({
loadConfig,
isFastTestEnv,
configOverride,
});
const useFastTestBootstrap = shouldUseReplyFastTestBootstrap({
isFastTestEnv,
configOverride,
});
const targetSessionKey =
ctx.CommandSource === "native" ? ctx.CommandTargetSessionKey?.trim() : undefined;
const agentSessionKey = targetSessionKey || ctx.SessionKey;
@@ -181,10 +191,12 @@ export async function getReplyFromConfig(
}
const workspaceDirRaw = resolveAgentWorkspaceDir(cfg, agentId) ?? DEFAULT_AGENT_WORKSPACE_DIR;
const workspace = await ensureAgentWorkspace({
dir: workspaceDirRaw,
ensureBootstrapFiles: !agentCfg?.skipBootstrap && !isFastTestEnv,
});
const workspace = useFastTestBootstrap
? (await fs.mkdir(workspaceDirRaw, { recursive: true }), { dir: workspaceDirRaw })
: await ensureAgentWorkspace({
dir: workspaceDirRaw,
ensureBootstrapFiles: !agentCfg?.skipBootstrap && !isFastTestEnv,
});
const workspaceDir = workspace.dir;
const agentDir = resolveAgentDir(cfg, agentId);
const timeoutMs = resolveAgentTimeoutMs({ cfg, overrideSeconds: opts?.timeoutOverrideSeconds });
@@ -227,11 +239,19 @@ export async function getReplyFromConfig(
cfg,
commandAuthorized,
});
const sessionState = await initSessionState({
ctx: finalized,
cfg,
commandAuthorized,
});
const sessionState = useFastTestBootstrap
? initFastReplySessionState({
ctx: finalized,
cfg,
agentId,
commandAuthorized,
workspaceDir,
})
: await initSessionState({
ctx: finalized,
cfg,
commandAuthorized,
});
let {
sessionCtx,
sessionEntry,
@@ -434,32 +454,34 @@ export async function getReplyFromConfig(
abortedLastRun = inlineActionResult.abortedLastRun ?? abortedLastRun;
// Allow plugins to intercept and return a synthetic reply before the LLM runs.
const { getGlobalHookRunner } = await loadHookRunnerGlobal();
const hookRunner = getGlobalHookRunner();
if (hookRunner?.hasHooks("before_agent_reply")) {
const { resolveOriginMessageProvider } = await loadOriginRouting();
const hookMessageProvider = resolveOriginMessageProvider({
originatingChannel: sessionCtx.OriginatingChannel,
provider: sessionCtx.Provider,
});
const hookResult = await hookRunner.runBeforeAgentReply(
{ cleanedBody },
{
agentId,
sessionKey: agentSessionKey,
sessionId,
workspaceDir,
messageProvider: hookMessageProvider,
trigger: opts?.isHeartbeat ? "heartbeat" : "user",
channelId: hookMessageProvider,
},
);
if (hookResult?.handled) {
return hookResult.reply ?? { text: SILENT_REPLY_TOKEN };
if (!useFastTestBootstrap) {
const { getGlobalHookRunner } = await loadHookRunnerGlobal();
const hookRunner = getGlobalHookRunner();
if (hookRunner?.hasHooks("before_agent_reply")) {
const { resolveOriginMessageProvider } = await loadOriginRouting();
const hookMessageProvider = resolveOriginMessageProvider({
originatingChannel: sessionCtx.OriginatingChannel,
provider: sessionCtx.Provider,
});
const hookResult = await hookRunner.runBeforeAgentReply(
{ cleanedBody },
{
agentId,
sessionKey: agentSessionKey,
sessionId,
workspaceDir,
messageProvider: hookMessageProvider,
trigger: opts?.isHeartbeat ? "heartbeat" : "user",
channelId: hookMessageProvider,
},
);
if (hookResult?.handled) {
return hookResult.reply ?? { text: SILENT_REPLY_TOKEN };
}
}
}
if (sessionKey && hasInboundMedia(ctx)) {
if (!useFastTestBootstrap && sessionKey && hasInboundMedia(ctx)) {
const { stageSandboxMedia } = await loadStageSandboxMediaRuntime();
await stageSandboxMedia({
ctx,