From e31251293b467c10663a7267f62c43c471ba9940 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 16 Jan 2026 23:52:14 +0000 Subject: [PATCH] fix: scope history injection to pending-only --- CHANGELOG.md | 1 + docs/channels/whatsapp.md | 6 +- docs/concepts/group-messages.md | 2 +- docs/concepts/groups.md | 2 +- docs/concepts/messages.md | 4 ++ .../src/monitor-handler/message-handler.ts | 31 +++++---- src/auto-reply/reply/history.test.ts | 24 +++++++ src/auto-reply/reply/history.ts | 36 ++++++++-- .../monitor/message-handler.preflight.ts | 10 ++- .../monitor/message-handler.process.ts | 18 ++--- src/imessage/monitor/monitor-provider.ts | 56 ++++++++-------- src/signal/monitor/event-handler.ts | 20 ++---- ...ends-tool-summaries-responseprefix.test.ts | 22 +++---- src/slack/monitor/message-handler/dispatch.ts | 7 +- src/slack/monitor/message-handler/prepare.ts | 65 ++++++++++++------- src/telegram/bot-message-context.ts | 64 ++++++++++-------- src/telegram/bot-message-dispatch.ts | 6 +- ...-activation-silent-token-preserves.test.ts | 8 +-- src/web/auto-reply/monitor/broadcast.ts | 8 +-- src/web/auto-reply/monitor/group-gating.ts | 54 ++++++++++----- src/web/auto-reply/monitor/process-message.ts | 9 ++- 21 files changed, 278 insertions(+), 175 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26023ebc2..ecdc6ef6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ - Telegram: scope inline buttons with allowlist default + callback gating in DMs/groups. - Telegram: default reaction notifications to own. - Tools: improve `web_fetch` extraction using Readability (with fallback). +- Channels: inject only pending (mention-gated) group history; clear history on any processed message. - Heartbeat: tighten prompt guidance + suppress duplicate alerts for 24h. (#980) — thanks @voidserf. - Repo: ignore local identity files to avoid accidental commits. (#1001) — thanks @gerardward2007. - Sessions/Security: add `session.dmScope` for multi-user DM isolation and audit warnings. (#948) — thanks @Alphonse-arianee. diff --git a/docs/channels/whatsapp.md b/docs/channels/whatsapp.md index 57041fdfa..8bbabaa65 100644 --- a/docs/channels/whatsapp.md +++ b/docs/channels/whatsapp.md @@ -202,9 +202,9 @@ The wizard uses it to set your **allowlist/owner** so your own DMs are permitted - `always`: always triggers. - `/activation mention|always` is owner-only and must be sent as a standalone message. - Owner = `channels.whatsapp.allowFrom` (or self E.164 if unset). -- **History injection**: - - Recent messages (default 50) inserted under: - `[Chat messages since your last reply - for context]` +- **History injection** (pending-only): + - Recent *unprocessed* messages (default 50) inserted under: + `[Chat messages since your last reply - for context]` (messages already in the session are not re-injected) - Current message under: `[Current message - respond to this]` - Sender suffix appended: `[from: Name (+E164)]` diff --git a/docs/concepts/group-messages.md b/docs/concepts/group-messages.md index f26a9252d..934e5524d 100644 --- a/docs/concepts/group-messages.md +++ b/docs/concepts/group-messages.md @@ -13,7 +13,7 @@ Note: `agents.list[].groupChat.mentionPatterns` is now used by Telegram/Discord/ - Activation modes: `mention` (default) or `always`. `mention` requires a ping (real WhatsApp @-mentions via `mentionedJids`, regex patterns, or the bot’s E.164 anywhere in the text). `always` wakes the agent on every message but it should reply only when it can add meaningful value; otherwise it returns the silent token `NO_REPLY`. Defaults can be set in config (`channels.whatsapp.groups`) and overridden per group via `/activation`. When `channels.whatsapp.groups` is set, it also acts as a group allowlist (include `"*"` to allow all). - Group policy: `channels.whatsapp.groupPolicy` controls whether group messages are accepted (`open|disabled|allowlist`). `allowlist` uses `channels.whatsapp.groupAllowFrom` (fallback: explicit `channels.whatsapp.allowFrom`). Default is `allowlist` (blocked until you add senders). - Per-group sessions: session keys look like `agent::whatsapp:group:` so commands such as `/verbose on` or `/think high` (sent as standalone messages) are scoped to that group; personal DM state is untouched. Heartbeats are skipped for group threads. -- Context injection: last N (default 50) group messages are prefixed under `[Chat messages since your last reply - for context]`, with the triggering line under `[Current message - respond to this]`. +- Context injection: **pending-only** group messages (default 50) that *did not* trigger a run are prefixed under `[Chat messages since your last reply - for context]`, with the triggering line under `[Current message - respond to this]`. Messages already in the session are not re-injected. - Sender surfacing: every group batch now ends with `[from: Sender Name (+E164)]` so Pi knows who is speaking. - Ephemeral/view-once: we unwrap those before extracting text/mentions, so pings inside them still trigger. - Group system prompt: on the first turn of a group session (and whenever `/activation` changes the mode) we inject a short blurb into the system prompt like `You are replying inside the WhatsApp group "". Group members: Alice (+44...), Bob (+43...), … Activation: trigger-only … Address the specific sender noted in the message context.` If metadata isn’t available we still tell the agent it’s a group chat. diff --git a/docs/concepts/groups.md b/docs/concepts/groups.md index 43358be97..f64b5bd8a 100644 --- a/docs/concepts/groups.md +++ b/docs/concepts/groups.md @@ -221,7 +221,7 @@ Notes: - Per-agent override: `agents.list[].groupChat.mentionPatterns` (useful when multiple agents share a group). - Mention gating is only enforced when mention detection is possible (native mentions or `mentionPatterns` are configured). - Discord defaults live in `channels.discord.guilds."*"` (overridable per guild/channel). -- Group history context is wrapped uniformly across channels; use `messages.groupChat.historyLimit` for the global default and `channels..historyLimit` (or `channels..accounts.*.historyLimit`) for overrides. Set `0` to disable. +- Group history context is wrapped uniformly across channels and is **pending-only** (messages skipped due to mention gating); use `messages.groupChat.historyLimit` for the global default and `channels..historyLimit` (or `channels..accounts.*.historyLimit`) for overrides. Set `0` to disable. ## Group allowlists When `channels.whatsapp.groups`, `channels.telegram.groups`, or `channels.imessage.groups` is configured, the keys act as a group allowlist. Use `"*"` to allow all groups while still setting default mention behavior. diff --git a/docs/concepts/messages.md b/docs/concepts/messages.md index 37e487987..a6f225f53 100644 --- a/docs/concepts/messages.md +++ b/docs/concepts/messages.md @@ -85,6 +85,10 @@ When a channel supplies history, it uses a shared wrapper: - `[Chat messages since your last reply - for context]` - `[Current message - respond to this]` +History buffers are **pending-only**: they include group messages that did *not* +trigger a run (for example, mention-gated messages) and **exclude** messages +already in the session transcript. + Directive stripping only applies to the **current message** section so history remains intact. Channels that wrap history should set `CommandBody` (or `RawBody`) to the original message text and keep `Body` as the combined prompt. diff --git a/extensions/msteams/src/monitor-handler/message-handler.ts b/extensions/msteams/src/monitor-handler/message-handler.ts index 0e89c18bd..5b58dc8ea 100644 --- a/extensions/msteams/src/monitor-handler/message-handler.ts +++ b/extensions/msteams/src/monitor-handler/message-handler.ts @@ -6,9 +6,10 @@ import { } from "../../../../src/auto-reply/inbound-debounce.js"; import { dispatchReplyFromConfig } from "../../../../src/auto-reply/reply/dispatch-from-config.js"; import { - buildHistoryContextFromMap, + buildPendingHistoryContextFromMap, clearHistoryEntries, DEFAULT_GROUP_HISTORY_LIMIT, + recordPendingHistoryEntry, type HistoryEntry, } from "../../../../src/auto-reply/reply/history.js"; import { resolveMentionGating } from "../../../../src/channels/mention-gating.js"; @@ -302,6 +303,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { teamConfig, channelConfig, }); + const timestamp = parseMSTeamsActivityTimestamp(activity.timestamp); if (!isDirectMessage) { const mentionGate = resolveMentionGating({ @@ -319,11 +321,22 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { requireMention, mentioned, }); + if (historyLimit > 0) { + recordPendingHistoryEntry({ + historyMap: conversationHistories, + historyKey: conversationId, + limit: historyLimit, + entry: { + sender: senderName, + body: rawBody, + timestamp: timestamp?.getTime(), + messageId: activity.id ?? undefined, + }, + }); + } return; } } - - const timestamp = parseMSTeamsActivityTimestamp(activity.timestamp); const mediaList = await resolveMSTeamsInboundMedia({ attachments, htmlSummary: htmlSummary ?? undefined, @@ -352,16 +365,10 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { const isRoomish = !isDirectMessage; const historyKey = isRoomish ? conversationId : undefined; if (isRoomish && historyKey && historyLimit > 0) { - combinedBody = buildHistoryContextFromMap({ + combinedBody = buildPendingHistoryContextFromMap({ historyMap: conversationHistories, historyKey, limit: historyLimit, - entry: { - sender: senderName, - body: rawBody, - timestamp: timestamp?.getTime(), - messageId: activity.id ?? undefined, - }, currentMessage: combinedBody, formatEntry: (entry) => formatAgentEnvelope({ @@ -432,7 +439,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { const didSendReply = counts.final + counts.tool + counts.block > 0; if (!queuedFinal) { - if (isRoomish && historyKey && historyLimit > 0 && didSendReply) { + if (isRoomish && historyKey && historyLimit > 0) { clearHistoryEntries({ historyMap: conversationHistories, historyKey, @@ -446,7 +453,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { `msteams: delivered ${finalCount} reply${finalCount === 1 ? "" : "ies"} to ${teamsTo}`, ); } - if (isRoomish && historyKey && historyLimit > 0 && didSendReply) { + if (isRoomish && historyKey && historyLimit > 0) { clearHistoryEntries({ historyMap: conversationHistories, historyKey }); } } catch (err) { diff --git a/src/auto-reply/reply/history.test.ts b/src/auto-reply/reply/history.test.ts index 027320acb..8641fa86d 100644 --- a/src/auto-reply/reply/history.test.ts +++ b/src/auto-reply/reply/history.test.ts @@ -4,6 +4,7 @@ import { buildHistoryContext, buildHistoryContextFromEntries, buildHistoryContextFromMap, + buildPendingHistoryContextFromMap, HISTORY_CONTEXT_MARKER, } from "./history.js"; import { CURRENT_MESSAGE_MARKER } from "./mentions.js"; @@ -81,4 +82,27 @@ describe("history helpers", () => { expect(result).toContain("B: two"); expect(result).not.toContain("C: three"); }); + + it("builds context from pending map without appending", () => { + const historyMap = new Map(); + historyMap.set("room", [ + { sender: "A", body: "one" }, + { sender: "B", body: "two" }, + ]); + + const result = buildPendingHistoryContextFromMap({ + historyMap, + historyKey: "room", + limit: 3, + currentMessage: "current", + formatEntry: (entry) => `${entry.sender}: ${entry.body}`, + }); + + expect(historyMap.get("room")?.map((entry) => entry.body)).toEqual(["one", "two"]); + expect(result).toContain(HISTORY_CONTEXT_MARKER); + expect(result).toContain("A: one"); + expect(result).toContain("B: two"); + expect(result).toContain(CURRENT_MESSAGE_MARKER); + expect(result).toContain("current"); + }); }); diff --git a/src/auto-reply/reply/history.ts b/src/auto-reply/reply/history.ts index 632d71f1f..b51e27da4 100644 --- a/src/auto-reply/reply/history.ts +++ b/src/auto-reply/reply/history.ts @@ -23,12 +23,12 @@ export function buildHistoryContext(params: { ); } -export function appendHistoryEntry(params: { - historyMap: Map; +export function appendHistoryEntry(params: { + historyMap: Map; historyKey: string; - entry: HistoryEntry; + entry: T; limit: number; -}): HistoryEntry[] { +}): T[] { const { historyMap, historyKey, entry } = params; if (params.limit <= 0) return []; const history = historyMap.get(historyKey) ?? []; @@ -38,6 +38,34 @@ export function appendHistoryEntry(params: { return history; } +export function recordPendingHistoryEntry(params: { + historyMap: Map; + historyKey: string; + entry: T; + limit: number; +}): T[] { + return appendHistoryEntry(params); +} + +export function buildPendingHistoryContextFromMap(params: { + historyMap: Map; + historyKey: string; + limit: number; + currentMessage: string; + formatEntry: (entry: HistoryEntry) => string; + lineBreak?: string; +}): string { + if (params.limit <= 0) return params.currentMessage; + const entries = params.historyMap.get(params.historyKey) ?? []; + return buildHistoryContextFromEntries({ + entries, + currentMessage: params.currentMessage, + formatEntry: params.formatEntry, + lineBreak: params.lineBreak, + excludeLast: false, + }); +} + export function buildHistoryContextFromMap(params: { historyMap: Map; historyKey: string; diff --git a/src/discord/monitor/message-handler.preflight.ts b/src/discord/monitor/message-handler.preflight.ts index 3f6fa3cd6..68f9c3eea 100644 --- a/src/discord/monitor/message-handler.preflight.ts +++ b/src/discord/monitor/message-handler.preflight.ts @@ -2,7 +2,7 @@ import { ChannelType, MessageType, type User } from "@buape/carbon"; import { hasControlCommand } from "../../auto-reply/command-detection.js"; import { shouldHandleTextCommands } from "../../auto-reply/commands-registry.js"; -import type { HistoryEntry } from "../../auto-reply/reply/history.js"; +import { recordPendingHistoryEntry, type HistoryEntry } from "../../auto-reply/reply/history.js"; import { buildMentionRegexes, matchesMentionPatterns } from "../../auto-reply/reply/mentions.js"; import { logVerbose, shouldLogVerbose } from "../../globals.js"; import { recordChannelActivity } from "../../infra/channel-activity.js"; @@ -353,6 +353,14 @@ export async function preflightDiscordMessage( }, "discord: skipping guild message", ); + if (historyEntry && params.historyLimit > 0) { + recordPendingHistoryEntry({ + historyMap: params.guildHistories, + historyKey: message.channelId, + limit: params.historyLimit, + entry: historyEntry, + }); + } return null; } } diff --git a/src/discord/monitor/message-handler.process.ts b/src/discord/monitor/message-handler.process.ts index ee2695b78..e5a70a9eb 100644 --- a/src/discord/monitor/message-handler.process.ts +++ b/src/discord/monitor/message-handler.process.ts @@ -10,7 +10,10 @@ import { } from "../../auto-reply/reply/response-prefix-template.js"; import { formatAgentEnvelope, formatThreadStarterEnvelope } from "../../auto-reply/envelope.js"; import { dispatchReplyFromConfig } from "../../auto-reply/reply/dispatch-from-config.js"; -import { buildHistoryContextFromMap, clearHistoryEntries } from "../../auto-reply/reply/history.js"; +import { + buildPendingHistoryContextFromMap, + clearHistoryEntries, +} from "../../auto-reply/reply/history.js"; import { createReplyDispatcherWithTyping } from "../../auto-reply/reply/reply-dispatcher.js"; import type { ReplyPayload } from "../../auto-reply/types.js"; import { resolveStorePath, updateLastRoute } from "../../config/sessions.js"; @@ -59,7 +62,6 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) shouldRequireMention, canDetectMention, effectiveWasMentioned, - historyEntry, threadChannel, threadParentId, threadParentName, @@ -130,15 +132,13 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) timestamp: resolveTimestampMs(message.timestamp), body: text, }); - let shouldClearHistory = false; const shouldIncludeChannelHistory = !isDirectMessage && !(isGuildMessage && channelConfig?.autoThread && !threadChannel); if (shouldIncludeChannelHistory) { - combinedBody = buildHistoryContextFromMap({ + combinedBody = buildPendingHistoryContextFromMap({ historyMap: guildHistories, historyKey: message.channelId, limit: historyLimit, - entry: historyEntry, currentMessage: combinedBody, formatEntry: (entry) => formatAgentEnvelope({ @@ -148,7 +148,6 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) body: `${entry.sender}: ${entry.body} [id:${entry.messageId ?? "unknown"} channel:${message.channelId}]`, }), }); - shouldClearHistory = true; } if (!isDirectMessage) { const name = formatDiscordUserTag(author); @@ -279,7 +278,6 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) ); } - let didSendReply = false; const typingChannelId = deliverTarget.startsWith("channel:") ? deliverTarget.slice("channel:".length) : message.channelId; @@ -306,7 +304,6 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) textLimit, maxLinesPerMessage: discordConfig?.maxLinesPerMessage, }); - didSendReply = true; replyReference.markSent(); }, onError: (err, info) => { @@ -337,7 +334,7 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) }); markDispatchIdle(); if (!queuedFinal) { - if (isGuildMessage && shouldClearHistory && historyLimit > 0 && didSendReply) { + if (isGuildMessage && historyLimit > 0) { clearHistoryEntries({ historyMap: guildHistories, historyKey: message.channelId, @@ -345,7 +342,6 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) } return; } - didSendReply = true; if (shouldLogVerbose()) { const finalCount = counts.final; logVerbose( @@ -365,7 +361,7 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) }); }); } - if (isGuildMessage && shouldClearHistory && historyLimit > 0 && didSendReply) { + if (isGuildMessage && historyLimit > 0) { clearHistoryEntries({ historyMap: guildHistories, historyKey: message.channelId, diff --git a/src/imessage/monitor/monitor-provider.ts b/src/imessage/monitor/monitor-provider.ts index 67e5d55e9..3377f85b5 100644 --- a/src/imessage/monitor/monitor-provider.ts +++ b/src/imessage/monitor/monitor-provider.ts @@ -16,9 +16,10 @@ import { } from "../../auto-reply/inbound-debounce.js"; import { dispatchReplyFromConfig } from "../../auto-reply/reply/dispatch-from-config.js"; import { - buildHistoryContextFromMap, + buildPendingHistoryContextFromMap, clearHistoryEntries, DEFAULT_GROUP_HISTORY_LIMIT, + recordPendingHistoryEntry, type HistoryEntry, } from "../../auto-reply/reply/history.js"; import { buildMentionRegexes, matchesMentionPatterns } from "../../auto-reply/reply/mentions.js"; @@ -260,6 +261,18 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P }); const mentionRegexes = buildMentionRegexes(cfg, route.agentId); const messageText = (message.text ?? "").trim(); + const attachments = includeAttachments ? (message.attachments ?? []) : []; + const firstAttachment = attachments?.find((entry) => entry?.original_path && !entry?.missing); + const mediaPath = firstAttachment?.original_path ?? undefined; + const mediaType = firstAttachment?.mime_type ?? undefined; + const kind = mediaKindFromMime(mediaType ?? undefined); + const placeholder = kind ? `` : attachments?.length ? "" : ""; + const bodyText = messageText || placeholder; + if (!bodyText) return; + const createdAt = message.created_at ? Date.parse(message.created_at) : undefined; + const historyKey = isGroup + ? String(chatId ?? chatGuid ?? chatIdentifier ?? "unknown") + : undefined; const mentioned = isGroup ? matchesMentionPatterns(messageText, mentionRegexes) : true; const requireMention = resolveChannelGroupRequireMention({ cfg, @@ -290,23 +303,26 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P const effectiveWasMentioned = mentioned || shouldBypassMention; if (isGroup && requireMention && canDetectMention && !mentioned && !shouldBypassMention) { logVerbose(`imessage: skipping group message (no mention)`); + if (historyKey && historyLimit > 0) { + recordPendingHistoryEntry({ + historyMap: groupHistories, + historyKey, + limit: historyLimit, + entry: { + sender: normalizeIMessageHandle(sender), + body: bodyText, + timestamp: createdAt, + messageId: message.id ? String(message.id) : undefined, + }, + }); + } return; } - const attachments = includeAttachments ? (message.attachments ?? []) : []; - const firstAttachment = attachments?.find((entry) => entry?.original_path && !entry?.missing); - const mediaPath = firstAttachment?.original_path ?? undefined; - const mediaType = firstAttachment?.mime_type ?? undefined; - const kind = mediaKindFromMime(mediaType ?? undefined); - const placeholder = kind ? `` : attachments?.length ? "" : ""; - const bodyText = messageText || placeholder; - if (!bodyText) return; - const chatTarget = formatIMessageChatTarget(chatId); const fromLabel = isGroup ? `${message.chat_name || "iMessage Group"} id:${chatId ?? "unknown"}` : `${normalizeIMessageHandle(sender)} id:${sender}`; - const createdAt = message.created_at ? Date.parse(message.created_at) : undefined; const body = formatAgentEnvelope({ channel: "iMessage", from: fromLabel, @@ -314,20 +330,11 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P body: bodyText, }); let combinedBody = body; - const historyKey = isGroup - ? String(chatId ?? chatGuid ?? chatIdentifier ?? "unknown") - : undefined; if (isGroup && historyKey && historyLimit > 0) { - combinedBody = buildHistoryContextFromMap({ + combinedBody = buildPendingHistoryContextFromMap({ historyMap: groupHistories, historyKey, limit: historyLimit, - entry: { - sender: normalizeIMessageHandle(sender), - body: bodyText, - timestamp: createdAt, - messageId: message.id ? String(message.id) : undefined, - }, currentMessage: combinedBody, formatEntry: (entry) => formatAgentEnvelope({ @@ -393,8 +400,6 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P ); } - let didSendReply = false; - // Create mutable context for response prefix template interpolation let prefixContext: ResponsePrefixContext = { identityName: resolveIdentityName(cfg, route.agentId), @@ -414,7 +419,6 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P maxBytes: mediaMaxBytes, textLimit, }); - didSendReply = true; }, onError: (err, info) => { runtime.error?.(danger(`imessage ${info.kind} reply failed: ${String(err)}`)); @@ -440,12 +444,12 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P }, }); if (!queuedFinal) { - if (isGroup && historyKey && historyLimit > 0 && didSendReply) { + if (isGroup && historyKey && historyLimit > 0) { clearHistoryEntries({ historyMap: groupHistories, historyKey }); } return; } - if (isGroup && historyKey && historyLimit > 0 && didSendReply) { + if (isGroup && historyKey && historyLimit > 0) { clearHistoryEntries({ historyMap: groupHistories, historyKey }); } } diff --git a/src/signal/monitor/event-handler.ts b/src/signal/monitor/event-handler.ts index 71dd0b00f..02b618679 100644 --- a/src/signal/monitor/event-handler.ts +++ b/src/signal/monitor/event-handler.ts @@ -14,7 +14,10 @@ import { resolveInboundDebounceMs, } from "../../auto-reply/inbound-debounce.js"; import { dispatchReplyFromConfig } from "../../auto-reply/reply/dispatch-from-config.js"; -import { buildHistoryContextFromMap, clearHistoryEntries } from "../../auto-reply/reply/history.js"; +import { + buildPendingHistoryContextFromMap, + clearHistoryEntries, +} from "../../auto-reply/reply/history.js"; import { createReplyDispatcher } from "../../auto-reply/reply/reply-dispatcher.js"; import { resolveStorePath, updateLastRoute } from "../../config/sessions.js"; import { danger, logVerbose, shouldLogVerbose } from "../../globals.js"; @@ -72,16 +75,10 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) { let combinedBody = body; const historyKey = entry.isGroup ? String(entry.groupId ?? "unknown") : undefined; if (entry.isGroup && historyKey && deps.historyLimit > 0) { - combinedBody = buildHistoryContextFromMap({ + combinedBody = buildPendingHistoryContextFromMap({ historyMap: deps.groupHistories, historyKey, limit: deps.historyLimit, - entry: { - sender: entry.senderName, - body: entry.bodyText, - timestamp: entry.timestamp ?? undefined, - messageId: entry.messageId, - }, currentMessage: combinedBody, formatEntry: (historyEntry) => formatAgentEnvelope({ @@ -150,8 +147,6 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) { logVerbose(`signal inbound: from=${ctxPayload.From} len=${body.length} preview="${preview}"`); } - let didSendReply = false; - // Create mutable context for response prefix template interpolation let prefixContext: ResponsePrefixContext = { identityName: resolveIdentityName(deps.cfg, route.agentId), @@ -172,7 +167,6 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) { maxBytes: deps.mediaMaxBytes, textLimit: deps.textLimit, }); - didSendReply = true; }, onError: (err, info) => { deps.runtime.error?.(danger(`signal ${info.kind} reply failed: ${String(err)}`)); @@ -196,12 +190,12 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) { }, }); if (!queuedFinal) { - if (entry.isGroup && historyKey && deps.historyLimit > 0 && didSendReply) { + if (entry.isGroup && historyKey && deps.historyLimit > 0) { clearHistoryEntries({ historyMap: deps.groupHistories, historyKey }); } return; } - if (entry.isGroup && historyKey && deps.historyLimit > 0 && didSendReply) { + if (entry.isGroup && historyKey && deps.historyLimit > 0) { clearHistoryEntries({ historyMap: deps.groupHistories, historyKey }); } } diff --git a/src/slack/monitor.tool-result.sends-tool-summaries-responseprefix.test.ts b/src/slack/monitor.tool-result.sends-tool-summaries-responseprefix.test.ts index 33792f6f6..15e5678cb 100644 --- a/src/slack/monitor.tool-result.sends-tool-summaries-responseprefix.test.ts +++ b/src/slack/monitor.tool-result.sends-tool-summaries-responseprefix.test.ts @@ -264,7 +264,7 @@ describe("monitorSlackProvider tool results", () => { expect(sendMock.mock.calls[1][1]).toBe("final reply"); }); - it("wraps room history in Body and preserves RawBody", async () => { + it("preserves RawBody without injecting processed room history", async () => { config = { messages: { ackReactionScope: "group-mentions" }, channels: { @@ -320,9 +320,9 @@ describe("monitorSlackProvider tool results", () => { await run; expect(replyMock).toHaveBeenCalledTimes(2); - expect(capturedCtx.Body).toContain(HISTORY_CONTEXT_MARKER); - expect(capturedCtx.Body).toContain(CURRENT_MESSAGE_MARKER); - expect(capturedCtx.Body).toContain("first"); + expect(capturedCtx.Body).not.toContain(HISTORY_CONTEXT_MARKER); + expect(capturedCtx.Body).not.toContain(CURRENT_MESSAGE_MARKER); + expect(capturedCtx.Body).not.toContain("first"); expect(capturedCtx.RawBody).toBe("second"); expect(capturedCtx.CommandBody).toBe("second"); }); @@ -334,7 +334,7 @@ describe("monitorSlackProvider tool results", () => { slack: { historyLimit: 5, dm: { enabled: true, policy: "open", allowFrom: ["*"] }, - channels: { C1: { allow: true, requireMention: false } }, + channels: { C1: { allow: true, requireMention: true } }, }, }, }; @@ -372,7 +372,7 @@ describe("monitorSlackProvider tool results", () => { event: { type: "message", user: "U1", - text: "thread-a-two", + text: "<@bot-user> thread-a-two", ts: "201", thread_ts: "100", channel: "C1", @@ -384,7 +384,7 @@ describe("monitorSlackProvider tool results", () => { event: { type: "message", user: "U2", - text: "thread-b-one", + text: "<@bot-user> thread-b-one", ts: "301", thread_ts: "300", channel: "C1", @@ -396,10 +396,10 @@ describe("monitorSlackProvider tool results", () => { controller.abort(); await run; - expect(replyMock).toHaveBeenCalledTimes(3); - expect(capturedCtx[1]?.Body).toContain("thread-a-one"); - expect(capturedCtx[2]?.Body).not.toContain("thread-a-one"); - expect(capturedCtx[2]?.Body).not.toContain("thread-a-two"); + expect(replyMock).toHaveBeenCalledTimes(2); + expect(capturedCtx[0]?.Body).toContain("thread-a-one"); + expect(capturedCtx[1]?.Body).not.toContain("thread-a-one"); + expect(capturedCtx[1]?.Body).not.toContain("thread-a-two"); }); it("updates assistant thread status when replies start", async () => { diff --git a/src/slack/monitor/message-handler/dispatch.ts b/src/slack/monitor/message-handler/dispatch.ts index fb0eb66b1..9f8d3ba7e 100644 --- a/src/slack/monitor/message-handler/dispatch.ts +++ b/src/slack/monitor/message-handler/dispatch.ts @@ -66,8 +66,6 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag }); }; - let didSendReply = false; - // Create mutable context for response prefix template interpolation let prefixContext: ResponsePrefixContext = { identityName: resolveIdentityName(cfg, route.agentId), @@ -88,7 +86,6 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag textLimit: ctx.textLimit, replyThreadTs, }); - didSendReply = true; replyPlan.markSent(); }, onError: (err, info) => { @@ -136,7 +133,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag } if (!queuedFinal) { - if (prepared.isRoomish && ctx.historyLimit > 0 && didSendReply) { + if (prepared.isRoomish && ctx.historyLimit > 0) { clearHistoryEntries({ historyMap: ctx.channelHistories, historyKey: prepared.historyKey, @@ -168,7 +165,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag }); } - if (prepared.isRoomish && ctx.historyLimit > 0 && didSendReply) { + if (prepared.isRoomish && ctx.historyLimit > 0) { clearHistoryEntries({ historyMap: ctx.channelHistories, historyKey: prepared.historyKey, diff --git a/src/slack/monitor/message-handler/prepare.ts b/src/slack/monitor/message-handler/prepare.ts index f284ec527..ba4ba55ea 100644 --- a/src/slack/monitor/message-handler/prepare.ts +++ b/src/slack/monitor/message-handler/prepare.ts @@ -2,7 +2,10 @@ import { resolveAckReaction } from "../../../agents/identity.js"; import { hasControlCommand } from "../../../auto-reply/command-detection.js"; import { shouldHandleTextCommands } from "../../../auto-reply/commands-registry.js"; import { formatAgentEnvelope, formatThreadStarterEnvelope } from "../../../auto-reply/envelope.js"; -import { buildHistoryContextFromMap } from "../../../auto-reply/reply/history.js"; +import { + buildPendingHistoryContextFromMap, + recordPendingHistoryEntry, +} from "../../../auto-reply/reply/history.js"; import { buildMentionRegexes, matchesMentionPatterns } from "../../../auto-reply/reply/mentions.js"; import { logVerbose, shouldLogVerbose } from "../../../globals.js"; import { enqueueSystemEvent } from "../../../infra/system-events.js"; @@ -167,6 +170,19 @@ export async function prepareSlackMessage(params: { }, }); + const baseSessionKey = route.sessionKey; + const threadTs = message.thread_ts; + const hasThreadTs = typeof threadTs === "string" && threadTs.length > 0; + const isThreadReply = hasThreadTs && (threadTs !== message.ts || Boolean(message.parent_user_id)); + const threadKeys = resolveThreadSessionKeys({ + baseSessionKey, + threadId: isThreadReply ? threadTs : undefined, + parentSessionKey: isThreadReply && ctx.threadInheritParent ? baseSessionKey : undefined, + }); + const sessionKey = threadKeys.sessionKey; + const historyKey = + isThreadReply && ctx.threadHistoryScope === "thread" ? sessionKey : message.channel; + const mentionRegexes = buildMentionRegexes(cfg, route.agentId); const wasMentioned = opts.wasMentioned ?? @@ -233,6 +249,28 @@ export async function prepareSlackMessage(params: { const effectiveWasMentioned = mentionGate.effectiveWasMentioned; if (isRoom && shouldRequireMention && mentionGate.shouldSkip) { ctx.logger.info({ channel: message.channel, reason: "no-mention" }, "skipping room message"); + if (ctx.historyLimit > 0) { + const pendingText = (message.text ?? "").trim(); + const fallbackFile = message.files?.[0]?.name + ? `[Slack file: ${message.files[0].name}]` + : message.files?.length + ? "[Slack file]" + : ""; + const pendingBody = pendingText || fallbackFile; + if (pendingBody) { + recordPendingHistoryEntry({ + historyMap: ctx.channelHistories, + historyKey, + limit: ctx.historyLimit, + entry: { + sender: senderName, + body: pendingBody, + timestamp: message.ts ? Math.round(Number(message.ts) * 1000) : undefined, + messageId: message.ts, + }, + }); + } + } return null; } @@ -277,16 +315,6 @@ export async function prepareSlackMessage(params: { : null; const roomLabel = channelName ? `#${channelName}` : `#${message.channel}`; - const historyEntry = - isRoomish && ctx.historyLimit > 0 - ? { - sender: senderName, - body: rawBody, - timestamp: message.ts ? Math.round(Number(message.ts) * 1000) : undefined, - messageId: message.ts, - } - : undefined; - const preview = rawBody.replace(/\s+/g, " ").slice(0, 160); const inboundLabel = isDirectMessage ? `Slack DM from ${senderName}` @@ -297,18 +325,6 @@ export async function prepareSlackMessage(params: { ? `slack:channel:${message.channel}` : `slack:group:${message.channel}`; - const baseSessionKey = route.sessionKey; - const threadTs = message.thread_ts; - const hasThreadTs = typeof threadTs === "string" && threadTs.length > 0; - const isThreadReply = hasThreadTs && (threadTs !== message.ts || Boolean(message.parent_user_id)); - const threadKeys = resolveThreadSessionKeys({ - baseSessionKey, - threadId: isThreadReply ? threadTs : undefined, - parentSessionKey: isThreadReply && ctx.threadInheritParent ? baseSessionKey : undefined, - }); - const sessionKey = threadKeys.sessionKey; - const historyKey = - isThreadReply && ctx.threadHistoryScope === "thread" ? sessionKey : message.channel; enqueueSystemEvent(`${inboundLabel}: ${preview}`, { sessionKey, contextKey: `slack:message:${message.channel}:${message.ts ?? "unknown"}`, @@ -324,11 +340,10 @@ export async function prepareSlackMessage(params: { let combinedBody = body; if (isRoomish && ctx.historyLimit > 0) { - combinedBody = buildHistoryContextFromMap({ + combinedBody = buildPendingHistoryContextFromMap({ historyMap: ctx.channelHistories, historyKey, limit: ctx.historyLimit, - entry: historyEntry, currentMessage: combinedBody, formatEntry: (entry) => formatAgentEnvelope({ diff --git a/src/telegram/bot-message-context.ts b/src/telegram/bot-message-context.ts index 271db0f50..3027c96b8 100644 --- a/src/telegram/bot-message-context.ts +++ b/src/telegram/bot-message-context.ts @@ -3,7 +3,10 @@ import { resolveAckReaction } from "../agents/identity.js"; import { hasControlCommand } from "../auto-reply/command-detection.js"; import { normalizeCommandBody } from "../auto-reply/commands-registry.js"; import { formatAgentEnvelope } from "../auto-reply/envelope.js"; -import { buildHistoryContextFromMap } from "../auto-reply/reply/history.js"; +import { + buildPendingHistoryContextFromMap, + recordPendingHistoryEntry, +} from "../auto-reply/reply/history.js"; import { buildMentionRegexes, matchesMentionPatterns } from "../auto-reply/reply/mentions.js"; import { formatLocationText, toLocationContext } from "../channels/location.js"; import { resolveStorePath, updateLastRoute } from "../config/sessions.js"; @@ -200,6 +203,25 @@ export const buildTelegramMessageContext = async ({ senderId, senderUsername, }); + const historyKey = isGroup ? buildTelegramGroupPeerId(chatId, resolvedThreadId) : undefined; + + let placeholder = ""; + if (msg.photo) placeholder = ""; + else if (msg.video) placeholder = ""; + else if (msg.audio || msg.voice) placeholder = ""; + else if (msg.document) placeholder = ""; + + const locationData = extractTelegramLocation(msg); + const locationText = locationData ? formatLocationText(locationData) : undefined; + const rawText = (msg.text ?? msg.caption ?? "").trim(); + let rawBody = [rawText, locationText].filter(Boolean).join("\n").trim(); + if (!rawBody) rawBody = placeholder; + if (!rawBody && allMedia.length === 0) return null; + + let bodyText = rawBody; + if (!bodyText && allMedia.length > 0) { + bodyText = `${allMedia.length > 1 ? ` (${allMedia.length} images)` : ""}`; + } const computedWasMentioned = (Boolean(botUsername) && hasBotMention(msg, botUsername)) || matchesMentionPatterns(msg.text ?? msg.caption ?? "", mentionRegexes); @@ -243,6 +265,19 @@ export const buildTelegramMessageContext = async ({ if (isGroup && requireMention && canDetectMention) { if (mentionGate.shouldSkip) { logger.info({ chatId, reason: "no-mention" }, "skipping group message"); + if (historyKey && historyLimit > 0) { + recordPendingHistoryEntry({ + historyMap: groupHistories, + historyKey, + limit: historyLimit, + entry: { + sender: buildSenderLabel(msg, senderId || chatId), + body: rawBody, + timestamp: msg.date ? msg.date * 1000 : undefined, + messageId: typeof msg.message_id === "number" ? String(msg.message_id) : undefined, + }, + }); + } return null; } } @@ -283,25 +318,7 @@ export const buildTelegramMessageContext = async ({ ) : null; - let placeholder = ""; - if (msg.photo) placeholder = ""; - else if (msg.video) placeholder = ""; - else if (msg.audio || msg.voice) placeholder = ""; - else if (msg.document) placeholder = ""; - const replyTarget = describeReplyTarget(msg); - const locationData = extractTelegramLocation(msg); - const locationText = locationData ? formatLocationText(locationData) : undefined; - const rawText = (msg.text ?? msg.caption ?? "").trim(); - let rawBody = [rawText, locationText].filter(Boolean).join("\n").trim(); - if (!rawBody) rawBody = placeholder; - if (!rawBody && allMedia.length === 0) return null; - - let bodyText = rawBody; - if (!bodyText && allMedia.length > 0) { - bodyText = `${allMedia.length > 1 ? ` (${allMedia.length} images)` : ""}`; - } - const replySuffix = replyTarget ? `\n\n[Replying to ${replyTarget.sender}${ replyTarget.id ? ` id:${replyTarget.id}` : "" @@ -317,18 +334,11 @@ export const buildTelegramMessageContext = async ({ body: `${bodyText}${replySuffix}`, }); let combinedBody = body; - const historyKey = isGroup ? buildTelegramGroupPeerId(chatId, resolvedThreadId) : undefined; if (isGroup && historyKey && historyLimit > 0) { - combinedBody = buildHistoryContextFromMap({ + combinedBody = buildPendingHistoryContextFromMap({ historyMap: groupHistories, historyKey, limit: historyLimit, - entry: { - sender: buildSenderLabel(msg, senderId || chatId), - body: rawBody, - timestamp: msg.date ? msg.date * 1000 : undefined, - messageId: typeof msg.message_id === "number" ? String(msg.message_id) : undefined, - }, currentMessage: combinedBody, formatEntry: (entry) => formatAgentEnvelope({ diff --git a/src/telegram/bot-message-dispatch.ts b/src/telegram/bot-message-dispatch.ts index cbcf321e1..79f57a28c 100644 --- a/src/telegram/bot-message-dispatch.ts +++ b/src/telegram/bot-message-dispatch.ts @@ -124,7 +124,6 @@ export const dispatchTelegramMessage = async ({ identityName: resolveIdentityName(cfg, route.agentId), }; - let didSendReply = false; const { queuedFinal } = await dispatchReplyWithBufferedBlockDispatcher({ ctx: ctxPayload, cfg, @@ -147,7 +146,6 @@ export const dispatchTelegramMessage = async ({ messageThreadId: resolvedThreadId, onVoiceRecording: sendRecordVoice, }); - didSendReply = true; }, onError: (err, info) => { runtime.error?.(danger(`telegram ${info.kind} reply failed: ${String(err)}`)); @@ -174,7 +172,7 @@ export const dispatchTelegramMessage = async ({ }); draftStream?.stop(); if (!queuedFinal) { - if (isGroup && historyKey && historyLimit > 0 && didSendReply) { + if (isGroup && historyKey && historyLimit > 0) { clearHistoryEntries({ historyMap: groupHistories, historyKey }); } return; @@ -189,7 +187,7 @@ export const dispatchTelegramMessage = async ({ }); }); } - if (isGroup && historyKey && historyLimit > 0 && didSendReply) { + if (isGroup && historyKey && historyLimit > 0) { clearHistoryEntries({ historyMap: groupHistories, historyKey }); } }; 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 9a42efb90..12b7d9728 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 @@ -106,7 +106,7 @@ describe("web auto-reply", () => { vi.useRealTimers(); }); - it("supports always-on group activation with silent token and preserves history", async () => { + it("supports always-on group activation with silent token and clears pending history", async () => { const sendMedia = vi.fn(); const reply = vi.fn().mockResolvedValue(undefined); const sendComposing = vi.fn(); @@ -180,9 +180,9 @@ describe("web auto-reply", () => { expect(resolver).toHaveBeenCalledTimes(2); const payload = resolver.mock.calls[1][0]; - expect(payload.Body).toContain("Chat messages since your last reply"); - expect(payload.Body).toContain("Alice (+111): first"); - expect(payload.Body).toContain("[message_id: g-always-1]"); + 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(reply).toHaveBeenCalledTimes(1); diff --git a/src/web/auto-reply/monitor/broadcast.ts b/src/web/auto-reply/monitor/broadcast.ts index 1013d50d4..ef76ce3b0 100644 --- a/src/web/auto-reply/monitor/broadcast.ts +++ b/src/web/auto-reply/monitor/broadcast.ts @@ -77,17 +77,15 @@ export async function maybeBroadcastMessage(params: { } }; - let didSendReply = false; if (strategy === "sequential") { for (const agentId of broadcastAgents) { - if (await processForAgent(agentId)) didSendReply = true; + await processForAgent(agentId); } } else { - const results = await Promise.allSettled(broadcastAgents.map(processForAgent)); - didSendReply = results.some((result) => result.status === "fulfilled" && result.value); + await Promise.allSettled(broadcastAgents.map(processForAgent)); } - if (params.msg.chatType === "group" && didSendReply) { + if (params.msg.chatType === "group") { params.groupHistories.set(params.groupHistoryKey, []); } diff --git a/src/web/auto-reply/monitor/group-gating.ts b/src/web/auto-reply/monitor/group-gating.ts index eeddc5c10..a4e46d41e 100644 --- a/src/web/auto-reply/monitor/group-gating.ts +++ b/src/web/auto-reply/monitor/group-gating.ts @@ -6,6 +6,7 @@ import { resolveMentionGating } from "../../../channels/mention-gating.js"; import type { MentionConfig } from "../mentions.js"; import { buildMentionConfig, debugMention, resolveOwnerList } from "../mentions.js"; import type { WebInboundMsg } from "../types.js"; +import { recordPendingHistoryEntry } from "../../../auto-reply/reply/history.js"; import { stripMentionsForCommand } from "./commands.js"; import { resolveGroupActivationFor, resolveGroupPolicyFor } from "./group-activation.js"; import { noteGroupMember } from "./group-members.js"; @@ -65,26 +66,27 @@ export function applyGroupGating(params: { if (activationCommand.hasCommand && !owner) { params.logVerbose(`Ignoring /activation from non-owner in group ${params.conversationId}`); + if (params.groupHistoryLimit > 0) { + const sender = + params.msg.senderName && params.msg.senderE164 + ? `${params.msg.senderName} (${params.msg.senderE164})` + : (params.msg.senderName ?? params.msg.senderE164 ?? "Unknown"); + recordPendingHistoryEntry({ + historyMap: params.groupHistories, + historyKey: params.groupHistoryKey, + limit: params.groupHistoryLimit, + entry: { + sender, + body: params.msg.body, + timestamp: params.msg.timestamp, + id: params.msg.id, + senderJid: params.msg.senderJid, + }, + }); + } return { shouldProcess: false }; } - if (!shouldBypassMention) { - const history = params.groupHistories.get(params.groupHistoryKey) ?? []; - const sender = - params.msg.senderName && params.msg.senderE164 - ? `${params.msg.senderName} (${params.msg.senderE164})` - : (params.msg.senderName ?? params.msg.senderE164 ?? "Unknown"); - history.push({ - sender, - body: params.msg.body, - timestamp: params.msg.timestamp, - id: params.msg.id, - senderJid: params.msg.senderJid, - }); - while (history.length > params.groupHistoryLimit) history.shift(); - params.groupHistories.set(params.groupHistoryKey, history); - } - const mentionDebug = debugMention(params.msg, mentionConfig, params.authDir); params.replyLogger.debug( { @@ -124,6 +126,24 @@ export function applyGroupGating(params: { params.logVerbose( `Group message stored for context (no mention detected) in ${params.conversationId}: ${params.msg.body}`, ); + if (params.groupHistoryLimit > 0) { + const sender = + params.msg.senderName && params.msg.senderE164 + ? `${params.msg.senderName} (${params.msg.senderE164})` + : (params.msg.senderName ?? params.msg.senderE164 ?? "Unknown"); + recordPendingHistoryEntry({ + historyMap: params.groupHistories, + historyKey: params.groupHistoryKey, + limit: params.groupHistoryLimit, + entry: { + sender, + body: params.msg.body, + timestamp: params.msg.timestamp, + id: params.msg.id, + senderJid: params.msg.senderJid, + }, + }); + } return { shouldProcess: false }; } diff --git a/src/web/auto-reply/monitor/process-message.ts b/src/web/auto-reply/monitor/process-message.ts index 51570b1f8..2d6f26bd4 100644 --- a/src/web/auto-reply/monitor/process-message.ts +++ b/src/web/auto-reply/monitor/process-message.ts @@ -76,10 +76,9 @@ export async function processMessage(params: { if (params.msg.chatType === "group") { const history = params.groupHistory ?? params.groupHistories.get(params.groupHistoryKey) ?? []; - const historyWithoutCurrent = history.length > 0 ? history.slice(0, -1) : []; - if (historyWithoutCurrent.length > 0) { + if (history.length > 0) { const lineBreak = "\\n"; - const historyText = historyWithoutCurrent + const historyText = history .map((m) => { const bodyWithId = m.id ? `${m.body}\n[message_id: ${m.id}]` : m.body; return formatAgentEnvelope({ @@ -299,14 +298,14 @@ export async function processMessage(params: { }); if (!queuedFinal) { - if (shouldClearGroupHistory && didSendReply) { + if (shouldClearGroupHistory) { params.groupHistories.set(params.groupHistoryKey, []); } logVerbose("Skipping auto-reply: silent token or no text/media returned from resolver"); return false; } - if (shouldClearGroupHistory && didSendReply) { + if (shouldClearGroupHistory) { params.groupHistories.set(params.groupHistoryKey, []); }