From 3af391eec7616999a5428c77b5adb912ec7ac438 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 17 Jan 2026 03:31:57 +0000 Subject: [PATCH] refactor: centralize group sender identity --- extensions/matrix/src/matrix/monitor/index.ts | 27 +++---- .../src/monitor-handler/message-handler.ts | 17 +++-- extensions/zalo/src/monitor.ts | 14 ++-- extensions/zalouser/src/monitor.ts | 14 ++-- .../reply/dispatch-from-config.test.ts | 73 ------------------- src/auto-reply/reply/dispatch-from-config.ts | 39 ---------- .../reply/inbound-sender-meta.test.ts | 41 +++++++++++ src/auto-reply/reply/inbound-sender-meta.ts | 38 ++++++++++ .../reply/session.sender-meta.test.ts | 52 +++++++++++++ src/auto-reply/reply/session.ts | 6 +- .../monitor/message-handler.process.ts | 5 -- src/slack/monitor/message-handler/prepare.ts | 3 +- src/telegram/bot-message-context.ts | 19 +++-- ...patterns-match-without-botusername.test.ts | 33 +++++---- src/telegram/bot.test.ts | 11 ++- src/telegram/bot/helpers.ts | 11 --- ...asts-sequentially-configured-order.test.ts | 6 +- ...oup-chats-injects-history-replying.test.ts | 4 +- ...-activation-silent-token-preserves.test.ts | 4 +- src/web/auto-reply/monitor/message-line.ts | 6 +- src/web/auto-reply/monitor/process-message.ts | 35 ++++----- 21 files changed, 236 insertions(+), 222 deletions(-) create mode 100644 src/auto-reply/reply/inbound-sender-meta.test.ts create mode 100644 src/auto-reply/reply/inbound-sender-meta.ts create mode 100644 src/auto-reply/reply/session.sender-meta.test.ts diff --git a/extensions/matrix/src/matrix/monitor/index.ts b/extensions/matrix/src/matrix/monitor/index.ts index d441d633d..254b71e6b 100644 --- a/extensions/matrix/src/matrix/monitor/index.ts +++ b/extensions/matrix/src/matrix/monitor/index.ts @@ -328,20 +328,21 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi const messageId = event.getId() ?? ""; const threadRootId = resolveMatrixThreadRootId({ event, content }); - const threadTarget = resolveMatrixThreadTarget({ - threadReplies, - messageId, - threadRootId, - isThreadRoot: event.isThreadRoot, - }); + const threadTarget = resolveMatrixThreadTarget({ + threadReplies, + messageId, + threadRootId, + isThreadRoot: event.isThreadRoot, + }); - const textWithId = `${bodyText}\n[matrix event id: ${messageId} room: ${roomId}]`; - const body = formatAgentEnvelope({ - channel: "Matrix", - from: senderName, - timestamp: event.getTs() ?? undefined, - body: textWithId, - }); + const envelopeFrom = isDirectMessage ? senderName : (roomName ?? roomId); + const textWithId = `${bodyText}\n[matrix event id: ${messageId} room: ${roomId}]`; + const body = formatAgentEnvelope({ + channel: "Matrix", + from: envelopeFrom, + timestamp: event.getTs() ?? undefined, + body: textWithId, + }); const route = resolveAgentRoute({ cfg, diff --git a/extensions/msteams/src/monitor-handler/message-handler.ts b/extensions/msteams/src/monitor-handler/message-handler.ts index 5b58dc8ea..9c63ce04d 100644 --- a/extensions/msteams/src/monitor-handler/message-handler.ts +++ b/extensions/msteams/src/monitor-handler/message-handler.ts @@ -352,15 +352,16 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { channelData: activity.channelData, }, log, - }); + }); - const mediaPayload = buildMSTeamsMediaPayload(mediaList); - const body = formatAgentEnvelope({ - channel: "Teams", - from: senderName, - timestamp, - body: rawBody, - }); + const mediaPayload = buildMSTeamsMediaPayload(mediaList); + const envelopeFrom = isDirectMessage ? senderName : conversationType; + const body = formatAgentEnvelope({ + channel: "Teams", + from: envelopeFrom, + timestamp, + body: rawBody, + }); let combinedBody = body; const isRoomish = !isDirectMessage; const historyKey = isRoomish ? conversationId : undefined; diff --git a/extensions/zalo/src/monitor.ts b/extensions/zalo/src/monitor.ts index d81025a8c..377c6ba3e 100644 --- a/extensions/zalo/src/monitor.ts +++ b/extensions/zalo/src/monitor.ts @@ -495,13 +495,13 @@ async function processMessageWithPipeline(params: { }, }); - const rawBody = text?.trim() || (mediaPath ? "" : ""); - const fromLabel = isGroup - ? `group:${chatId} from ${senderName || senderId}` - : senderName || `user:${senderId}`; - const body = deps.formatAgentEnvelope({ - channel: "Zalo", - from: fromLabel, + const rawBody = text?.trim() || (mediaPath ? "" : ""); + const fromLabel = isGroup + ? `group:${chatId}` + : senderName || `user:${senderId}`; + const body = deps.formatAgentEnvelope({ + channel: "Zalo", + from: fromLabel, timestamp: date ? date * 1000 : undefined, body: rawBody, }); diff --git a/extensions/zalouser/src/monitor.ts b/extensions/zalouser/src/monitor.ts index 8b6d22361..2f283d9de 100644 --- a/extensions/zalouser/src/monitor.ts +++ b/extensions/zalouser/src/monitor.ts @@ -170,13 +170,13 @@ async function processMessage( }, }); - const rawBody = content.trim(); - const fromLabel = isGroup - ? `group:${chatId} from ${senderName || senderId}` - : senderName || `user:${senderId}`; - const body = deps.formatAgentEnvelope({ - channel: "Zalo Personal", - from: fromLabel, + const rawBody = content.trim(); + const fromLabel = isGroup + ? `group:${chatId}` + : senderName || `user:${senderId}`; + const body = deps.formatAgentEnvelope({ + channel: "Zalo Personal", + from: fromLabel, timestamp: timestamp ? timestamp * 1000 : undefined, body: rawBody, }); diff --git a/src/auto-reply/reply/dispatch-from-config.test.ts b/src/auto-reply/reply/dispatch-from-config.test.ts index 0605a3628..2dac96ca1 100644 --- a/src/auto-reply/reply/dispatch-from-config.test.ts +++ b/src/auto-reply/reply/dispatch-from-config.test.ts @@ -184,77 +184,4 @@ describe("dispatchReplyFromConfig", () => { expect(replyResolver).toHaveBeenCalledTimes(1); }); - - it("appends sender meta for non-direct chats when missing", async () => { - mocks.tryFastAbortFromMessage.mockResolvedValue({ - handled: false, - aborted: false, - }); - const cfg = {} as ClawdbotConfig; - const dispatcher = createDispatcher(); - const ctx: MsgContext = { - Provider: "imessage", - ChatType: "group", - Body: "[iMessage group:1] hello", - SenderName: "+15555550123", - SenderId: "+15555550123", - }; - - const replyResolver = vi.fn(async (resolvedCtx: MsgContext) => { - expect(resolvedCtx.Body).toContain("\n[from: +15555550123]"); - return { text: "ok" } satisfies ReplyPayload; - }); - - await dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyResolver }); - expect(replyResolver).toHaveBeenCalledTimes(1); - }); - - it("does not append sender meta when Body already includes a from line", async () => { - mocks.tryFastAbortFromMessage.mockResolvedValue({ - handled: false, - aborted: false, - }); - const cfg = {} as ClawdbotConfig; - const dispatcher = createDispatcher(); - const ctx: MsgContext = { - Provider: "whatsapp", - ChatType: "group", - Body: "[WhatsApp group:1] hello\\n[from: Bob (+222)]", - SenderName: "Bob", - SenderId: "+222", - }; - - const replyResolver = vi.fn(async (resolvedCtx: MsgContext) => { - expect(resolvedCtx.Body.match(/\\n\[from:/g)?.length ?? 0).toBe(1); - return { text: "ok" } satisfies ReplyPayload; - }); - - await dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyResolver }); - expect(replyResolver).toHaveBeenCalledTimes(1); - }); - - it("does not append sender meta for other providers (scope is signal/imessage only)", async () => { - mocks.tryFastAbortFromMessage.mockResolvedValue({ - handled: false, - aborted: false, - }); - const cfg = {} as ClawdbotConfig; - const dispatcher = createDispatcher(); - const ctx: MsgContext = { - Provider: "slack", - OriginatingChannel: "slack", - ChatType: "group", - Body: "[Slack #room 2026-01-01T00:00Z] hi", - SenderName: "Bob", - SenderId: "U123", - }; - - const replyResolver = vi.fn(async (resolvedCtx: MsgContext) => { - expect(resolvedCtx.Body).not.toContain("[from:"); - return { text: "ok" } satisfies ReplyPayload; - }); - - await dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyResolver }); - expect(replyResolver).toHaveBeenCalledTimes(1); - }); }); diff --git a/src/auto-reply/reply/dispatch-from-config.ts b/src/auto-reply/reply/dispatch-from-config.ts index 59d99b2cb..c7a766fed 100644 --- a/src/auto-reply/reply/dispatch-from-config.ts +++ b/src/auto-reply/reply/dispatch-from-config.ts @@ -22,8 +22,6 @@ export async function dispatchReplyFromConfig(params: { }): Promise { const { ctx, cfg, dispatcher } = params; - maybeAppendSenderMeta(ctx); - if (shouldSkipDuplicateInbound(ctx)) { return { queuedFinal: false, counts: dispatcher.getQueuedCounts() }; } @@ -162,40 +160,3 @@ export async function dispatchReplyFromConfig(params: { counts.final += routedFinalCount; return { queuedFinal, counts }; } - -function maybeAppendSenderMeta(ctx: MsgContext): void { - if (!ctx.Body?.trim()) return; - if (ctx.ChatType !== "group") return; - if (!shouldInjectSenderMeta(ctx)) return; - if (hasSenderMetaLine(ctx.Body)) return; - - const senderLabel = formatSenderLabel(ctx); - if (!senderLabel) return; - - const lineBreak = resolveBodyLineBreak(ctx.Body); - ctx.Body = `${ctx.Body}${lineBreak}[from: ${senderLabel}]`; -} - -function shouldInjectSenderMeta(ctx: MsgContext): boolean { - const origin = (ctx.OriginatingChannel ?? ctx.Provider ?? "").toLowerCase(); - return origin === "imessage" || origin === "signal"; -} - -function resolveBodyLineBreak(body: string): string { - if (body.includes("\n")) return "\n"; - if (body.includes("\\n")) return "\\n"; - return "\n"; -} - -function hasSenderMetaLine(body: string): boolean { - return /(^|\n|\\n)\[from:/i.test(body); -} - -function formatSenderLabel(ctx: MsgContext): string | null { - const senderName = ctx.SenderName?.trim(); - const senderId = ctx.SenderId?.trim(); - if (senderName && senderId && senderName !== senderId) { - return `${senderName} (${senderId})`; - } - return senderName ?? senderId ?? null; -} diff --git a/src/auto-reply/reply/inbound-sender-meta.test.ts b/src/auto-reply/reply/inbound-sender-meta.test.ts new file mode 100644 index 000000000..9bc5839cb --- /dev/null +++ b/src/auto-reply/reply/inbound-sender-meta.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest"; + +import type { MsgContext } from "../templating.js"; +import { formatInboundBodyWithSenderMeta } from "./inbound-sender-meta.js"; + +describe("formatInboundBodyWithSenderMeta", () => { + it("does nothing for direct messages", () => { + const ctx: MsgContext = { ChatType: "direct", SenderName: "Alice", SenderId: "A1" }; + expect(formatInboundBodyWithSenderMeta({ ctx, body: "[X] hi" })).toBe("[X] hi"); + }); + + it("appends a sender meta line for non-direct messages", () => { + const ctx: MsgContext = { ChatType: "group", SenderName: "Alice", SenderId: "A1" }; + expect(formatInboundBodyWithSenderMeta({ ctx, body: "[X] hi" })).toBe("[X] hi\n[from: Alice (A1)]"); + }); + + it("prefers SenderE164 in the label when present", () => { + const ctx: MsgContext = { + ChatType: "group", + SenderName: "Bob", + SenderId: "bob@s.whatsapp.net", + SenderE164: "+222", + }; + expect(formatInboundBodyWithSenderMeta({ ctx, body: "[X] hi" })).toBe("[X] hi\n[from: Bob (+222)]"); + }); + + it("preserves escaped newline style when body uses 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)]", + ); + }); + + it("does not duplicate a sender meta line when one is already present", () => { + const ctx: MsgContext = { ChatType: "group", SenderName: "Alice", SenderId: "A1" }; + expect(formatInboundBodyWithSenderMeta({ ctx, body: "[X] hi\n[from: Alice (A1)]" })).toBe( + "[X] hi\n[from: Alice (A1)]", + ); + }); +}); + diff --git a/src/auto-reply/reply/inbound-sender-meta.ts b/src/auto-reply/reply/inbound-sender-meta.ts new file mode 100644 index 000000000..24e102c4d --- /dev/null +++ b/src/auto-reply/reply/inbound-sender-meta.ts @@ -0,0 +1,38 @@ +import type { MsgContext } from "../templating.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(); + 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"; +} + +function hasSenderMetaLine(body: string): boolean { + return /(^|\n|\\n)\[from:/i.test(body); +} + +function formatSenderLabel(ctx: MsgContext): string | null { + const senderName = ctx.SenderName?.trim(); + const senderId = (ctx.SenderE164?.trim() || ctx.SenderId?.trim()) ?? ""; + if (senderName && senderId && senderName !== senderId) { + return `${senderName} (${senderId})`; + } + return senderName ?? (senderId || null); +} diff --git a/src/auto-reply/reply/session.sender-meta.test.ts b/src/auto-reply/reply/session.sender-meta.test.ts new file mode 100644 index 000000000..47691f966 --- /dev/null +++ b/src/auto-reply/reply/session.sender-meta.test.ts @@ -0,0 +1,52 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; + +import { describe, expect, it } from "vitest"; + +import type { ClawdbotConfig } from "../../config/config.js"; +import { initSessionState } from "./session.js"; + +describe("initSessionState sender meta", () => { + it("injects sender meta into BodyStripped for group chats", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-sender-meta-")); + const storePath = path.join(root, "sessions.json"); + const cfg = { session: { store: storePath } } as ClawdbotConfig; + + const result = await initSessionState({ + ctx: { + Body: "[WhatsApp 123@g.us] ping", + ChatType: "group", + SenderName: "Bob", + SenderE164: "+222", + SenderId: "222@s.whatsapp.net", + SessionKey: "agent:main:whatsapp:group:123@g.us", + }, + cfg, + commandAuthorized: true, + }); + + expect(result.sessionCtx.BodyStripped).toBe("[WhatsApp 123@g.us] ping\n[from: Bob (+222)]"); + }); + + it("does not inject sender meta for direct chats", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-sender-meta-direct-")); + const storePath = path.join(root, "sessions.json"); + const cfg = { session: { store: storePath } } as ClawdbotConfig; + + const result = await initSessionState({ + ctx: { + Body: "[WhatsApp +1] ping", + ChatType: "direct", + SenderName: "Bob", + SenderE164: "+222", + SessionKey: "agent:main:whatsapp:dm:+222", + }, + cfg, + commandAuthorized: true, + }); + + expect(result.sessionCtx.BodyStripped).toBe("[WhatsApp +1] ping"); + }); +}); + diff --git a/src/auto-reply/reply/session.ts b/src/auto-reply/reply/session.ts index 0a4b636b3..d66369177 100644 --- a/src/auto-reply/reply/session.ts +++ b/src/auto-reply/reply/session.ts @@ -26,6 +26,7 @@ import { normalizeMainKey } from "../../routing/session-key.js"; import { resolveCommandAuthorization } from "../command-auth.js"; import type { MsgContext, TemplateContext } from "../templating.js"; import { stripMentions, stripStructuralPrefixes } from "./mentions.js"; +import { formatInboundBodyWithSenderMeta } from "./inbound-sender-meta.js"; export type SessionInitResult = { sessionCtx: TemplateContext; @@ -305,7 +306,10 @@ export async function initSessionState(params: { ...ctx, // Keep BodyStripped aligned with Body (best default for agent prompts). // RawBody is reserved for command/directive parsing and may omit context. - BodyStripped: bodyStripped ?? ctx.Body ?? ctx.CommandBody ?? ctx.RawBody, + BodyStripped: formatInboundBodyWithSenderMeta({ + ctx, + body: bodyStripped ?? ctx.Body ?? ctx.CommandBody ?? ctx.RawBody ?? "", + }), SessionId: sessionId, IsNewSession: isNewSession ? "true" : "false", }; diff --git a/src/discord/monitor/message-handler.process.ts b/src/discord/monitor/message-handler.process.ts index e5a70a9eb..f136a2e64 100644 --- a/src/discord/monitor/message-handler.process.ts +++ b/src/discord/monitor/message-handler.process.ts @@ -149,11 +149,6 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) }), }); } - if (!isDirectMessage) { - const name = formatDiscordUserTag(author); - const id = author.id; - combinedBody = `${combinedBody}\n[from: ${name} user id:${id}]`; - } const replyContext = resolveReplyContext(message, resolveDiscordMessageText); if (replyContext) { combinedBody = `[Replied message - for context]\n${replyContext}\n\n${combinedBody}`; diff --git a/src/slack/monitor/message-handler/prepare.ts b/src/slack/monitor/message-handler/prepare.ts index ba4ba55ea..40e5854dd 100644 --- a/src/slack/monitor/message-handler/prepare.ts +++ b/src/slack/monitor/message-handler/prepare.ts @@ -330,10 +330,11 @@ export async function prepareSlackMessage(params: { contextKey: `slack:message:${message.channel}:${message.ts ?? "unknown"}`, }); + const envelopeFrom = isDirectMessage ? senderName : roomLabel; const textWithId = `${rawBody}\n[slack message id: ${message.ts} channel: ${message.channel}]`; const body = formatAgentEnvelope({ channel: "Slack", - from: senderName, + from: envelopeFrom, timestamp: message.ts ? Math.round(Number(message.ts) * 1000) : undefined, body: textWithId, }); diff --git a/src/telegram/bot-message-context.ts b/src/telegram/bot-message-context.ts index 3027c96b8..8769d4799 100644 --- a/src/telegram/bot-message-context.ts +++ b/src/telegram/bot-message-context.ts @@ -15,7 +15,6 @@ import { recordChannelActivity } from "../infra/channel-activity.js"; import { resolveAgentRoute } from "../routing/resolve-route.js"; import { resolveMentionGating } from "../channels/mention-gating.js"; import { - buildGroupFromLabel, buildGroupLabel, buildSenderLabel, buildSenderName, @@ -324,15 +323,15 @@ export const buildTelegramMessageContext = async ({ replyTarget.id ? ` id:${replyTarget.id}` : "" }]\n${replyTarget.body}\n[/Replying]` : ""; - const groupLabel = isGroup ? buildGroupLabel(msg, chatId, resolvedThreadId) : undefined; - const body = formatAgentEnvelope({ - channel: "Telegram", - from: isGroup - ? buildGroupFromLabel(msg, chatId, senderId, resolvedThreadId) - : buildSenderLabel(msg, senderId || chatId), - timestamp: msg.date ? msg.date * 1000 : undefined, - body: `${bodyText}${replySuffix}`, - }); + const groupLabel = isGroup ? buildGroupLabel(msg, chatId, resolvedThreadId) : undefined; + const body = formatAgentEnvelope({ + channel: "Telegram", + from: isGroup + ? (groupLabel ?? `group:${chatId}`) + : buildSenderLabel(msg, senderId || chatId), + timestamp: msg.date ? msg.date * 1000 : undefined, + body: `${bodyText}${replySuffix}`, + }); let combinedBody = body; if (isGroup && historyKey && historyLimit > 0) { combinedBody = buildPendingHistoryContextFromMap({ diff --git a/src/telegram/bot.create-telegram-bot.accepts-group-messages-mentionpatterns-match-without-botusername.test.ts b/src/telegram/bot.create-telegram-bot.accepts-group-messages-mentionpatterns-match-without-botusername.test.ts index 96e5ff741..92524b3f7 100644 --- a/src/telegram/bot.create-telegram-bot.accepts-group-messages-mentionpatterns-match-without-botusername.test.ts +++ b/src/telegram/bot.create-telegram-bot.accepts-group-messages-mentionpatterns-match-without-botusername.test.ts @@ -171,15 +171,17 @@ describe("createTelegramBot", () => { getFile: async () => ({ download: async () => new Uint8Array() }), }); - expect(replySpy).toHaveBeenCalledTimes(1); - const payload = replySpy.mock.calls[0][0]; - expect(payload.WasMentioned).toBe(true); - expect(payload.Body).toMatch(/^\[Telegram Test Group id:7 from Ada id:9 2025-01-09T00:00Z\]/); - }); - it("includes sender identity in group envelope headers", async () => { - onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; - replySpy.mockReset(); + expect(replySpy).toHaveBeenCalledTimes(1); + const payload = replySpy.mock.calls[0][0]; + expect(payload.WasMentioned).toBe(true); + expect(payload.SenderName).toBe("Ada"); + expect(payload.SenderId).toBe("9"); + expect(payload.Body).toMatch(/^\[Telegram Test Group id:7 2025-01-09T00:00Z\]/); + }); + it("keeps group envelope headers stable (sender identity is separate)", async () => { + onSpy.mockReset(); + const replySpy = replyModule.__replySpy as unknown as ReturnType; + replySpy.mockReset(); loadConfig.mockReturnValue({ channels: { @@ -210,12 +212,13 @@ describe("createTelegramBot", () => { getFile: async () => ({ download: async () => new Uint8Array() }), }); - expect(replySpy).toHaveBeenCalledTimes(1); - const payload = replySpy.mock.calls[0][0]; - expect(payload.Body).toMatch( - /^\[Telegram Ops id:42 from Ada Lovelace \(@ada\) id:99 2025-01-09T00:00Z\]/, - ); - }); + expect(replySpy).toHaveBeenCalledTimes(1); + const payload = replySpy.mock.calls[0][0]; + expect(payload.SenderName).toBe("Ada Lovelace"); + expect(payload.SenderId).toBe("99"); + expect(payload.SenderUsername).toBe("ada"); + expect(payload.Body).toMatch(/^\[Telegram Ops id:42 2025-01-09T00:00Z\]/); + }); it("reacts to mention-gated group messages when ackReaction is enabled", async () => { onSpy.mockReset(); setMessageReactionSpy.mockReset(); diff --git a/src/telegram/bot.test.ts b/src/telegram/bot.test.ts index 7966856f8..c4a1bcdb1 100644 --- a/src/telegram/bot.test.ts +++ b/src/telegram/bot.test.ts @@ -584,7 +584,9 @@ describe("createTelegramBot", () => { expect(replySpy).toHaveBeenCalledTimes(1); const payload = replySpy.mock.calls[0][0]; expect(payload.WasMentioned).toBe(true); - expect(payload.Body).toMatch(/^\[Telegram Test Group id:7 from Ada id:9 2025-01-09T00:00Z\]/); + expect(payload.Body).toMatch(/^\[Telegram Test Group id:7 2025-01-09T00:00Z\]/); + expect(payload.SenderName).toBe("Ada"); + expect(payload.SenderId).toBe("9"); }); it("includes sender identity in group envelope headers", async () => { @@ -623,9 +625,10 @@ describe("createTelegramBot", () => { expect(replySpy).toHaveBeenCalledTimes(1); const payload = replySpy.mock.calls[0][0]; - expect(payload.Body).toMatch( - /^\[Telegram Ops id:42 from Ada Lovelace \(@ada\) id:99 2025-01-09T00:00Z\]/, - ); + expect(payload.Body).toMatch(/^\[Telegram Ops id:42 2025-01-09T00:00Z\]/); + expect(payload.SenderName).toBe("Ada Lovelace"); + expect(payload.SenderId).toBe("99"); + expect(payload.SenderUsername).toBe("ada"); }); it("reacts to mention-gated group messages when ackReaction is enabled", async () => { diff --git a/src/telegram/bot/helpers.ts b/src/telegram/bot/helpers.ts index 41eace902..40c2aea63 100644 --- a/src/telegram/bot/helpers.ts +++ b/src/telegram/bot/helpers.ts @@ -98,17 +98,6 @@ export function buildGroupLabel( return `group:${chatId}${topicSuffix}`; } -export function buildGroupFromLabel( - msg: TelegramMessage, - chatId: number | string, - senderId?: number | string, - messageThreadId?: number, -) { - const groupLabel = buildGroupLabel(msg, chatId, messageThreadId); - const senderLabel = buildSenderLabel(msg, senderId); - return `${groupLabel} from ${senderLabel}`; -} - export function hasBotMention(msg: TelegramMessage, botUsername: string) { const text = (msg.text ?? msg.caption ?? "").toLowerCase(); if (text.includes(`@${botUsername}`)) return true; diff --git a/src/web/auto-reply.broadcast-groups.broadcasts-sequentially-configured-order.test.ts b/src/web/auto-reply.broadcast-groups.broadcasts-sequentially-configured-order.test.ts index d94e5bb8b..2c6707853 100644 --- a/src/web/auto-reply.broadcast-groups.broadcasts-sequentially-configured-order.test.ts +++ b/src/web/auto-reply.broadcast-groups.broadcasts-sequentially-configured-order.test.ts @@ -216,12 +216,14 @@ describe("broadcast groups", () => { expect(resolver).toHaveBeenCalledTimes(2); for (const call of resolver.mock.calls.slice(0, 2)) { - const payload = call[0] as { Body: string }; + const payload = call[0] as { Body: string; SenderName?: string; SenderE164?: string; SenderId?: string }; expect(payload.Body).toContain("Chat messages since your last reply"); expect(payload.Body).toContain("Alice (+111): hello group"); expect(payload.Body).toContain("[message_id: g1]"); expect(payload.Body).toContain("@bot ping"); - expect(payload.Body).toContain("[from: Bob (+222)]"); + expect(payload.SenderName).toBe("Bob"); + expect(payload.SenderE164).toBe("+222"); + expect(payload.SenderId).toBe("+222"); } await capturedOnMessage?.({ diff --git a/src/web/auto-reply.web-auto-reply.requires-mention-group-chats-injects-history-replying.test.ts b/src/web/auto-reply.web-auto-reply.requires-mention-group-chats-injects-history-replying.test.ts index 9965ca0b5..0a3136484 100644 --- a/src/web/auto-reply.web-auto-reply.requires-mention-group-chats-injects-history-replying.test.ts +++ b/src/web/auto-reply.web-auto-reply.requires-mention-group-chats-injects-history-replying.test.ts @@ -166,7 +166,9 @@ describe("web auto-reply", () => { expect(payload.Body).toContain("Alice (+111): hello group"); expect(payload.Body).toContain("[message_id: g1]"); expect(payload.Body).toContain("@bot ping"); - expect(payload.Body).toContain("[from: Bob (+222)]"); + expect(payload.SenderName).toBe("Bob"); + expect(payload.SenderE164).toBe("+222"); + expect(payload.SenderId).toBe("+222"); }); it("bypasses mention gating for owner /new in group chats", async () => { diff --git a/src/web/auto-reply.web-auto-reply.supports-always-group-activation-silent-token-preserves.test.ts b/src/web/auto-reply.web-auto-reply.supports-always-group-activation-silent-token-preserves.test.ts index 12b7d9728..eac52bbb2 100644 --- a/src/web/auto-reply.web-auto-reply.supports-always-group-activation-silent-token-preserves.test.ts +++ b/src/web/auto-reply.web-auto-reply.supports-always-group-activation-silent-token-preserves.test.ts @@ -183,7 +183,9 @@ describe("web auto-reply", () => { expect(payload.Body).not.toContain("Chat messages since your last reply"); expect(payload.Body).not.toContain("Alice (+111): first"); expect(payload.Body).not.toContain("[message_id: g-always-1]"); - expect(payload.Body).toContain("Bob: second"); + expect(payload.Body).toContain("second"); + expect(payload.SenderName).toBe("Bob"); + expect(payload.SenderE164).toBe("+222"); expect(reply).toHaveBeenCalledTimes(1); await cleanup(); diff --git a/src/web/auto-reply/monitor/message-line.ts b/src/web/auto-reply/monitor/message-line.ts index 26e55bec9..8fcedebc1 100644 --- a/src/web/auto-reply/monitor/message-line.ts +++ b/src/web/auto-reply/monitor/message-line.ts @@ -22,12 +22,8 @@ export function buildInboundLine(params: { hasAllowFrom: (cfg.channels?.whatsapp?.allowFrom?.length ?? 0) > 0, }); const prefixStr = messagePrefix ? `${messagePrefix} ` : ""; - const senderLabel = - msg.chatType === "group" ? `${msg.senderName ?? msg.senderE164 ?? "Someone"}: ` : ""; const replyContext = formatReplyContext(msg); - const baseLine = `${prefixStr}${senderLabel}${msg.body}${ - replyContext ? `\n\n${replyContext}` : "" - }`; + const baseLine = `${prefixStr}${msg.body}${replyContext ? `\n\n${replyContext}` : ""}`; // Wrap with standardized envelope for the agent. return formatAgentEnvelope({ diff --git a/src/web/auto-reply/monitor/process-message.ts b/src/web/auto-reply/monitor/process-message.ts index 2d6f26bd4..c5524e825 100644 --- a/src/web/auto-reply/monitor/process-message.ts +++ b/src/web/auto-reply/monitor/process-message.ts @@ -9,7 +9,7 @@ import { } from "../../../auto-reply/reply/response-prefix-template.js"; import { resolveTextChunkLimit } from "../../../auto-reply/chunk.js"; import { formatAgentEnvelope } from "../../../auto-reply/envelope.js"; -import { buildHistoryContext } from "../../../auto-reply/reply/history.js"; +import { buildHistoryContextFromEntries, type HistoryEntry } from "../../../auto-reply/reply/history.js"; import { dispatchReplyWithBufferedBlockDispatcher } from "../../../auto-reply/reply/provider-dispatcher.js"; import type { getReplyFromConfig } from "../../../auto-reply/reply.js"; import type { ReplyPayload } from "../../../auto-reply/types.js"; @@ -77,30 +77,27 @@ export async function processMessage(params: { if (params.msg.chatType === "group") { const history = params.groupHistory ?? params.groupHistories.get(params.groupHistoryKey) ?? []; if (history.length > 0) { - const lineBreak = "\\n"; - const historyText = history - .map((m) => { - const bodyWithId = m.id ? `${m.body}\n[message_id: ${m.id}]` : m.body; + const historyEntries: HistoryEntry[] = history.map((m) => ({ + sender: m.sender, + body: m.body, + timestamp: m.timestamp, + messageId: m.id, + })); + combinedBody = buildHistoryContextFromEntries({ + entries: historyEntries, + currentMessage: combinedBody, + excludeLast: false, + formatEntry: (entry) => { + const bodyWithId = entry.messageId ? `${entry.body}\n[message_id: ${entry.messageId}]` : entry.body; return formatAgentEnvelope({ channel: "WhatsApp", from: conversationId, - timestamp: m.timestamp, - body: `${m.sender}: ${bodyWithId}`, + timestamp: entry.timestamp, + body: `${entry.sender}: ${bodyWithId}`, }); - }) - .join(lineBreak); - combinedBody = buildHistoryContext({ - historyText, - currentMessage: combinedBody, - lineBreak, + }, }); } - // Always surface who sent the triggering message so the agent can address them. - const senderLabel = - params.msg.senderName && params.msg.senderE164 - ? `${params.msg.senderName} (${params.msg.senderE164})` - : (params.msg.senderName ?? params.msg.senderE164 ?? "Unknown"); - combinedBody = `${combinedBody}\\n[from: ${senderLabel}]`; shouldClearGroupHistory = !(params.suppressGroupHistoryClear ?? false); }