From 95fe63e63f7fcbbc523948f507b90d54c4eb3f22 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 6 Apr 2026 18:46:21 +0100 Subject: [PATCH] perf(auto-reply): fast-path getReply test bootstrap --- ...irective.directive-behavior.e2e-harness.ts | 5 +- src/auto-reply/reply.test-harness.ts | 61 ++++++- src/auto-reply/reply/get-reply-fast-path.ts | 126 ++++++++++++++ .../reply/get-reply.fast-path.runtime.test.ts | 64 +++++++ .../reply/get-reply.fast-path.test.ts | 164 ++++++++++++++++++ src/auto-reply/reply/get-reply.ts | 96 ++++++---- 6 files changed, 475 insertions(+), 41 deletions(-) create mode 100644 src/auto-reply/reply/get-reply-fast-path.ts create mode 100644 src/auto-reply/reply/get-reply.fast-path.runtime.test.ts create mode 100644 src/auto-reply/reply/get-reply.fast-path.test.ts diff --git a/src/auto-reply/reply.directive.directive-behavior.e2e-harness.ts b/src/auto-reply/reply.directive.directive-behavior.e2e-harness.ts index c4a19f0a21e..aa75ea67cec 100644 --- a/src/auto-reply/reply.directive.directive-behavior.e2e-harness.ts +++ b/src/auto-reply/reply.directive.directive-behavior.e2e-harness.ts @@ -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, extra: Record = {}, ) { - 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 = { diff --git a/src/auto-reply/reply.test-harness.ts b/src/auto-reply/reply.test-harness.ts index ee899dce7f0..79d4538da0a 100644 --- a/src/auto-reply/reply.test-harness.ts +++ b/src/auto-reply/reply.test-harness.ts @@ -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 { diff --git a/src/auto-reply/reply/get-reply-fast-path.ts b/src/auto-reply/reply/get-reply-fast-path.ts new file mode 100644 index 00000000000..6ee7812f4d9 --- /dev/null +++ b/src/auto-reply/reply/get-reply-fast-path.ts @@ -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(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 = { [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, + }; +} diff --git a/src/auto-reply/reply/get-reply.fast-path.runtime.test.ts b/src/auto-reply/reply/get-reply.fast-path.runtime.test.ts new file mode 100644 index 00000000000..91ded9b027b --- /dev/null +++ b/src/auto-reply/reply/get-reply.fast-path.runtime.test.ts @@ -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"); + }); + }); +}); diff --git a/src/auto-reply/reply/get-reply.fast-path.test.ts b/src/auto-reply/reply/get-reply.fast-path.test.ts new file mode 100644 index 00000000000..c7b7be8aefc --- /dev/null +++ b/src/auto-reply/reply/get-reply.fast-path.test.ts @@ -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(); + 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(); + 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 { + 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", + }), + }), + }), + }), + ); + }); +}); diff --git a/src/auto-reply/reply/get-reply.ts b/src/auto-reply/reply/get-reply.ts index d1e16007b40..c7fbf2d20d0 100644 --- a/src/auto-reply/reply/get-reply.ts +++ b/src/auto-reply/reply/get-reply.ts @@ -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 { 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,