refactor: unify pending history helpers

This commit is contained in:
Peter Steinberger
2026-01-23 22:36:43 +00:00
parent 05e7e06146
commit 521ea4ae5b
13 changed files with 196 additions and 155 deletions

View File

@@ -8,9 +8,9 @@ import type {
} from "clawdbot/plugin-sdk";
import {
buildPendingHistoryContextFromMap,
clearHistoryEntries,
clearHistoryEntriesIfEnabled,
DEFAULT_GROUP_HISTORY_LIMIT,
recordPendingHistoryEntry,
recordPendingHistoryEntryIfEnabled,
resolveChannelMediaMaxBytes,
type HistoryEntry,
} from "clawdbot/plugin-sdk";
@@ -534,19 +534,19 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
: "");
const pendingSender = senderName;
const recordPendingHistory = () => {
if (!historyKey || historyLimit <= 0) return;
const trimmed = pendingBody.trim();
if (!trimmed) return;
recordPendingHistoryEntry({
recordPendingHistoryEntryIfEnabled({
historyMap: channelHistories,
historyKey,
limit: historyLimit,
entry: {
sender: pendingSender,
body: trimmed,
timestamp: typeof post.create_at === "number" ? post.create_at : undefined,
messageId: post.id ?? undefined,
},
historyKey: historyKey ?? "",
entry: historyKey && trimmed
? {
sender: pendingSender,
body: trimmed,
timestamp: typeof post.create_at === "number" ? post.create_at : undefined,
messageId: post.id ?? undefined,
}
: null,
});
};
@@ -623,7 +623,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
sender: { name: senderName, id: senderId },
});
let combinedBody = body;
if (historyKey && historyLimit > 0) {
if (historyKey) {
combinedBody = buildPendingHistoryContextFromMap({
historyMap: channelHistories,
historyKey,
@@ -772,8 +772,8 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
},
});
markDispatchIdle();
if (historyKey && historyLimit > 0) {
clearHistoryEntries({ historyMap: channelHistories, historyKey });
if (historyKey) {
clearHistoryEntriesIfEnabled({ historyMap: channelHistories, historyKey, limit: historyLimit });
}
};

View File

@@ -1,8 +1,8 @@
import {
buildPendingHistoryContextFromMap,
clearHistoryEntries,
clearHistoryEntriesIfEnabled,
DEFAULT_GROUP_HISTORY_LIMIT,
recordPendingHistoryEntry,
recordPendingHistoryEntryIfEnabled,
resolveMentionGating,
formatAllowlistMatchMeta,
type HistoryEntry,
@@ -371,19 +371,17 @@ 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,
},
});
}
recordPendingHistoryEntryIfEnabled({
historyMap: conversationHistories,
historyKey: conversationId,
limit: historyLimit,
entry: {
sender: senderName,
body: rawBody,
timestamp: timestamp?.getTime(),
messageId: activity.id ?? undefined,
},
});
return;
}
}
@@ -426,7 +424,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
let combinedBody = body;
const isRoomish = !isDirectMessage;
const historyKey = isRoomish ? conversationId : undefined;
if (isRoomish && historyKey && historyLimit > 0) {
if (isRoomish && historyKey) {
combinedBody = buildPendingHistoryContextFromMap({
historyMap: conversationHistories,
historyKey,
@@ -512,10 +510,11 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
const didSendReply = counts.final + counts.tool + counts.block > 0;
if (!queuedFinal) {
if (isRoomish && historyKey && historyLimit > 0) {
clearHistoryEntries({
if (isRoomish && historyKey) {
clearHistoryEntriesIfEnabled({
historyMap: conversationHistories,
historyKey,
limit: historyLimit,
});
}
return;
@@ -524,8 +523,12 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
logVerboseMessage(
`msteams: delivered ${finalCount} reply${finalCount === 1 ? "" : "ies"} to ${teamsTo}`,
);
if (isRoomish && historyKey && historyLimit > 0) {
clearHistoryEntries({ historyMap: conversationHistories, historyKey });
if (isRoomish && historyKey) {
clearHistoryEntriesIfEnabled({
historyMap: conversationHistories,
historyKey,
limit: historyLimit,
});
}
} catch (err) {
log.error("dispatch failed", { error: String(err) });

View File

@@ -47,6 +47,22 @@ export function recordPendingHistoryEntry<T extends HistoryEntry>(params: {
return appendHistoryEntry(params);
}
export function recordPendingHistoryEntryIfEnabled<T extends HistoryEntry>(params: {
historyMap: Map<string, T[]>;
historyKey: string;
entry?: T | null;
limit: number;
}): T[] {
if (!params.entry) return [];
if (params.limit <= 0) return [];
return recordPendingHistoryEntry({
historyMap: params.historyMap,
historyKey: params.historyKey,
entry: params.entry,
limit: params.limit,
});
}
export function buildPendingHistoryContextFromMap(params: {
historyMap: Map<string, HistoryEntry[]>;
historyKey: string;
@@ -101,6 +117,15 @@ export function clearHistoryEntries(params: {
params.historyMap.set(params.historyKey, []);
}
export function clearHistoryEntriesIfEnabled(params: {
historyMap: Map<string, HistoryEntry[]>;
historyKey: string;
limit: number;
}): void {
if (params.limit <= 0) return;
clearHistoryEntries({ historyMap: params.historyMap, historyKey: params.historyKey });
}
export function buildHistoryContextFromEntries(params: {
entries: HistoryEntry[];
currentMessage: string;

View File

@@ -2,7 +2,10 @@ 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 { recordPendingHistoryEntry, type HistoryEntry } from "../../auto-reply/reply/history.js";
import {
recordPendingHistoryEntryIfEnabled,
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";
@@ -410,14 +413,12 @@ 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,
});
}
recordPendingHistoryEntryIfEnabled({
historyMap: params.guildHistories,
historyKey: message.channelId,
limit: params.historyLimit,
entry: historyEntry ?? null,
});
return null;
}
}

View File

@@ -20,7 +20,7 @@ import {
import { dispatchInboundMessage } from "../../auto-reply/dispatch.js";
import {
buildPendingHistoryContextFromMap,
clearHistoryEntries,
clearHistoryEntriesIfEnabled,
} from "../../auto-reply/reply/history.js";
import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js";
import { createReplyDispatcherWithTyping } from "../../auto-reply/reply/reply-dispatcher.js";
@@ -383,10 +383,11 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
});
markDispatchIdle();
if (!queuedFinal) {
if (isGuildMessage && historyLimit > 0) {
clearHistoryEntries({
if (isGuildMessage) {
clearHistoryEntriesIfEnabled({
historyMap: guildHistories,
historyKey: message.channelId,
limit: historyLimit,
});
}
return;
@@ -409,10 +410,11 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
);
},
});
if (isGuildMessage && historyLimit > 0) {
clearHistoryEntries({
if (isGuildMessage) {
clearHistoryEntriesIfEnabled({
historyMap: guildHistories,
historyKey: message.channelId,
limit: historyLimit,
});
}
}

View File

@@ -24,9 +24,9 @@ import { dispatchInboundMessage } from "../../auto-reply/dispatch.js";
import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js";
import {
buildPendingHistoryContextFromMap,
clearHistoryEntries,
clearHistoryEntriesIfEnabled,
DEFAULT_GROUP_HISTORY_LIMIT,
recordPendingHistoryEntry,
recordPendingHistoryEntryIfEnabled,
type HistoryEntry,
} from "../../auto-reply/reply/history.js";
import { buildMentionRegexes, matchesMentionPatterns } from "../../auto-reply/reply/mentions.js";
@@ -405,19 +405,19 @@ 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: senderNormalized,
body: bodyText,
timestamp: createdAt,
messageId: message.id ? String(message.id) : undefined,
},
});
}
recordPendingHistoryEntryIfEnabled({
historyMap: groupHistories,
historyKey: historyKey ?? "",
limit: historyLimit,
entry: historyKey
? {
sender: senderNormalized,
body: bodyText,
timestamp: createdAt,
messageId: message.id ? String(message.id) : undefined,
}
: null,
});
return;
}
@@ -454,7 +454,7 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
envelope: envelopeOptions,
});
let combinedBody = body;
if (isGroup && historyKey && historyLimit > 0) {
if (isGroup && historyKey) {
combinedBody = buildPendingHistoryContextFromMap({
historyMap: groupHistories,
historyKey,
@@ -584,13 +584,17 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
},
});
if (!queuedFinal) {
if (isGroup && historyKey && historyLimit > 0) {
clearHistoryEntries({ historyMap: groupHistories, historyKey });
if (isGroup && historyKey) {
clearHistoryEntriesIfEnabled({
historyMap: groupHistories,
historyKey,
limit: historyLimit,
});
}
return;
}
if (isGroup && historyKey && historyLimit > 0) {
clearHistoryEntries({ historyMap: groupHistories, historyKey });
if (isGroup && historyKey) {
clearHistoryEntriesIfEnabled({ historyMap: groupHistories, historyKey, limit: historyLimit });
}
}

View File

@@ -108,8 +108,10 @@ export { SILENT_REPLY_TOKEN, isSilentReplyText } from "../auto-reply/tokens.js";
export {
buildPendingHistoryContextFromMap,
clearHistoryEntries,
clearHistoryEntriesIfEnabled,
DEFAULT_GROUP_HISTORY_LIMIT,
recordPendingHistoryEntry,
recordPendingHistoryEntryIfEnabled,
} from "../auto-reply/reply/history.js";
export type { HistoryEntry } from "../auto-reply/reply/history.js";
export { mergeAllowlist, summarizeMapping } from "../channels/allowlists/resolve-utils.js";

View File

@@ -20,7 +20,7 @@ import {
import { dispatchInboundMessage } from "../../auto-reply/dispatch.js";
import {
buildPendingHistoryContextFromMap,
clearHistoryEntries,
clearHistoryEntriesIfEnabled,
} from "../../auto-reply/reply/history.js";
import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js";
import { createReplyDispatcherWithTyping } from "../../auto-reply/reply/reply-dispatcher.js";
@@ -111,7 +111,7 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
});
let combinedBody = body;
const historyKey = entry.isGroup ? String(entry.groupId ?? "unknown") : undefined;
if (entry.isGroup && historyKey && deps.historyLimit > 0) {
if (entry.isGroup && historyKey) {
combinedBody = buildPendingHistoryContextFromMap({
historyMap: deps.groupHistories,
historyKey,
@@ -244,13 +244,21 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
});
markDispatchIdle();
if (!queuedFinal) {
if (entry.isGroup && historyKey && deps.historyLimit > 0) {
clearHistoryEntries({ historyMap: deps.groupHistories, historyKey });
if (entry.isGroup && historyKey) {
clearHistoryEntriesIfEnabled({
historyMap: deps.groupHistories,
historyKey,
limit: deps.historyLimit,
});
}
return;
}
if (entry.isGroup && historyKey && deps.historyLimit > 0) {
clearHistoryEntries({ historyMap: deps.groupHistories, historyKey });
if (entry.isGroup && historyKey) {
clearHistoryEntriesIfEnabled({
historyMap: deps.groupHistories,
historyKey,
limit: deps.historyLimit,
});
}
}

View File

@@ -8,7 +8,7 @@ import {
type ResponsePrefixContext,
} from "../../../auto-reply/reply/response-prefix-template.js";
import { dispatchInboundMessage } from "../../../auto-reply/dispatch.js";
import { clearHistoryEntries } from "../../../auto-reply/reply/history.js";
import { clearHistoryEntriesIfEnabled } from "../../../auto-reply/reply/history.js";
import { removeAckReactionAfterReply } from "../../../channels/ack-reactions.js";
import { createReplyDispatcherWithTyping } from "../../../auto-reply/reply/reply-dispatcher.js";
import { resolveStorePath, updateLastRoute } from "../../../config/sessions.js";
@@ -137,10 +137,11 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
}
if (!queuedFinal) {
if (prepared.isRoomish && ctx.historyLimit > 0) {
clearHistoryEntries({
if (prepared.isRoomish) {
clearHistoryEntriesIfEnabled({
historyMap: ctx.channelHistories,
historyKey: prepared.historyKey,
limit: ctx.historyLimit,
});
}
return;
@@ -174,10 +175,11 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
},
});
if (prepared.isRoomish && ctx.historyLimit > 0) {
clearHistoryEntries({
if (prepared.isRoomish) {
clearHistoryEntriesIfEnabled({
historyMap: ctx.channelHistories,
historyKey: prepared.historyKey,
limit: ctx.historyLimit,
});
}
}

View File

@@ -9,7 +9,7 @@ import {
} from "../../../auto-reply/envelope.js";
import {
buildPendingHistoryContextFromMap,
recordPendingHistoryEntry,
recordPendingHistoryEntryIfEnabled,
} from "../../../auto-reply/reply/history.js";
import { finalizeInboundContext } from "../../../auto-reply/reply/inbound-context.js";
import { buildMentionRegexes, matchesMentionPatterns } from "../../../auto-reply/reply/mentions.js";
@@ -292,28 +292,26 @@ export async function prepareSlackMessage(params: {
const effectiveWasMentioned = mentionGate.effectiveWasMentioned;
if (isRoom && shouldRequireMention && mentionGate.shouldSkip) {
ctx.logger.info({ channel: message.channel, reason: "no-mention" }, "skipping channel 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: {
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;
recordPendingHistoryEntryIfEnabled({
historyMap: ctx.channelHistories,
historyKey,
limit: ctx.historyLimit,
entry: pendingBody
? {
sender: senderName,
body: pendingBody,
timestamp: message.ts ? Math.round(Number(message.ts) * 1000) : undefined,
messageId: message.ts,
},
});
}
}
}
: null,
});
return null;
}

View File

@@ -6,7 +6,7 @@ import { normalizeCommandBody } from "../auto-reply/commands-registry.js";
import { formatInboundEnvelope, resolveEnvelopeFormatOptions } from "../auto-reply/envelope.js";
import {
buildPendingHistoryContextFromMap,
recordPendingHistoryEntry,
recordPendingHistoryEntryIfEnabled,
type HistoryEntry,
} from "../auto-reply/reply/history.js";
import { finalizeInboundContext } from "../auto-reply/reply/inbound-context.js";
@@ -350,19 +350,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,
},
});
}
recordPendingHistoryEntryIfEnabled({
historyMap: groupHistories,
historyKey: historyKey ?? "",
limit: historyLimit,
entry: historyKey
? {
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,
}
: null,
});
return null;
}
}

View File

@@ -5,7 +5,7 @@ import {
type ResponsePrefixContext,
} from "../auto-reply/reply/response-prefix-template.js";
import { EmbeddedBlockChunker } from "../agents/pi-embedded-block-chunker.js";
import { clearHistoryEntries } from "../auto-reply/reply/history.js";
import { clearHistoryEntriesIfEnabled } from "../auto-reply/reply/history.js";
import { dispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/provider-dispatcher.js";
import { removeAckReactionAfterReply } from "../channels/ack-reactions.js";
import { danger, logVerbose } from "../globals.js";
@@ -180,8 +180,8 @@ export const dispatchTelegramMessage = async ({
});
draftStream?.stop();
if (!queuedFinal) {
if (isGroup && historyKey && historyLimit > 0) {
clearHistoryEntries({ historyMap: groupHistories, historyKey });
if (isGroup && historyKey) {
clearHistoryEntriesIfEnabled({ historyMap: groupHistories, historyKey, limit: historyLimit });
}
return;
}
@@ -197,7 +197,7 @@ export const dispatchTelegramMessage = async ({
);
},
});
if (isGroup && historyKey && historyLimit > 0) {
clearHistoryEntries({ historyMap: groupHistories, historyKey });
if (isGroup && historyKey) {
clearHistoryEntriesIfEnabled({ historyMap: groupHistories, historyKey, limit: historyLimit });
}
};

View File

@@ -6,7 +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 { recordPendingHistoryEntryIfEnabled } from "../../../auto-reply/reply/history.js";
import { stripMentionsForCommand } from "./commands.js";
import { resolveGroupActivationFor, resolveGroupPolicyFor } from "./group-activation.js";
import { noteGroupMember } from "./group-members.js";
@@ -66,24 +66,22 @@ 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,
},
});
}
const sender =
params.msg.senderName && params.msg.senderE164
? `${params.msg.senderName} (${params.msg.senderE164})`
: (params.msg.senderName ?? params.msg.senderE164 ?? "Unknown");
recordPendingHistoryEntryIfEnabled({
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 };
}
@@ -126,24 +124,22 @@ 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,
},
});
}
const sender =
params.msg.senderName && params.msg.senderE164
? `${params.msg.senderName} (${params.msg.senderE164})`
: (params.msg.senderName ?? params.msg.senderE164 ?? "Unknown");
recordPendingHistoryEntryIfEnabled({
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 };
}