refactor: normalize inbound context

This commit is contained in:
Peter Steinberger
2026-01-17 04:04:05 +00:00
parent 9f4b7a1683
commit a2b5b1f0cb
31 changed files with 155 additions and 35 deletions

View File

@@ -308,6 +308,7 @@ export function createSessionStatusTool(opts?: {
const isGroup =
resolved.entry.chatType === "group" ||
resolved.entry.chatType === "channel" ||
resolved.entry.chatType === "room" ||
resolved.key.startsWith("group:") ||
resolved.key.includes(":group:") ||

View File

@@ -120,12 +120,18 @@ export async function resolveReplyDirectives(params: {
// Prefer CommandBody/RawBody (clean message without structural context) for directive parsing.
// Keep `Body`/`BodyStripped` as the best-available prompt text (may include context).
const commandSource =
sessionCtx.BodyForCommands ??
sessionCtx.CommandBody ??
sessionCtx.RawBody ??
sessionCtx.Transcript ??
sessionCtx.BodyStripped ??
sessionCtx.Body ??
ctx.BodyForCommands ??
ctx.CommandBody ??
ctx.RawBody ??
"";
const promptSource = sessionCtx.BodyForAgent ?? sessionCtx.BodyStripped ?? sessionCtx.Body ?? "";
const commandText = commandSource || promptSource;
const command = buildCommandContext({
ctx,
cfg,
@@ -162,7 +168,7 @@ export async function resolveReplyDirectives(params: {
.filter((alias): alias is string => Boolean(alias))
.filter((alias) => !reservedCommands.has(alias.toLowerCase()));
const allowStatusDirective = allowTextCommands && command.isAuthorizedSender;
let parsedDirectives = parseInlineDirectives(commandSource, {
let parsedDirectives = parseInlineDirectives(commandText, {
modelAliases: configuredAliases,
allowStatusDirective,
});
@@ -253,6 +259,7 @@ export async function resolveReplyDirectives(params: {
cleanedBody = stripInlineStatus(cleanedBody).cleaned;
}
sessionCtx.BodyForAgent = cleanedBody;
sessionCtx.Body = cleanedBody;
sessionCtx.BodyStripped = cleanedBody;
@@ -402,7 +409,7 @@ export async function resolveReplyDirectives(params: {
return {
kind: "continue",
result: {
commandSource,
commandSource: commandText,
command,
allowTextCommands,
skillCommands,

View File

@@ -135,7 +135,9 @@ export async function handleInlineActions(params: {
].filter((entry): entry is string => Boolean(entry));
const rewrittenBody = promptParts.join("\n\n");
ctx.Body = rewrittenBody;
ctx.BodyForAgent = rewrittenBody;
sessionCtx.Body = rewrittenBody;
sessionCtx.BodyForAgent = rewrittenBody;
sessionCtx.BodyStripped = rewrittenBody;
cleanedBody = rewrittenBody;
}
@@ -153,6 +155,7 @@ export async function handleInlineActions(params: {
if (inlineCommand) {
cleanedBody = inlineCommand.cleaned;
sessionCtx.Body = cleanedBody;
sessionCtx.BodyForAgent = cleanedBody;
sessionCtx.BodyStripped = cleanedBody;
}

View File

@@ -295,7 +295,10 @@ export async function runPreparedReply(
abortKey: command.abortKey,
messageId: sessionCtx.MessageSid,
});
const isGroupSession = sessionEntry?.chatType === "group" || sessionEntry?.chatType === "room";
const isGroupSession =
sessionEntry?.chatType === "group" ||
sessionEntry?.chatType === "channel" ||
sessionEntry?.chatType === "room";
const isMainSession = !isGroupSession && sessionKey === normalizeMainKey(sessionCfg?.mainKey);
prefixedBodyBase = await prependSystemEvents({
cfg,

View File

@@ -28,10 +28,10 @@ describe("formatInboundBodyWithSenderMeta", () => {
);
});
it("preserves escaped newline style when body uses literal \\\\n", () => {
it("appends with a real newline even if the body contains literal \\\\n", () => {
const ctx: MsgContext = { ChatType: "group", SenderName: "Bob", SenderId: "+222" };
expect(formatInboundBodyWithSenderMeta({ ctx, body: "[X] one\\n[X] two" })).toBe(
"[X] one\\n[X] two\\n[from: Bob (+222)]",
"[X] one\\n[X] two\n[from: Bob (+222)]",
);
});

View File

@@ -1,28 +1,21 @@
import type { MsgContext } from "../templating.js";
import { normalizeChatType } from "../../channels/chat-type.js";
export function formatInboundBodyWithSenderMeta(params: { body: string; ctx: MsgContext }): string {
const body = params.body;
if (!body.trim()) return body;
const chatType = params.ctx.ChatType?.trim().toLowerCase();
const chatType = normalizeChatType(params.ctx.ChatType);
if (!chatType || chatType === "direct") return body;
if (hasSenderMetaLine(body)) return body;
const senderLabel = formatSenderLabel(params.ctx);
if (!senderLabel) return body;
const lineBreak = resolveBodyLineBreak(body);
return `${body}${lineBreak}[from: ${senderLabel}]`;
}
function resolveBodyLineBreak(body: string): string {
const hasEscaped = body.includes("\\n");
const hasNewline = body.includes("\n");
if (hasEscaped && !hasNewline) return "\\n";
return "\n";
return `${body}\n[from: ${senderLabel}]`;
}
function hasSenderMetaLine(body: string): boolean {
return /(^|\n|\\n)\[from:/i.test(body);
return /(^|\n)\[from:/i.test(body);
}
function formatSenderLabel(ctx: MsgContext): string | null {

View File

@@ -25,8 +25,10 @@ import {
import { normalizeMainKey } from "../../routing/session-key.js";
import { resolveCommandAuthorization } from "../command-auth.js";
import type { MsgContext, TemplateContext } from "../templating.js";
import { normalizeChatType } from "../../channels/chat-type.js";
import { stripMentions, stripStructuralPrefixes } from "./mentions.js";
import { formatInboundBodyWithSenderMeta } from "./inbound-sender-meta.js";
import { normalizeInboundTextNewlines } from "./inbound-text.js";
export type SessionInitResult = {
sessionCtx: TemplateContext;
@@ -126,10 +128,11 @@ export async function initSessionState(params: {
let persistedProviderOverride: string | undefined;
const groupResolution = resolveGroupSessionKey(sessionCtxForState) ?? undefined;
const isGroup = ctx.ChatType?.trim().toLowerCase() === "group" || Boolean(groupResolution);
const normalizedChatType = normalizeChatType(ctx.ChatType);
const isGroup = normalizedChatType != null && normalizedChatType !== "direct" ? true : Boolean(groupResolution);
// Prefer CommandBody/RawBody (clean message) for command detection; fall back
// to Body which may contain structural context (history, sender labels).
const commandSource = ctx.CommandBody ?? ctx.RawBody ?? ctx.Body ?? "";
const commandSource = ctx.BodyForCommands ?? ctx.CommandBody ?? ctx.RawBody ?? ctx.Body ?? "";
const triggerBodyNormalized = stripStructuralPrefixes(commandSource).trim().toLowerCase();
// Use CommandBody/RawBody for reset trigger matching (clean message without structural context).
@@ -308,7 +311,15 @@ export async function initSessionState(params: {
// RawBody is reserved for command/directive parsing and may omit context.
BodyStripped: formatInboundBodyWithSenderMeta({
ctx,
body: bodyStripped ?? ctx.Body ?? ctx.CommandBody ?? ctx.RawBody ?? "",
body: normalizeInboundTextNewlines(
bodyStripped ??
ctx.BodyForAgent ??
ctx.Body ??
ctx.CommandBody ??
ctx.RawBody ??
ctx.BodyForCommands ??
"",
),
}),
SessionId: sessionId,
IsNewSession: isNewSession ? "true" : "false",

View File

@@ -253,6 +253,7 @@ export function buildStatusMessage(args: StatusArgs): string {
const isGroupSession =
entry?.chatType === "group" ||
entry?.chatType === "channel" ||
entry?.chatType === "room" ||
Boolean(args.sessionKey?.includes(":group:")) ||
Boolean(args.sessionKey?.includes(":channel:")) ||

View File

@@ -8,6 +8,11 @@ export type OriginatingChannelType = ChannelId | InternalMessageChannel;
export type MsgContext = {
Body?: string;
/**
* Agent prompt body (may include envelope/history/context). Prefer this for prompt shaping.
* Should use real newlines (`\n`), not escaped `\\n`.
*/
BodyForAgent?: string;
/**
* Raw message body without structural context (history, sender labels).
* Legacy alias for CommandBody. Falls back to Body if not set.
@@ -17,6 +22,11 @@ export type MsgContext = {
* Prefer for command detection; RawBody is treated as legacy alias.
*/
CommandBody?: string;
/**
* Command parsing body. Prefer this over CommandBody/RawBody when set.
* Should be the "clean" text (no history/sender context).
*/
BodyForCommands?: string;
CommandArgs?: CommandArgs;
From?: string;
To?: string;
@@ -46,6 +56,8 @@ export type MsgContext = {
Prompt?: string;
MaxChars?: number;
ChatType?: string;
/** Human label for envelope headers (conversation label, not sender). */
ConversationLabel?: string;
GroupSubject?: string;
GroupRoom?: string;
GroupSpace?: string;

View File

@@ -110,7 +110,9 @@ const formatAge = (ms: number | null | undefined) => {
function classifyKey(key: string, entry?: SessionEntry): SessionRow["kind"] {
if (key === "global") return "global";
if (key === "unknown") return "unknown";
if (entry?.chatType === "group" || entry?.chatType === "room") return "group";
if (entry?.chatType === "group" || entry?.chatType === "channel" || entry?.chatType === "room") {
return "group";
}
if (key.startsWith("group:") || key.includes(":group:") || key.includes(":channel:")) {
return "group";
}

View File

@@ -19,7 +19,9 @@ import type { HeartbeatStatus, SessionStatus, StatusSummary } from "./status.typ
const classifyKey = (key: string, entry?: SessionEntry): SessionStatus["kind"] => {
if (key === "global") return "global";
if (key === "unknown") return "unknown";
if (entry?.chatType === "group" || entry?.chatType === "room") return "group";
if (entry?.chatType === "group" || entry?.chatType === "channel" || entry?.chatType === "room") {
return "group";
}
if (key.startsWith("group:") || key.includes(":group:") || key.includes(":channel:")) {
return "group";
}

View File

@@ -136,6 +136,6 @@ export function resolveGroupSessionKey(ctx: MsgContext): GroupKeyResolution | nu
legacyKey,
channel: resolvedProvider,
id: id || raw || from,
chatType: resolvedKind === "channel" ? "room" : "group",
chatType: resolvedKind === "channel" ? "channel" : "group",
};
}

View File

@@ -7,7 +7,12 @@ export type SessionScope = "per-sender" | "global";
export type SessionChannelId = ChannelId | "webchat";
export type SessionChatType = "direct" | "group" | "room";
export type SessionChatType =
| "direct"
| "group"
| "channel"
// Legacy alias for "channel".
| "room";
export type SessionEntry = {
/**

View File

@@ -39,7 +39,13 @@ export const SessionSchema = z
.object({
channel: z.string().optional(),
chatType: z
.union([z.literal("direct"), z.literal("group"), z.literal("room")])
.union([
z.literal("direct"),
z.literal("group"),
z.literal("channel"),
// Legacy alias for "channel".
z.literal("room"),
])
.optional(),
keyPrefix: z.string().optional(),
})

View File

@@ -221,13 +221,16 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
const ctxPayload = {
Body: combinedBody,
BodyForAgent: combinedBody,
RawBody: baseText,
CommandBody: baseText,
BodyForCommands: baseText,
From: effectiveFrom,
To: effectiveTo,
SessionKey: autoThreadContext?.SessionKey ?? threadKeys.sessionKey,
AccountId: route.accountId,
ChatType: isDirectMessage ? "direct" : "group",
ChatType: isDirectMessage ? "direct" : "channel",
ConversationLabel: fromLabel,
SenderName: data.member?.nickname ?? author.globalName ?? author.username,
SenderId: author.id,
SenderUsername: author.username,

View File

@@ -569,16 +569,20 @@ async function dispatchDiscordCommandInteraction(params: {
id: isDirectMessage ? user.id : channelId,
},
});
const conversationLabel = isDirectMessage ? (user.globalName ?? user.username) : channelId;
const ctxPayload = {
Body: prompt,
BodyForAgent: prompt,
CommandBody: prompt,
BodyForCommands: prompt,
CommandArgs: commandArgs,
From: isDirectMessage ? `discord:${user.id}` : `group:${channelId}`,
To: `slash:${user.id}`,
SessionKey: `agent:${route.agentId}:${sessionPrefix}:${user.id}`,
CommandTargetSessionKey: route.sessionKey,
AccountId: route.accountId,
ChatType: isDirectMessage ? "direct" : "group",
ChatType: isDirectMessage ? "direct" : isGroupDm ? "group" : "channel",
ConversationLabel: conversationLabel,
GroupSubject: isGuild ? interaction.guild?.name : undefined,
GroupSystemPrompt: isGuild
? (() => {

View File

@@ -59,7 +59,9 @@ export function loadSessionEntry(sessionKey: string) {
export function classifySessionKey(key: string, entry?: SessionEntry): GatewaySessionRow["kind"] {
if (key === "global") return "global";
if (key === "unknown") return "unknown";
if (entry?.chatType === "group" || entry?.chatType === "room") return "group";
if (entry?.chatType === "group" || entry?.chatType === "channel" || entry?.chatType === "room") {
return "group";
}
if (key.startsWith("group:") || key.includes(":group:") || key.includes(":channel:")) {
return "group";
}

View File

@@ -15,7 +15,7 @@ export type GatewaySessionRow = {
subject?: string;
room?: string;
space?: string;
chatType?: "direct" | "group" | "room";
chatType?: "direct" | "group" | "channel" | "room";
updatedAt: number | null;
sessionId?: string;
systemSent?: boolean;

View File

@@ -389,13 +389,16 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
const imessageTo = (isGroup ? chatTarget : undefined) || `imessage:${sender}`;
const ctxPayload = {
Body: combinedBody,
BodyForAgent: combinedBody,
RawBody: bodyText,
CommandBody: bodyText,
BodyForCommands: bodyText,
From: isGroup ? `group:${chatId}` : `imessage:${sender}`,
To: imessageTo,
SessionKey: route.sessionKey,
AccountId: route.accountId,
ChatType: isGroup ? "group" : "direct",
ConversationLabel: fromLabel,
GroupSubject: isGroup ? (message.chat_name ?? undefined) : undefined,
GroupMembers: isGroup ? (message.participants ?? []).filter(Boolean).join(", ") : undefined,
SenderName: senderNormalized,

View File

@@ -104,8 +104,10 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
const signalTo = entry.isGroup ? `group:${entry.groupId}` : `signal:${entry.senderRecipient}`;
const ctxPayload = {
Body: combinedBody,
BodyForAgent: combinedBody,
RawBody: entry.bodyText,
CommandBody: entry.bodyText,
BodyForCommands: entry.bodyText,
From: entry.isGroup
? `group:${entry.groupId ?? "unknown"}`
: `signal:${entry.senderRecipient}`,
@@ -113,6 +115,7 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
SessionKey: route.sessionKey,
AccountId: route.accountId,
ChatType: entry.isGroup ? "group" : "direct",
ConversationLabel: fromLabel,
GroupSubject: entry.isGroup ? (entry.groupName ?? undefined) : undefined,
SenderName: entry.senderName,
SenderId: entry.senderDisplay,

View File

@@ -14,6 +14,7 @@ import { upsertChannelPairingRequest } from "../../../pairing/pairing-store.js";
import { resolveAgentRoute } from "../../../routing/resolve-route.js";
import { resolveThreadSessionKeys } from "../../../routing/session-key.js";
import { resolveMentionGating } from "../../../channels/mention-gating.js";
import { resolveConversationLabel } from "../../../channels/conversation-label.js";
import type { ResolvedSlackAccount } from "../../accounts.js";
import { reactSlackMessage } from "../../actions.js";
@@ -330,7 +331,13 @@ export async function prepareSlackMessage(params: {
contextKey: `slack:message:${message.channel}:${message.ts ?? "unknown"}`,
});
const envelopeFrom = isDirectMessage ? senderName : roomLabel;
const envelopeFrom =
resolveConversationLabel({
ChatType: isDirectMessage ? "direct" : "channel",
SenderName: senderName,
GroupSubject: isRoomish ? roomLabel : undefined,
From: slackFrom,
}) ?? (isDirectMessage ? senderName : roomLabel);
const textWithId = `${rawBody}\n[slack message id: ${message.ts} channel: ${message.channel}]`;
const body = formatAgentEnvelope({
channel: "Slack",
@@ -399,13 +406,16 @@ export async function prepareSlackMessage(params: {
const ctxPayload = {
Body: combinedBody,
BodyForAgent: combinedBody,
RawBody: rawBody,
CommandBody: rawBody,
BodyForCommands: rawBody,
From: slackFrom,
To: slackTo,
SessionKey: sessionKey,
AccountId: route.accountId,
ChatType: isDirectMessage ? "direct" : isRoom ? "room" : "group",
ChatType: isDirectMessage ? "direct" : "channel",
ConversationLabel: envelopeFrom,
GroupSubject: isRoomish ? roomLabel : undefined,
GroupSystemPrompt: isRoomish ? groupSystemPrompt : undefined,
SenderName: senderName,

View File

@@ -18,6 +18,7 @@ import {
upsertChannelPairingRequest,
} from "../../pairing/pairing-store.js";
import { resolveAgentRoute } from "../../routing/resolve-route.js";
import { resolveConversationLabel } from "../../channels/conversation-label.js";
import type { ResolvedSlackAccount } from "../accounts.js";
@@ -337,14 +338,27 @@ export function registerSlackMonitorSlashCommands(params: {
const ctxPayload = {
Body: prompt,
BodyForAgent: prompt,
CommandArgs: commandArgs,
BodyForCommands: prompt,
From: isDirectMessage
? `slack:${command.user_id}`
: isRoom
? `slack:channel:${command.channel_id}`
: `slack:group:${command.channel_id}`,
To: `slash:${command.user_id}`,
ChatType: isDirectMessage ? "direct" : isRoom ? "room" : "group",
ChatType: isDirectMessage ? "direct" : "channel",
ConversationLabel:
resolveConversationLabel({
ChatType: isDirectMessage ? "direct" : "channel",
SenderName: senderName,
GroupSubject: isRoomish ? roomLabel : undefined,
From: isDirectMessage
? `slack:${command.user_id}`
: isRoom
? `slack:channel:${command.channel_id}`
: `slack:group:${command.channel_id}`,
}) ?? (isDirectMessage ? senderName : roomLabel),
GroupSubject: isRoomish ? roomLabel : undefined,
GroupSystemPrompt: isRoomish ? groupSystemPrompt : undefined,
SenderName: senderName,

View File

@@ -324,9 +324,12 @@ export const buildTelegramMessageContext = async ({
}]\n${replyTarget.body}\n[/Replying]`
: "";
const groupLabel = isGroup ? buildGroupLabel(msg, chatId, resolvedThreadId) : undefined;
const conversationLabel = isGroup
? (groupLabel ?? `group:${chatId}`)
: buildSenderLabel(msg, senderId || chatId);
const body = formatAgentEnvelope({
channel: "Telegram",
from: isGroup ? (groupLabel ?? `group:${chatId}`) : buildSenderLabel(msg, senderId || chatId),
from: conversationLabel,
timestamp: msg.date ? msg.date * 1000 : undefined,
body: `${bodyText}${replySuffix}`,
});
@@ -357,13 +360,16 @@ export const buildTelegramMessageContext = async ({
const commandBody = normalizeCommandBody(rawBody, { botUsername });
const ctxPayload = {
Body: combinedBody,
BodyForAgent: combinedBody,
RawBody: rawBody,
CommandBody: commandBody,
BodyForCommands: commandBody,
From: isGroup ? buildTelegramGroupFrom(chatId, resolvedThreadId) : `telegram:${chatId}`,
To: `telegram:${chatId}`,
SessionKey: route.sessionKey,
AccountId: route.accountId,
ChatType: isGroup ? "group" : "direct",
ConversationLabel: conversationLabel,
GroupSubject: isGroup ? (msg.chat.title ?? undefined) : undefined,
GroupSystemPrompt: isGroup ? groupSystemPrompt : undefined,
SenderName: buildSenderName(msg),

View File

@@ -248,12 +248,18 @@ export const registerTelegramNativeCommands = ({
].filter((entry): entry is string => Boolean(entry));
const groupSystemPrompt =
systemPromptParts.length > 0 ? systemPromptParts.join("\n\n") : undefined;
const conversationLabel = isGroup
? (msg.chat.title ? `${msg.chat.title} id:${chatId}` : `group:${chatId}`)
: (buildSenderName(msg) ?? String(senderId || chatId));
const ctxPayload = {
Body: prompt,
BodyForAgent: prompt,
CommandArgs: commandArgs,
BodyForCommands: prompt,
From: isGroup ? buildTelegramGroupFrom(chatId, resolvedThreadId) : `telegram:${chatId}`,
To: `slash:${senderId || chatId}`,
ChatType: isGroup ? "group" : "direct",
ConversationLabel: conversationLabel,
GroupSubject: isGroup ? (msg.chat.title ?? undefined) : undefined,
GroupSystemPrompt: isGroup ? groupSystemPrompt : undefined,
SenderName: buildSenderName(msg),

View File

@@ -9,6 +9,7 @@ import {
import { listSkillCommandsForAgents } from "../auto-reply/skill-commands.js";
import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js";
import * as replyModule from "../auto-reply/reply.js";
import { expectInboundContextContract } from "../../test/helpers/inbound-contract.js";
import { createTelegramBot, getTelegramSequentialKey } from "./bot.js";
import { resolveTelegramFetch } from "./fetch.js";
@@ -583,6 +584,7 @@ describe("createTelegramBot", () => {
expect(replySpy).toHaveBeenCalledTimes(1);
const payload = replySpy.mock.calls[0][0];
expectInboundContextContract(payload);
expect(payload.WasMentioned).toBe(true);
expect(payload.Body).toMatch(/^\[Telegram Test Group id:7 2025-01-09T00:00Z\]/);
expect(payload.SenderName).toBe("Ada");
@@ -625,6 +627,7 @@ describe("createTelegramBot", () => {
expect(replySpy).toHaveBeenCalledTimes(1);
const payload = replySpy.mock.calls[0][0];
expectInboundContextContract(payload);
expect(payload.Body).toMatch(/^\[Telegram Ops id:42 2025-01-09T00:00Z\]/);
expect(payload.SenderName).toBe("Ada Lovelace");
expect(payload.SenderId).toBe("99");

View File

@@ -15,6 +15,7 @@ vi.mock("../agents/pi-embedded.js", () => ({
}));
import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js";
import { expectInboundContextContract } from "../../test/helpers/inbound-contract.js";
import { resetLogger, setLoggerOverride } from "../logging.js";
import { monitorWebChannel, SILENT_REPLY_TOKEN } from "./auto-reply.js";
import { resetBaileysMocks, resetLoadConfigMock, setLoadConfigMock } from "./test-helpers.js";
@@ -184,6 +185,7 @@ describe("web auto-reply", () => {
expect(payload.Body).not.toContain("Alice (+111): first");
expect(payload.Body).not.toContain("[message_id: g-always-1]");
expect(payload.Body).toContain("second");
expectInboundContextContract(payload);
expect(payload.SenderName).toBe("Bob");
expect(payload.SenderE164).toBe("+222");
expect(reply).toHaveBeenCalledTimes(1);

View File

@@ -22,6 +22,7 @@ import { logVerbose, shouldLogVerbose } from "../../../globals.js";
import type { getChildLogger } from "../../../logging.js";
import type { resolveAgentRoute } from "../../../routing/resolve-route.js";
import { jidToE164, normalizeE164 } from "../../../utils.js";
import { normalizeChatType } from "../../../channels/chat-type.js";
import { newConnectionId } from "../../reconnect.js";
import { formatError } from "../../session.js";
import { deliverWebReply } from "../deliver-reply.js";
@@ -197,8 +198,10 @@ export async function processMessage(params: {
const { queuedFinal } = await dispatchReplyWithBufferedBlockDispatcher({
ctx: {
Body: combinedBody,
BodyForAgent: combinedBody,
RawBody: params.msg.body,
CommandBody: params.msg.body,
BodyForCommands: params.msg.body,
From: params.msg.from,
To: params.msg.to,
SessionKey: params.route.sessionKey,
@@ -210,7 +213,8 @@ export async function processMessage(params: {
MediaPath: params.msg.mediaPath,
MediaUrl: params.msg.mediaUrl,
MediaType: params.msg.mediaType,
ChatType: params.msg.chatType,
ChatType: normalizeChatType(params.msg.chatType) ?? params.msg.chatType,
ConversationLabel: params.msg.chatType === "group" ? conversationId : params.msg.from,
GroupSubject: params.msg.groupSubject,
GroupMembers: formatGroupMembers({
participants: params.msg.groupParticipants,