refactor: centralize history context wrapping

This commit is contained in:
Peter Steinberger
2026-01-10 19:09:06 +01:00
parent b977ae19af
commit 82f71d25e5
8 changed files with 137 additions and 83 deletions

View File

@@ -3,6 +3,7 @@ import {
appendHistoryEntry, appendHistoryEntry,
buildHistoryContext, buildHistoryContext,
buildHistoryContextFromEntries, buildHistoryContextFromEntries,
buildHistoryContextFromMap,
HISTORY_CONTEXT_MARKER, HISTORY_CONTEXT_MARKER,
} from "./history.js"; } from "./history.js";
import { CURRENT_MESSAGE_MARKER } from "./mentions.js"; import { CURRENT_MESSAGE_MARKER } from "./mentions.js";
@@ -60,4 +61,31 @@ describe("history helpers", () => {
"three", "three",
]); ]);
}); });
it("builds context from map and appends entry", () => {
const historyMap = new Map<string, { sender: string; body: string }[]>();
historyMap.set("room", [
{ sender: "A", body: "one" },
{ sender: "B", body: "two" },
]);
const result = buildHistoryContextFromMap({
historyMap,
historyKey: "room",
limit: 3,
entry: { sender: "C", body: "three" },
currentMessage: "current",
formatEntry: (entry) => `${entry.sender}: ${entry.body}`,
});
expect(historyMap.get("room")?.map((entry) => entry.body)).toEqual([
"one",
"two",
"three",
]);
expect(result).toContain(HISTORY_CONTEXT_MARKER);
expect(result).toContain("A: one");
expect(result).toContain("B: two");
expect(result).not.toContain("C: three");
});
}); });

View File

