perf(inbound): trim dispatch and command startup imports (#52374)

* perf(inbound): trim dispatch and command startup imports

* fix(reply): restore command alias canonicalization

* style(reply): format command context

* fix(reply): restore runtime shim exports

* test(reply): mock ACP route seam

* fix(reply): repair dispatch type seams
This commit is contained in:
Vincent Koc 2026-03-22 13:19:57 -07:00 committed by GitHub
parent 3025760867
commit 5369ea53be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 234 additions and 131 deletions

View File

@ -0,0 +1 @@
export { formatAbortReplyText, tryFastAbortFromMessage } from "./abort.js";

View File

@ -0,0 +1,29 @@
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import { buildCommandContext } from "./commands-context.js";
import { buildTestCtx } from "./test-ctx.js";
describe("buildCommandContext", () => {
it("canonicalizes registered aliases like /id to their primary command", () => {
const ctx = buildTestCtx({
Provider: "discord",
Surface: "discord",
From: "user",
To: "bot",
Body: "/id",
RawBody: "/id",
CommandBody: "/id",
BodyForCommands: "/id",
});
const result = buildCommandContext({
ctx,
cfg: {} as OpenClawConfig,
isGroup: false,
triggerBodyNormalized: "/id",
commandAuthorized: true,
});
expect(result.commandBodyNormalized).toBe("/whoami");
});
});

View File

@ -1,39 +1,10 @@
import type { OpenClawConfig } from "../../config/config.js";
import { resolveCommandAuthorization } from "../command-auth.js";
import { normalizeCommandBody } from "../commands-registry.js";
import type { MsgContext } from "../templating.js";
import type { CommandContext } from "./commands-types.js";
import { stripMentions } from "./mentions.js";
function normalizeCommandBodyLite(raw: string, botUsername?: string): string {
const trimmed = raw.trim();
if (!trimmed.startsWith("/")) {
return trimmed;
}
const newline = trimmed.indexOf("\n");
const singleLine = newline === -1 ? trimmed : trimmed.slice(0, newline).trim();
const colonMatch = singleLine.match(/^\/([^\s:]+)\s*:(.*)$/);
const normalized = colonMatch
? (() => {
const [, command, rest] = colonMatch;
const normalizedRest = rest.trimStart();
return normalizedRest ? `/${command} ${normalizedRest}` : `/${command}`;
})()
: singleLine;
const normalizedBotUsername = botUsername?.trim().toLowerCase();
const mentionMatch = normalizedBotUsername
? normalized.match(/^\/([^\s@]+)@([^\s]+)(.*)$/)
: null;
const mentionNormalized =
mentionMatch && mentionMatch[2].toLowerCase() === normalizedBotUsername
? `/${mentionMatch[1]}${mentionMatch[3] ?? ""}`
: normalized;
return mentionNormalized.replace(/^\/([^\s]+)(.*)$/, (_, command: string, rest: string) => {
return `/${command.toLowerCase()}${rest ?? ""}`;
});
}
export function buildCommandContext(params: {
ctx: MsgContext;
cfg: OpenClawConfig;
@ -53,9 +24,9 @@ export function buildCommandContext(params: {
const channel = (ctx.Provider ?? surface).trim().toLowerCase();
const abortKey = sessionKey ?? (auth.from || undefined) ?? (auth.to || undefined);
const rawBodyNormalized = triggerBodyNormalized;
const commandBodyNormalized = normalizeCommandBodyLite(
const commandBodyNormalized = normalizeCommandBody(
isGroup ? stripMentions(rawBodyNormalized, ctx, cfg, agentId) : rawBodyNormalized,
ctx.BotUsername,
{ botUsername: ctx.BotUsername },
);
return {

View File

@ -6,44 +6,26 @@ import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
import { isAcpSessionKey, resolveAgentIdFromSessionKey } from "../../routing/session-key.js";
import { resolveSendPolicy } from "../../sessions/send-policy.js";
import { shouldHandleTextCommands } from "../commands-registry.js";
import { handleAcpCommand } from "./commands-acp.js";
import { resolveBoundAcpThreadSessionKey } from "./commands-acp/targets.js";
import { handleAllowlistCommand } from "./commands-allowlist.js";
import { handleApproveCommand } from "./commands-approve.js";
import { handleBashCommand } from "./commands-bash.js";
import { handleBtwCommand } from "./commands-btw.js";
import { handleCompactCommand } from "./commands-compact.js";
import { handleConfigCommand, handleDebugCommand } from "./commands-config.js";
import {
handleCommandsListCommand,
handleContextCommand,
handleExportSessionCommand,
handleHelpCommand,
handleStatusCommand,
handleWhoamiCommand,
} from "./commands-info.js";
import { handleMcpCommand } from "./commands-mcp.js";
import { handleModelsCommand } from "./commands-models.js";
import { handlePluginCommand } from "./commands-plugin.js";
import { handlePluginsCommand } from "./commands-plugins.js";
import {
handleAbortTrigger,
handleActivationCommand,
handleFastCommand,
handleRestartCommand,
handleSessionCommand,
handleSendPolicyCommand,
handleStopCommand,
handleUsageCommand,
} from "./commands-session.js";
import { handleSubagentsCommand } from "./commands-subagents.js";
import { handleTtsCommands } from "./commands-tts.js";
import type {
CommandHandler,
CommandHandlerResult,
HandleCommandsParams,
} from "./commands-types.js";
import { routeReply } from "./route-reply.js";
let routeReplyRuntimePromise: Promise<typeof import("./route-reply.runtime.js")> | null = null;
let commandHandlersRuntimePromise: Promise<typeof import("./commands-handlers.runtime.js")> | null =
null;
function loadRouteReplyRuntime() {
routeReplyRuntimePromise ??= import("./route-reply.runtime.js");
return routeReplyRuntimePromise;
}
function loadCommandHandlersRuntime() {
commandHandlersRuntimePromise ??= import("./commands-handlers.runtime.js");
return commandHandlersRuntimePromise;
}
let HANDLERS: CommandHandler[] | null = null;
@ -82,6 +64,7 @@ export async function emitResetCommandHooks(params: {
const to = params.ctx.OriginatingTo || params.command.from || params.command.to;
if (channel && to) {
const { routeReply } = await loadRouteReplyRuntime();
const hookReply = { text: hookEvent.messages.join("\n\n") };
await routeReply({
payload: hookReply,
@ -174,37 +157,7 @@ function resolveSessionEntryForHookSessionKey(
export async function handleCommands(params: HandleCommandsParams): Promise<CommandHandlerResult> {
if (HANDLERS === null) {
HANDLERS = [
// Plugin commands are processed first, before built-in commands
handlePluginCommand,
handleBtwCommand,
handleBashCommand,
handleActivationCommand,
handleSendPolicyCommand,
handleFastCommand,
handleUsageCommand,
handleSessionCommand,
handleRestartCommand,
handleTtsCommands,
handleHelpCommand,
handleCommandsListCommand,
handleStatusCommand,
handleAllowlistCommand,
handleApproveCommand,
handleContextCommand,
handleExportSessionCommand,
handleWhoamiCommand,
handleSubagentsCommand,
handleAcpCommand,
handleMcpCommand,
handlePluginsCommand,
handleConfigCommand,
handleDebugCommand,
handleModelsCommand,
handleStopCommand,
handleCompactCommand,
handleAbortTrigger,
];
HANDLERS = (await loadCommandHandlersRuntime()).loadCommandHandlers();
}
const resetMatch = params.command.commandBodyNormalized.match(/^\/(new|reset)(?:\s|$)/);
const resetRequested = Boolean(resetMatch);

View File

@ -0,0 +1,65 @@
import { handleAcpCommand } from "./commands-acp.js";
import { handleAllowlistCommand } from "./commands-allowlist.js";
import { handleApproveCommand } from "./commands-approve.js";
import { handleBashCommand } from "./commands-bash.js";
import { handleBtwCommand } from "./commands-btw.js";
import { handleCompactCommand } from "./commands-compact.js";
import { handleConfigCommand, handleDebugCommand } from "./commands-config.js";
import {
handleCommandsListCommand,
handleContextCommand,
handleExportSessionCommand,
handleHelpCommand,
handleStatusCommand,
handleWhoamiCommand,
} from "./commands-info.js";
import { handleMcpCommand } from "./commands-mcp.js";
import { handleModelsCommand } from "./commands-models.js";
import { handlePluginCommand } from "./commands-plugin.js";
import { handlePluginsCommand } from "./commands-plugins.js";
import {
handleAbortTrigger,
handleActivationCommand,
handleFastCommand,
handleRestartCommand,
handleSendPolicyCommand,
handleSessionCommand,
handleStopCommand,
handleUsageCommand,
} from "./commands-session.js";
import { handleSubagentsCommand } from "./commands-subagents.js";
import { handleTtsCommands } from "./commands-tts.js";
import type { CommandHandler } from "./commands-types.js";
export function loadCommandHandlers(): CommandHandler[] {
return [
handlePluginCommand,
handleBtwCommand,
handleBashCommand,
handleActivationCommand,
handleSendPolicyCommand,
handleFastCommand,
handleUsageCommand,
handleSessionCommand,
handleRestartCommand,
handleTtsCommands,
handleHelpCommand,
handleCommandsListCommand,
handleStatusCommand,
handleAllowlistCommand,
handleApproveCommand,
handleContextCommand,
handleExportSessionCommand,
handleWhoamiCommand,
handleSubagentsCommand,
handleAcpCommand,
handleMcpCommand,
handlePluginsCommand,
handleConfigCommand,
handleDebugCommand,
handleModelsCommand,
handleStopCommand,
handleCompactCommand,
handleAbortTrigger,
];
}

View File

@ -0,0 +1 @@
export { shouldBypassAcpDispatchForCommand, tryDispatchAcpReply } from "./dispatch-acp.js";

View File

@ -104,6 +104,24 @@ const ttsMocks = vi.hoisted(() => {
};
});
vi.mock("./route-reply.runtime.js", () => ({
isRoutableChannel: (channel: string | undefined) =>
Boolean(
channel &&
[
"telegram",
"slack",
"discord",
"signal",
"imessage",
"whatsapp",
"feishu",
"mattermost",
].includes(channel),
),
routeReply: mocks.routeReply,
}));
vi.mock("./route-reply.js", () => ({
isRoutableChannel: (channel: string | undefined) =>
Boolean(
@ -122,7 +140,7 @@ vi.mock("./route-reply.js", () => ({
routeReply: mocks.routeReply,
}));
vi.mock("./abort.js", () => ({
vi.mock("./abort.runtime.js", () => ({
tryFastAbortFromMessage: mocks.tryFastAbortFromMessage,
formatAbortReplyText: (stoppedSubagents?: number) => {
if (typeof stoppedSubagents !== "number" || stoppedSubagents <= 0) {
@ -138,15 +156,21 @@ vi.mock("../../logging/diagnostic.js", () => ({
logMessageProcessed: diagnosticMocks.logMessageProcessed,
logSessionStateChange: diagnosticMocks.logSessionStateChange,
}));
vi.mock("../../config/sessions.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../config/sessions.js")>();
vi.mock("../../config/sessions/store.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../config/sessions/store.js")>();
return {
...actual,
loadSessionStore: sessionStoreMocks.loadSessionStore,
resolveStorePath: sessionStoreMocks.resolveStorePath,
resolveSessionStoreEntry: sessionStoreMocks.resolveSessionStoreEntry,
};
});
vi.mock("../../config/sessions/paths.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../config/sessions/paths.js")>();
return {
...actual,
resolveStorePath: sessionStoreMocks.resolveStorePath,
};
});
vi.mock("../../plugins/hook-runner-global.js", () => ({
getGlobalHookRunner: () => hookMocks.runner,
@ -192,6 +216,13 @@ vi.mock("../../tts/tts.js", () => ({
normalizeTtsAutoMode: (value: unknown) => ttsMocks.normalizeTtsAutoMode(value),
resolveTtsConfig: (cfg: OpenClawConfig) => ttsMocks.resolveTtsConfig(cfg),
}));
vi.mock("../../tts/tts.runtime.js", () => ({
maybeApplyTtsToPayload: (params: unknown) => ttsMocks.maybeApplyTtsToPayload(params),
}));
vi.mock("../../tts/tts-config.js", () => ({
normalizeTtsAutoMode: (value: unknown) => ttsMocks.normalizeTtsAutoMode(value),
resolveConfiguredTtsMode: (cfg: OpenClawConfig) => ttsMocks.resolveTtsConfig(cfg).mode,
}));
const noAbortResult = { handled: false, aborted: false } as const;
const emptyConfig = {} as OpenClawConfig;

View File

@ -6,13 +6,10 @@ import {
} from "../../bindings/records.js";
import { shouldSuppressLocalExecApprovalPrompt } from "../../channels/plugins/exec-approval-local.js";
import type { OpenClawConfig } from "../../config/config.js";
import {
loadSessionStore,
parseSessionThreadInfo,
resolveSessionStoreEntry,
resolveStorePath,
type SessionEntry,
} from "../../config/sessions.js";
import { parseSessionThreadInfo } from "../../config/sessions/delivery-info.js";
import { resolveStorePath } from "../../config/sessions/paths.js";
import { loadSessionStore, resolveSessionStoreEntry } from "../../config/sessions/store.js";
import type { SessionEntry } from "../../config/sessions/types.js";
import { logVerbose } from "../../globals.js";
import { fireAndForgetHook } from "../../hooks/fire-and-forget.js";
import { createInternalHookEvent, triggerInternalHook } from "../../hooks/internal-hooks.js";
@ -41,19 +38,47 @@ import {
} from "../../plugins/conversation-binding.js";
import { getGlobalHookRunner, getGlobalPluginRegistry } from "../../plugins/hook-runner-global.js";
import { resolveSendPolicy } from "../../sessions/send-policy.js";
import { maybeApplyTtsToPayload, normalizeTtsAutoMode, resolveTtsConfig } from "../../tts/tts.js";
import { normalizeTtsAutoMode, resolveConfiguredTtsMode } from "../../tts/tts-config.js";
import { INTERNAL_MESSAGE_CHANNEL, normalizeMessageChannel } from "../../utils/message-channel.js";
import { getReplyFromConfig } from "../reply.js";
import type { FinalizedMsgContext } from "../templating.js";
import type { GetReplyOptions, ReplyPayload } from "../types.js";
import { formatAbortReplyText, tryFastAbortFromMessage } from "./abort.js";
import { shouldBypassAcpDispatchForCommand, tryDispatchAcpReply } from "./dispatch-acp.js";
import type { BlockReplyContext, GetReplyOptions, ReplyPayload } from "../types.js";
import { shouldSkipDuplicateInbound } from "./inbound-dedupe.js";
import type { ReplyDispatcher, ReplyDispatchKind } from "./reply-dispatcher.js";
import { shouldSuppressReasoningPayload } from "./reply-payloads.js";
import { isRoutableChannel, routeReply } from "./route-reply.js";
import { resolveRunTypingPolicy } from "./typing-policy.js";
let routeReplyRuntimePromise: Promise<typeof import("./route-reply.runtime.js")> | null = null;
let getReplyFromConfigRuntimePromise: Promise<
typeof import("./get-reply-from-config.runtime.js")
> | null = null;
let abortRuntimePromise: Promise<typeof import("./abort.runtime.js")> | null = null;
let dispatchAcpRuntimePromise: Promise<typeof import("./dispatch-acp.runtime.js")> | null = null;
let ttsRuntimePromise: Promise<typeof import("../../tts/tts.runtime.js")> | null = null;
function loadRouteReplyRuntime() {
routeReplyRuntimePromise ??= import("./route-reply.runtime.js");
return routeReplyRuntimePromise;
}
function loadGetReplyFromConfigRuntime() {
getReplyFromConfigRuntimePromise ??= import("./get-reply-from-config.runtime.js");
return getReplyFromConfigRuntimePromise;
}
function loadAbortRuntime() {
abortRuntimePromise ??= import("./abort.runtime.js");
return abortRuntimePromise;
}
function loadDispatchAcpRuntime() {
dispatchAcpRuntimePromise ??= import("./dispatch-acp.runtime.js");
return dispatchAcpRuntimePromise;
}
function loadTtsRuntime() {
ttsRuntimePromise ??= import("../../tts/tts.runtime.js");
return ttsRuntimePromise;
}
const AUDIO_PLACEHOLDER_RE = /^<media:audio>(\s*\([^)]*\))?$/i;
const AUDIO_HEADER_RE = /^\[Audio\b/i;
const normalizeMediaType = (value: string): string => value.split(";")[0]?.trim().toLowerCase();
@ -126,7 +151,7 @@ export async function dispatchReplyFromConfig(params: {
cfg: OpenClawConfig;
dispatcher: ReplyDispatcher;
replyOptions?: Omit<GetReplyOptions, "onToolResult" | "onBlockReply">;
replyResolver?: typeof getReplyFromConfig;
replyResolver?: typeof import("./get-reply-from-config.runtime.js").getReplyFromConfig;
}): Promise<DispatchFromConfigResult> {
const { ctx, cfg, dispatcher } = params;
const diagnosticsEnabled = isDiagnosticsEnabled(cfg);
@ -230,9 +255,10 @@ export async function dispatchReplyFromConfig(params: {
currentSurface === INTERNAL_MESSAGE_CHANNEL &&
(surfaceChannel === INTERNAL_MESSAGE_CHANNEL || !surfaceChannel) &&
ctx.ExplicitDeliverRoute !== true;
const routeReplyRuntime = await loadRouteReplyRuntime();
const shouldRouteToOriginating = Boolean(
!isInternalWebchatTurn &&
isRoutableChannel(originatingChannel) &&
routeReplyRuntime.isRoutableChannel(originatingChannel) &&
originatingTo &&
originatingChannel !== currentSurface,
);
@ -259,7 +285,7 @@ export async function dispatchReplyFromConfig(params: {
if (abortSignal?.aborted) {
return;
}
const result = await routeReply({
const result = await routeReplyRuntime.routeReply({
payload,
channel: originatingChannel,
to: originatingTo,
@ -282,7 +308,7 @@ export async function dispatchReplyFromConfig(params: {
mode: "additive" | "terminal",
): Promise<boolean> => {
if (shouldRouteToOriginating && originatingChannel && originatingTo) {
const result = await routeReply({
const result = await routeReplyRuntime.routeReply({
payload,
channel: originatingChannel,
to: originatingTo,
@ -418,15 +444,16 @@ export async function dispatchReplyFromConfig(params: {
markProcessing();
try {
const fastAbort = await tryFastAbortFromMessage({ ctx, cfg });
const abortRuntime = await loadAbortRuntime();
const fastAbort = await abortRuntime.tryFastAbortFromMessage({ ctx, cfg });
if (fastAbort.handled) {
const payload = {
text: formatAbortReplyText(fastAbort.stoppedSubagents),
text: abortRuntime.formatAbortReplyText(fastAbort.stoppedSubagents),
} satisfies ReplyPayload;
let queuedFinal = false;
let routedFinalCount = 0;
if (shouldRouteToOriginating && originatingChannel && originatingTo) {
const result = await routeReply({
const result = await routeReplyRuntime.routeReply({
payload,
channel: originatingChannel,
to: originatingTo,
@ -456,7 +483,8 @@ export async function dispatchReplyFromConfig(params: {
return { queuedFinal, counts };
}
const bypassAcpForCommand = shouldBypassAcpDispatchForCommand(ctx, cfg);
const dispatchAcpRuntime = await loadDispatchAcpRuntime();
const bypassAcpForCommand = dispatchAcpRuntime.shouldBypassAcpDispatchForCommand(ctx, cfg);
const sendPolicy = resolveSendPolicy({
cfg,
@ -481,7 +509,7 @@ export async function dispatchReplyFromConfig(params: {
}
const shouldSendToolSummaries = ctx.ChatType !== "group" && ctx.CommandSource !== "native";
const acpDispatch = await tryDispatchAcpReply({
const acpDispatch = await dispatchAcpRuntime.tryDispatchAcpReply({
ctx,
cfg,
dispatcher,
@ -508,6 +536,7 @@ export async function dispatchReplyFromConfig(params: {
// TTS audio separately from the accumulated block content.
let accumulatedBlockText = "";
let blockCount = 0;
const { maybeApplyTtsToPayload } = await loadTtsRuntime();
const resolveToolDeliveryPayload = (payload: ReplyPayload): ReplyPayload | null => {
if (
@ -547,7 +576,9 @@ export async function dispatchReplyFromConfig(params: {
systemEvent: shouldRouteToOriginating,
});
const replyResult = await (params.replyResolver ?? getReplyFromConfig)(
const replyResolver =
params.replyResolver ?? (await loadGetReplyFromConfigRuntime()).getReplyFromConfig;
const replyResult = await replyResolver(
ctx,
{
...params.replyOptions,
@ -575,12 +606,12 @@ export async function dispatchReplyFromConfig(params: {
};
return run();
},
onBlockReply: (payload: ReplyPayload, context) => {
onBlockReply: (payload: ReplyPayload, context?: BlockReplyContext) => {
const run = async () => {
// Suppress reasoning payloads — channels using this generic dispatch
// path (WhatsApp, web, etc.) do not have a dedicated reasoning lane.
// Telegram has its own dispatch path that handles reasoning splitting.
if (shouldSuppressReasoningPayload(payload)) {
if (payload.isReasoning === true) {
return;
}
// Accumulate block text for TTS generation after streaming.
@ -617,7 +648,7 @@ export async function dispatchReplyFromConfig(params: {
// Command handling prepared a trailing prompt after ACP in-place reset.
// Route that tail through ACP now (same turn) instead of embedded dispatch.
ctx.AcpDispatchTailAfterReset = false;
const acpTailDispatch = await tryDispatchAcpReply({
const acpTailDispatch = await dispatchAcpRuntime.tryDispatchAcpReply({
ctx,
cfg,
dispatcher,
@ -647,7 +678,7 @@ export async function dispatchReplyFromConfig(params: {
for (const reply of replies) {
// Suppress reasoning payloads from channel delivery — channels using this
// generic dispatch path do not have a dedicated reasoning lane.
if (shouldSuppressReasoningPayload(reply)) {
if (reply.isReasoning === true) {
continue;
}
const ttsReply = await maybeApplyTtsToPayload({
@ -660,7 +691,7 @@ export async function dispatchReplyFromConfig(params: {
});
if (shouldRouteToOriginating && originatingChannel && originatingTo) {
// Route final reply to originating channel.
const result = await routeReply({
const result = await routeReplyRuntime.routeReply({
payload: ttsReply,
channel: originatingChannel,
to: originatingTo,
@ -685,7 +716,7 @@ export async function dispatchReplyFromConfig(params: {
}
}
const ttsMode = resolveTtsConfig(cfg).mode ?? "final";
const ttsMode = resolveConfiguredTtsMode(cfg);
// Generate TTS-only reply after block streaming completes (when there's no final reply).
// This handles the case where block streaming succeeds and drops final payloads,
// but we still want TTS audio to be generated from the accumulated block content.
@ -712,7 +743,7 @@ export async function dispatchReplyFromConfig(params: {
audioAsVoice: ttsSyntheticReply.audioAsVoice,
};
if (shouldRouteToOriginating && originatingChannel && originatingTo) {
const result = await routeReply({
const result = await routeReplyRuntime.routeReply({
payload: ttsOnlyPayload,
channel: originatingChannel,
to: originatingTo,

View File

@ -0,0 +1 @@
export { getReplyFromConfig } from "../reply.js";

View File

@ -1 +1 @@
export { routeReply } from "./route-reply.js";
export { isRoutableChannel, routeReply } from "./route-reply.js";

19
src/tts/tts-config.ts Normal file
View File

@ -0,0 +1,19 @@
import type { OpenClawConfig } from "../config/config.js";
import type { TtsAutoMode, TtsMode } from "../config/types.tts.js";
const TTS_AUTO_MODES = new Set<TtsAutoMode>(["off", "always", "inbound", "tagged"]);
export function normalizeTtsAutoMode(value: unknown): TtsAutoMode | undefined {
if (typeof value !== "string") {
return undefined;
}
const normalized = value.trim().toLowerCase();
if (TTS_AUTO_MODES.has(normalized as TtsAutoMode)) {
return normalized as TtsAutoMode;
}
return undefined;
}
export function resolveConfiguredTtsMode(cfg: OpenClawConfig): TtsMode {
return cfg.messages?.tts?.mode ?? "final";
}

1
src/tts/tts.runtime.ts Normal file
View File

@ -0,0 +1 @@
export { maybeApplyTtsToPayload } from "./tts.js";