fix: scope history injection to pending-only
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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)]`
|
||||
|
||||
@@ -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:<agentId>:whatsapp:group:<jid>` 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 "<subject>". 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.
|
||||
|
||||
@@ -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.<channel>.historyLimit` (or `channels.<channel>.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.<channel>.historyLimit` (or `channels.<channel>.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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<string, { sender: string; body: string }[]>();
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,12 +23,12 @@ export function buildHistoryContext(params: {
|
||||
);
|
||||
}
|
||||
|
||||
export function appendHistoryEntry(params: {
|
||||
historyMap: Map<string, HistoryEntry[]>;
|
||||
export function appendHistoryEntry<T extends HistoryEntry>(params: {
|
||||
historyMap: Map<string, T[]>;
|
||||
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<T extends HistoryEntry>(params: {
|
||||
historyMap: Map<string, T[]>;
|
||||
historyKey: string;
|
||||
entry: T;
|
||||
limit: number;
|
||||
}): T[] {
|
||||
return appendHistoryEntry(params);
|
||||
}
|
||||
|
||||
export function buildPendingHistoryContextFromMap(params: {
|
||||
historyMap: Map<string, HistoryEntry[]>;
|
||||
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<string, HistoryEntry[]>;
|
||||
historyKey: string;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ? `<media:${kind}>` : attachments?.length ? "<media:attachment>" : "";
|
||||
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 ? `<media:${kind}>` : attachments?.length ? "<media:attachment>" : "";
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 = "<media:image>";
|
||||
else if (msg.video) placeholder = "<media:video>";
|
||||
else if (msg.audio || msg.voice) placeholder = "<media:audio>";
|
||||
else if (msg.document) placeholder = "<media:document>";
|
||||
|
||||
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 = `<media:image>${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 = "<media:image>";
|
||||
else if (msg.video) placeholder = "<media:video>";
|
||||
else if (msg.audio || msg.voice) placeholder = "<media:audio>";
|
||||
else if (msg.document) placeholder = "<media:document>";
|
||||
|
||||
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 = `<media:image>${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({
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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, []);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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, []);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user