@@ -43,6 +43,41 @@ export function appendHistoryEntry(params: {
return history; return history;
} }
export function buildHistoryContextFromMap(params: {
historyMap: Map<string, HistoryEntry[]>;
historyKey: string;
limit: number;
entry?: HistoryEntry;
currentMessage: string;
formatEntry: (entry: HistoryEntry) => string;
lineBreak?: string;
excludeLast?: boolean;
}): string {
if (params.limit <= 0) return params.currentMessage;
const entries = params.entry
? appendHistoryEntry({
historyMap: params.historyMap,
historyKey: params.historyKey,
entry: params.entry,
limit: params.limit,
})
: (params.historyMap.get(params.historyKey) ?? []);
return buildHistoryContextFromEntries({
entries,
currentMessage: params.currentMessage,
formatEntry: params.formatEntry,
lineBreak: params.lineBreak,
excludeLast: params.excludeLast,
});
}
export function clearHistoryEntries(params: {
historyMap: Map<string, HistoryEntry[]>;
historyKey: string;
}): void {
params.historyMap.set(params.historyKey, []);
}
export function buildHistoryContextFromEntries(params: { export function buildHistoryContextFromEntries(params: {
entries: HistoryEntry[]; entries: HistoryEntry[];
currentMessage: string; currentMessage: string;

View File

@@ -35,8 +35,8 @@ import {
} from "../auto-reply/envelope.js"; } from "../auto-reply/envelope.js";
import { dispatchReplyFromConfig } from "../auto-reply/reply/dispatch-from-config.js"; import { dispatchReplyFromConfig } from "../auto-reply/reply/dispatch-from-config.js";
import { import {
appendHistoryEntry, buildHistoryContextFromMap,
buildHistoryContextFromEntries, clearHistoryEntries,
type HistoryEntry, type HistoryEntry,
} from "../auto-reply/reply/history.js"; } from "../auto-reply/reply/history.js";
import { import {
@@ -887,23 +887,19 @@ export function createDiscordMessageHandler(params: {
const textForHistory = resolveDiscordMessageText(message, { const textForHistory = resolveDiscordMessageText(message, {
includeForwarded: true, includeForwarded: true,
}); });
if (isGuildMessage && historyLimit > 0 && textForHistory) { const historyEntry =
appendHistoryEntry({ isGuildMessage && historyLimit > 0 && textForHistory
historyMap: guildHistories, ? {
historyKey: message.channelId, sender:
limit: historyLimit, data.member?.nickname ??
entry: { author.globalName ??
sender: author.username ??
data.member?.nickname ?? author.id,
author.globalName ?? body: textForHistory,
author.username ?? timestamp: resolveTimestampMs(message.timestamp),
author.id, messageId: message.id,
body: textForHistory, }
timestamp: resolveTimestampMs(message.timestamp), : undefined;
messageId: message.id,
},
});
}
const shouldRequireMention = const shouldRequireMention =
channelConfig?.requireMention ?? guildInfo?.requireMention ?? true; channelConfig?.requireMention ?? guildInfo?.requireMention ?? true;
@@ -1049,10 +1045,11 @@ export function createDiscordMessageHandler(params: {
}); });
let shouldClearHistory = false; let shouldClearHistory = false;
if (!isDirectMessage) { if (!isDirectMessage) {
const history = combinedBody = buildHistoryContextFromMap({
historyLimit > 0 ? (guildHistories.get(message.channelId) ?? []) : []; historyMap: guildHistories,
combinedBody = buildHistoryContextFromEntries({ historyKey: message.channelId,
entries: history, limit: historyLimit,
entry: historyEntry,
currentMessage: combinedBody, currentMessage: combinedBody,
formatEntry: (entry) => formatEntry: (entry) =>
formatAgentEnvelope({ formatAgentEnvelope({
@@ -1227,7 +1224,10 @@ export function createDiscordMessageHandler(params: {
historyLimit > 0 && historyLimit > 0 &&
didSendReply didSendReply
) { ) {
guildHistories.set(message.channelId, []); clearHistoryEntries({
historyMap: guildHistories,
historyKey: message.channelId,
});
} }
return; return;
} }
@@ -1262,7 +1262,10 @@ export function createDiscordMessageHandler(params: {
historyLimit > 0 && historyLimit > 0 &&
didSendReply didSendReply
) { ) {
guildHistories.set(message.channelId, []); clearHistoryEntries({
historyMap: guildHistories,
historyKey: message.channelId,
});
} }
} catch (err) { } catch (err) {
runtime.error?.(danger(`handler failed: ${String(err)}`)); runtime.error?.(danger(`handler failed: ${String(err)}`));

View File

@@ -7,8 +7,8 @@ import { hasControlCommand } from "../auto-reply/command-detection.js";
import { formatAgentEnvelope } from "../auto-reply/envelope.js"; import { formatAgentEnvelope } from "../auto-reply/envelope.js";
import { dispatchReplyFromConfig } from "../auto-reply/reply/dispatch-from-config.js"; import { dispatchReplyFromConfig } from "../auto-reply/reply/dispatch-from-config.js";
import { import {
appendHistoryEntry, buildHistoryContextFromMap,
buildHistoryContextFromEntries, clearHistoryEntries,
DEFAULT_GROUP_HISTORY_LIMIT, DEFAULT_GROUP_HISTORY_LIMIT,
type HistoryEntry, type HistoryEntry,
} from "../auto-reply/reply/history.js"; } from "../auto-reply/reply/history.js";
@@ -409,7 +409,7 @@ export async function monitorIMessageProvider(
? String(chatId ?? chatGuid ?? chatIdentifier ?? "unknown") ? String(chatId ?? chatGuid ?? chatIdentifier ?? "unknown")
: undefined; : undefined;
if (isGroup && historyKey && historyLimit > 0) { if (isGroup && historyKey && historyLimit > 0) {
appendHistoryEntry({ combinedBody = buildHistoryContextFromMap({
historyMap: groupHistories, historyMap: groupHistories,
historyKey, historyKey,
limit: historyLimit, limit: historyLimit,
@@ -419,10 +419,6 @@ export async function monitorIMessageProvider(
timestamp: createdAt, timestamp: createdAt,
messageId: message.id ? String(message.id) : undefined, messageId: message.id ? String(message.id) : undefined,
}, },
});
const history = groupHistories.get(historyKey) ?? [];
combinedBody = buildHistoryContextFromEntries({
entries: history,
currentMessage: combinedBody, currentMessage: combinedBody,
formatEntry: (entry) => formatEntry: (entry) =>
formatAgentEnvelope({ formatAgentEnvelope({
@@ -527,12 +523,12 @@ export async function monitorIMessageProvider(
}); });
if (!queuedFinal) { if (!queuedFinal) {
if (isGroup && historyKey && historyLimit > 0 && didSendReply) { if (isGroup && historyKey && historyLimit > 0 && didSendReply) {
groupHistories.set(historyKey, []); clearHistoryEntries({ historyMap: groupHistories, historyKey });
} }
return; return;
} }
if (isGroup && historyKey && historyLimit > 0 && didSendReply) { if (isGroup && historyKey && historyLimit > 0 && didSendReply) {
groupHistories.set(historyKey, []); clearHistoryEntries({ historyMap: groupHistories, historyKey });
} }
}; };

View File

@@ -1,8 +1,8 @@
import { formatAgentEnvelope } from "../auto-reply/envelope.js"; import { formatAgentEnvelope } from "../auto-reply/envelope.js";
import { dispatchReplyFromConfig } from "../auto-reply/reply/dispatch-from-config.js"; import { dispatchReplyFromConfig } from "../auto-reply/reply/dispatch-from-config.js";
import { import {
appendHistoryEntry, buildHistoryContextFromMap,
buildHistoryContextFromEntries, clearHistoryEntries,
DEFAULT_GROUP_HISTORY_LIMIT, DEFAULT_GROUP_HISTORY_LIMIT,
type HistoryEntry, type HistoryEntry,
} from "../auto-reply/reply/history.js"; } from "../auto-reply/reply/history.js";
@@ -432,7 +432,7 @@ function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
const isRoomish = !isDirectMessage; const isRoomish = !isDirectMessage;
const historyKey = isRoomish ? conversationId : undefined; const historyKey = isRoomish ? conversationId : undefined;
if (isRoomish && historyKey && historyLimit > 0) { if (isRoomish && historyKey && historyLimit > 0) {
appendHistoryEntry({ combinedBody = buildHistoryContextFromMap({
historyMap: conversationHistories, historyMap: conversationHistories,
historyKey, historyKey,
limit: historyLimit, limit: historyLimit,
@@ -442,10 +442,6 @@ function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
timestamp: timestamp?.getTime(), timestamp: timestamp?.getTime(),
messageId: activity.id ?? undefined, messageId: activity.id ?? undefined,
}, },
});
const history = conversationHistories.get(historyKey) ?? [];
combinedBody = buildHistoryContextFromEntries({
entries: history,
currentMessage: combinedBody, currentMessage: combinedBody,
formatEntry: (entry) => formatEntry: (entry) =>
formatAgentEnvelope({ formatAgentEnvelope({
@@ -520,7 +516,10 @@ function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
const didSendReply = counts.final + counts.tool + counts.block > 0; const didSendReply = counts.final + counts.tool + counts.block > 0;
if (!queuedFinal) { if (!queuedFinal) {
if (isRoomish && historyKey && historyLimit > 0 && didSendReply) { if (isRoomish && historyKey && historyLimit > 0 && didSendReply) {
conversationHistories.set(historyKey, []); clearHistoryEntries({
historyMap: conversationHistories,
historyKey,
});
} }
return; return;
} }
@@ -531,7 +530,10 @@ function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
); );
} }
if (isRoomish && historyKey && historyLimit > 0 && didSendReply) { if (isRoomish && historyKey && historyLimit > 0 && didSendReply) {
conversationHistories.set(historyKey, []); clearHistoryEntries({
historyMap: conversationHistories,
historyKey,
});
} }
} catch (err) { } catch (err) {
log.error("dispatch failed", { error: String(err) }); log.error("dispatch failed", { error: String(err) });

View File

@@ -6,8 +6,8 @@ import { chunkText, resolveTextChunkLimit } from "../auto-reply/chunk.js";
import { formatAgentEnvelope } from "../auto-reply/envelope.js"; import { formatAgentEnvelope } from "../auto-reply/envelope.js";
import { dispatchReplyFromConfig } from "../auto-reply/reply/dispatch-from-config.js"; import { dispatchReplyFromConfig } from "../auto-reply/reply/dispatch-from-config.js";
import { import {
appendHistoryEntry, buildHistoryContextFromMap,
buildHistoryContextFromEntries, clearHistoryEntries,
DEFAULT_GROUP_HISTORY_LIMIT, DEFAULT_GROUP_HISTORY_LIMIT,
type HistoryEntry, type HistoryEntry,
} from "../auto-reply/reply/history.js"; } from "../auto-reply/reply/history.js";
@@ -635,7 +635,7 @@ export async function monitorSignalProvider(
let combinedBody = body; let combinedBody = body;
const historyKey = isGroup ? String(groupId ?? "unknown") : undefined; const historyKey = isGroup ? String(groupId ?? "unknown") : undefined;
if (isGroup && historyKey && historyLimit > 0) { if (isGroup && historyKey && historyLimit > 0) {
appendHistoryEntry({ combinedBody = buildHistoryContextFromMap({
historyMap: groupHistories, historyMap: groupHistories,
historyKey, historyKey,
limit: historyLimit, limit: historyLimit,
@@ -648,10 +648,6 @@ export async function monitorSignalProvider(
? String(envelope.timestamp) ? String(envelope.timestamp)
: undefined, : undefined,
}, },
});
const history = groupHistories.get(historyKey) ?? [];
combinedBody = buildHistoryContextFromEntries({
entries: history,
currentMessage: combinedBody, currentMessage: combinedBody,
formatEntry: (entry) => formatEntry: (entry) =>
formatAgentEnvelope({ formatAgentEnvelope({
@@ -763,12 +759,12 @@ export async function monitorSignalProvider(
}); });
if (!queuedFinal) { if (!queuedFinal) {
if (isGroup && historyKey && historyLimit > 0 && didSendReply) { if (isGroup && historyKey && historyLimit > 0 && didSendReply) {
groupHistories.set(historyKey, []); clearHistoryEntries({ historyMap: groupHistories, historyKey });
} }
return; return;
} }
if (isGroup && historyKey && historyLimit > 0 && didSendReply) { if (isGroup && historyKey && historyLimit > 0 && didSendReply) {
groupHistories.set(historyKey, []); clearHistoryEntries({ historyMap: groupHistories, historyKey });
} }
}; };

View File

@@ -25,8 +25,8 @@ import {
} from "../auto-reply/envelope.js"; } from "../auto-reply/envelope.js";
import { dispatchReplyFromConfig } from "../auto-reply/reply/dispatch-from-config.js"; import { dispatchReplyFromConfig } from "../auto-reply/reply/dispatch-from-config.js";
import { import {
appendHistoryEntry, buildHistoryContextFromMap,
buildHistoryContextFromEntries, clearHistoryEntries,
DEFAULT_GROUP_HISTORY_LIMIT, DEFAULT_GROUP_HISTORY_LIMIT,
type HistoryEntry, type HistoryEntry,
} from "../auto-reply/reply/history.js"; } from "../auto-reply/reply/history.js";
@@ -969,21 +969,17 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
const roomLabel = channelName ? `#${channelName}` : `#${message.channel}`; const roomLabel = channelName ? `#${channelName}` : `#${message.channel}`;
const isRoomish = isRoom || isGroupDm; const isRoomish = isRoom || isGroupDm;
const historyKey = message.channel; const historyKey = message.channel;
if (isRoomish && historyLimit > 0) { const historyEntry =
appendHistoryEntry({ isRoomish && historyLimit > 0
historyMap: channelHistories, ? {
historyKey, sender: senderName,
limit: historyLimit, body: rawBody,
entry: { timestamp: message.ts
sender: senderName, ? Math.round(Number(message.ts) * 1000)
body: rawBody, : undefined,
timestamp: message.ts messageId: message.ts,
? Math.round(Number(message.ts) * 1000) }
: undefined, : undefined;
messageId: message.ts,
},
});
}
const preview = rawBody.replace(/\s+/g, " ").slice(0, 160); const preview = rawBody.replace(/\s+/g, " ").slice(0, 160);
const inboundLabel = isDirectMessage const inboundLabel = isDirectMessage
@@ -1021,9 +1017,11 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
let combinedBody = body; let combinedBody = body;
if (isRoomish && historyLimit > 0) { if (isRoomish && historyLimit > 0) {
const history = channelHistories.get(historyKey) ?? []; combinedBody = buildHistoryContextFromMap({
combinedBody = buildHistoryContextFromEntries({ historyMap: channelHistories,
entries: history, historyKey,
limit: historyLimit,
entry: historyEntry,
currentMessage: combinedBody, currentMessage: combinedBody,
formatEntry: (entry) => formatEntry: (entry) =>
formatAgentEnvelope({ formatAgentEnvelope({
@@ -1220,7 +1218,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
} }
if (!queuedFinal) { if (!queuedFinal) {
if (isRoomish && historyLimit > 0 && didSendReply) { if (isRoomish && historyLimit > 0 && didSendReply) {
channelHistories.set(historyKey, []); clearHistoryEntries({ historyMap: channelHistories, historyKey });
} }
return; return;
} }
@@ -1245,7 +1243,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
}); });
} }
if (isRoomish && historyLimit > 0 && didSendReply) { if (isRoomish && historyLimit > 0 && didSendReply) {
channelHistories.set(historyKey, []); clearHistoryEntries({ historyMap: channelHistories, historyKey });
} }
}; };

View File

@@ -21,8 +21,8 @@ import {
import { formatAgentEnvelope } from "../auto-reply/envelope.js"; import { formatAgentEnvelope } from "../auto-reply/envelope.js";
import { resolveTelegramDraftStreamingChunking } from "../auto-reply/reply/block-streaming.js"; import { resolveTelegramDraftStreamingChunking } from "../auto-reply/reply/block-streaming.js";
import { import {
appendHistoryEntry, buildHistoryContextFromMap,
buildHistoryContextFromEntries, clearHistoryEntries,
DEFAULT_GROUP_HISTORY_LIMIT, DEFAULT_GROUP_HISTORY_LIMIT,
type HistoryEntry, type HistoryEntry,
} from "../auto-reply/reply/history.js"; } from "../auto-reply/reply/history.js";
@@ -671,7 +671,7 @@ export function createTelegramBot(opts: TelegramBotOptions) {
? buildTelegramGroupPeerId(chatId, messageThreadId) ? buildTelegramGroupPeerId(chatId, messageThreadId)
: undefined; : undefined;
if (isGroup && historyKey && historyLimit > 0) { if (isGroup && historyKey && historyLimit > 0) {
appendHistoryEntry({ combinedBody = buildHistoryContextFromMap({
historyMap: groupHistories, historyMap: groupHistories,
historyKey, historyKey,
limit: historyLimit, limit: historyLimit,
@@ -684,10 +684,6 @@ export function createTelegramBot(opts: TelegramBotOptions) {
? String(msg.message_id) ? String(msg.message_id)
: undefined, : undefined,
}, },
});
const history = groupHistories.get(historyKey) ?? [];
combinedBody = buildHistoryContextFromEntries({
entries: history,
currentMessage: combinedBody, currentMessage: combinedBody,
formatEntry: (entry) => formatEntry: (entry) =>
formatAgentEnvelope({ formatAgentEnvelope({
@@ -907,7 +903,7 @@ export function createTelegramBot(opts: TelegramBotOptions) {
draftStream?.stop(); draftStream?.stop();
if (!queuedFinal) { if (!queuedFinal) {
if (isGroup && historyKey && historyLimit > 0 && didSendReply) { if (isGroup && historyKey && historyLimit > 0 && didSendReply) {
groupHistories.set(historyKey, []); clearHistoryEntries({ historyMap: groupHistories, historyKey });
} }
return; return;
} }
@@ -927,7 +923,7 @@ export function createTelegramBot(opts: TelegramBotOptions) {
}); });
} }
if (isGroup && historyKey && historyLimit > 0 && didSendReply) { if (isGroup && historyKey && historyLimit > 0 && didSendReply) {
groupHistories.set(historyKey, []); clearHistoryEntries({ historyMap: groupHistories, historyKey });
} }
}; };