Skip to content

Commit 6de8563

Browse files
committed
refactor: centralize channel history window
1 parent 56303b9 commit 6de8563

11 files changed

Lines changed: 187 additions & 38 deletions

File tree

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
228e53a4748d3e797b54fba1ac9069ef65b459ece5807e100e01f14c872e0610 plugin-sdk-api-baseline.json
2-
e675bb9b25b849f6a54eca75fcd343efc1258cb1f5e33cf3ec0a00aa7eaf4f05 plugin-sdk-api-baseline.jsonl
1+
6baa084622419b66219a494865773fa1f5000d392558d8150afa247ce20fb66d plugin-sdk-api-baseline.json
2+
bb414f636e114ecc41ab3184da768d712f53b24f4f2b65de679affcdb7e0efce plugin-sdk-api-baseline.jsonl

extensions/discord/src/monitor/message-handler.context.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,7 @@ import { resolveChannelContextVisibilityMode } from "openclaw/plugin-sdk/context
66
import { resolvePinnedMainDmOwnerFromAllowlist } from "openclaw/plugin-sdk/conversation-runtime";
77
import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
88
import { finalizeInboundContext } from "openclaw/plugin-sdk/reply-dispatch-runtime";
9-
import {
10-
buildInboundHistoryFromMap,
11-
buildPendingHistoryContextFromMap,
12-
} from "openclaw/plugin-sdk/reply-history";
9+
import { createChannelHistoryWindow } from "openclaw/plugin-sdk/reply-history";
1310
import { buildAgentSessionKey, resolveThreadSessionKeys } from "openclaw/plugin-sdk/routing";
1411
import { danger, logVerbose, shouldLogVerbose } from "openclaw/plugin-sdk/runtime-env";
1512
import { evaluateSupplementalContextVisibility } from "openclaw/plugin-sdk/security-runtime";
@@ -154,6 +151,7 @@ export async function buildDiscordMessageProcessContext(params: {
154151
storePath,
155152
sessionKey: route.sessionKey,
156153
});
154+
const channelHistory = createChannelHistoryWindow({ historyMap: guildHistories });
157155
let combinedBody = formatInboundEnvelope({
158156
channel: "Discord",
159157
from: fromLabel,
@@ -167,8 +165,7 @@ export async function buildDiscordMessageProcessContext(params: {
167165
const shouldIncludeChannelHistory =
168166
!isDirectMessage && !(isGuildMessage && channelConfig?.autoThread && !threadChannel);
169167
if (shouldIncludeChannelHistory) {
170-
combinedBody = buildPendingHistoryContextFromMap({
171-
historyMap: guildHistories,
168+
combinedBody = channelHistory.buildPendingContext({
172169
historyKey: messageChannelId,
173170
limit: historyLimit,
174171
currentMessage: combinedBody,
@@ -316,8 +313,7 @@ export async function buildDiscordMessageProcessContext(params: {
316313
}
317314
const lastRouteTo = dmConversationTarget ?? effectiveTo;
318315
const inboundHistory = shouldIncludeChannelHistory
319-
? buildInboundHistoryFromMap({
320-
historyMap: guildHistories,
316+
? channelHistory.buildInboundHistory({
321317
historyKey: messageChannelId,
322318
limit: historyLimit,
323319
})

extensions/slack/src/monitor/message-handler/dispatch.preview-fallback.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,9 @@ vi.mock("openclaw/plugin-sdk/outbound-runtime", () => ({
525525

526526
vi.mock("openclaw/plugin-sdk/reply-history", () => ({
527527
clearHistoryEntriesIfEnabled: () => {},
528+
createChannelHistoryWindow: () => ({
529+
clear: () => {},
530+
}),
528531
}));
529532

530533
vi.mock("openclaw/plugin-sdk/reply-payload", () => ({

extensions/slack/src/monitor/message-handler/dispatch.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import {
3838
} from "openclaw/plugin-sdk/inbound-reply-dispatch";
3939
import { resolveAgentOutboundIdentity } from "openclaw/plugin-sdk/outbound-runtime";
4040
import { mergePairLoopGuardConfig } from "openclaw/plugin-sdk/pair-loop-guard-runtime";
41-
import { clearHistoryEntriesIfEnabled } from "openclaw/plugin-sdk/reply-history";
41+
import { createChannelHistoryWindow } from "openclaw/plugin-sdk/reply-history";
4242
import { resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
4343
import type { ReplyDispatchKind, ReplyPayload } from "openclaw/plugin-sdk/reply-runtime";
4444
import { resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
@@ -1396,12 +1396,12 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
13961396
agentId: route.agentId,
13971397
});
13981398
}
1399+
const channelHistory = createChannelHistoryWindow({ historyMap: ctx.channelHistories });
13991400

14001401
if (!anyReplyDelivered) {
14011402
await draftStream?.clear();
14021403
if (prepared.isRoomish && prepared.requireMention) {
1403-
clearHistoryEntriesIfEnabled({
1404-
historyMap: ctx.channelHistories,
1404+
channelHistory.clear({
14051405
historyKey: prepared.historyKey,
14061406
limit: ctx.historyLimit,
14071407
});
@@ -1443,8 +1443,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
14431443
}
14441444

14451445
if (prepared.isRoomish && prepared.requireMention) {
1446-
clearHistoryEntriesIfEnabled({
1447-
historyMap: ctx.channelHistories,
1446+
channelHistory.clear({
14481447
historyKey: prepared.historyKey,
14491448
limit: ctx.historyLimit,
14501449
});

extensions/slack/src/monitor/message-handler/prepare.ts

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,7 @@ import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
1919
import { recordDroppedChannelTurnHistory } from "openclaw/plugin-sdk/inbound-reply-dispatch";
2020
import { mimeTypeFromFilePath } from "openclaw/plugin-sdk/media-mime";
2121
import { finalizeInboundContext } from "openclaw/plugin-sdk/reply-dispatch-runtime";
22-
import {
23-
buildInboundHistoryFromMap,
24-
buildPendingHistoryContextFromMap,
25-
recordPendingHistoryEntryIfEnabled,
26-
} from "openclaw/plugin-sdk/reply-history";
22+
import { createChannelHistoryWindow } from "openclaw/plugin-sdk/reply-history";
2723
import type { FinalizedMsgContext } from "openclaw/plugin-sdk/reply-runtime";
2824
import { resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
2925
import { logVerbose, shouldLogVerbose } from "openclaw/plugin-sdk/runtime-env";
@@ -974,6 +970,7 @@ export async function prepareSlackMessage(params: {
974970
storePath,
975971
sessionKey,
976972
});
973+
const channelHistory = createChannelHistoryWindow({ historyMap: ctx.channelHistories });
977974
const dmHistoryLimit = isDirectMessage
978975
? resolveSlackDmHistoryLimit({
979976
account,
@@ -1007,8 +1004,7 @@ export async function prepareSlackMessage(params: {
10071004
combinedBody = `${dmHistoryContext.body}\n\n${combinedBody}`;
10081005
}
10091006
if (isRoomish && ctx.historyLimit > 0) {
1010-
combinedBody = buildPendingHistoryContextFromMap({
1011-
historyMap: ctx.channelHistories,
1007+
combinedBody = channelHistory.buildPendingContext({
10121008
historyKey,
10131009
limit: ctx.historyLimit,
10141010
currentMessage: combinedBody,
@@ -1064,8 +1060,7 @@ export async function prepareSlackMessage(params: {
10641060

10651061
const inboundHistory =
10661062
isRoomish && ctx.historyLimit > 0
1067-
? buildInboundHistoryFromMap({
1068-
historyMap: ctx.channelHistories,
1063+
? channelHistory.buildInboundHistory({
10691064
historyKey,
10701065
limit: ctx.historyLimit,
10711066
})
@@ -1132,8 +1127,7 @@ export async function prepareSlackMessage(params: {
11321127
}) satisfies FinalizedMsgContext;
11331128

11341129
if (isRoomish && !shouldRequireMention) {
1135-
recordPendingHistoryEntryIfEnabled({
1136-
historyMap: ctx.channelHistories,
1130+
channelHistory.record({
11371131
historyKey,
11381132
limit: ctx.historyLimit,
11391133
entry: {

extensions/zalouser/src/monitor.group-gating.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -944,6 +944,7 @@ describe("zalouser monitor group mention gating", () => {
944944
sender: "Alice",
945945
body: "first unmentioned line",
946946
timestamp: 1700000000000,
947+
messageId: "history-1",
947948
},
948949
]);
949950
expect(firstDispatch?.ctx?.Body ?? "").toContain("first unmentioned line");

extensions/zalouser/src/monitor.ts

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,9 @@ import { KeyedAsyncQueue } from "openclaw/plugin-sdk/core";
1010
import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
1111
import { createDeferred } from "openclaw/plugin-sdk/extension-shared";
1212
import {
13-
buildInboundHistoryFromMap,
1413
DEFAULT_GROUP_HISTORY_LIMIT,
1514
type HistoryEntry,
16-
buildPendingHistoryContextFromMap,
17-
clearHistoryEntriesIfEnabled,
18-
recordPendingHistoryEntryIfEnabled,
15+
createChannelHistoryWindow,
1916
} from "openclaw/plugin-sdk/reply-history";
2017
import {
2118
deliverTextOrMediaReply,
@@ -489,6 +486,9 @@ async function processMessage(
489486
},
490487
});
491488
const historyKey = isGroup ? route.sessionKey : undefined;
489+
const channelHistory = createChannelHistoryWindow({
490+
historyMap: historyState.groupHistories,
491+
});
492492

493493
const requireMention = isGroup
494494
? resolveGroupRequireMention({
@@ -538,8 +538,7 @@ async function processMessage(
538538
return;
539539
}
540540
if (isGroup && mentionDecision.shouldSkip) {
541-
recordPendingHistoryEntryIfEnabled({
542-
historyMap: historyState.groupHistories,
541+
channelHistory.record({
543542
historyKey: historyKey ?? "",
544543
limit: historyState.historyLimit,
545544
entry:
@@ -587,8 +586,7 @@ async function processMessage(
587586
});
588587
const combinedBody =
589588
isGroup && historyKey
590-
? buildPendingHistoryContextFromMap({
591-
historyMap: historyState.groupHistories,
589+
? channelHistory.buildPendingContext({
592590
historyKey,
593591
limit: historyState.historyLimit,
594592
currentMessage: body,
@@ -606,8 +604,7 @@ async function processMessage(
606604
: body;
607605
const inboundHistory =
608606
isGroup && historyKey && historyState.historyLimit > 0
609-
? buildInboundHistoryFromMap({
610-
historyMap: historyState.groupHistories,
607+
? channelHistory.buildInboundHistory({
611608
historyKey,
612609
limit: historyState.historyLimit,
613610
})
@@ -750,8 +747,7 @@ async function processMessage(
750747
},
751748
});
752749
if (isGroup && historyKey) {
753-
clearHistoryEntriesIfEnabled({
754-
historyMap: historyState.groupHistories,
750+
channelHistory.clear({
755751
historyKey,
756752
limit: historyState.historyLimit,
757753
});
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { describe, expect, it } from "vitest";
2+
import type { HistoryEntry } from "../../auto-reply/reply/history.types.js";
3+
import { createChannelHistoryWindow } from "./history-window.js";
4+
5+
describe("createChannelHistoryWindow", () => {
6+
it("records, formats, exposes, and clears a channel history window", async () => {
7+
const historyMap = new Map<string, HistoryEntry[]>();
8+
const history = createChannelHistoryWindow({ historyMap });
9+
10+
history.record({
11+
historyKey: "room-1",
12+
limit: 3,
13+
entry: {
14+
sender: "Alice",
15+
body: "first",
16+
timestamp: 1,
17+
messageId: "m1",
18+
},
19+
});
20+
await history.recordWithMedia({
21+
historyKey: "room-1",
22+
limit: 3,
23+
messageId: "m2",
24+
entry: {
25+
sender: "Bob",
26+
body: "<media:image>",
27+
timestamp: 2,
28+
messageId: "m2",
29+
},
30+
media: [
31+
{ path: "/tmp/image.png", contentType: "image/png", kind: "image" },
32+
{ path: "https://example.com/skip.png", contentType: "image/png", kind: "image" },
33+
],
34+
});
35+
36+
expect(
37+
history.buildPendingContext({
38+
historyKey: "room-1",
39+
limit: 3,
40+
currentMessage: "now",
41+
formatEntry: (entry) => `${entry.sender}: ${entry.body}`,
42+
}),
43+
).toContain("Alice: first\nBob: <media:image>");
44+
expect(history.buildInboundHistory({ historyKey: "room-1", limit: 3 })).toEqual([
45+
{
46+
sender: "Alice",
47+
body: "first",
48+
timestamp: 1,
49+
messageId: "m1",
50+
},
51+
{
52+
sender: "Bob",
53+
body: "<media:image>",
54+
timestamp: 2,
55+
messageId: "m2",
56+
media: [
57+
{ path: "/tmp/image.png", contentType: "image/png", kind: "image", messageId: "m2" },
58+
],
59+
},
60+
]);
61+
62+
history.clear({ historyKey: "room-1", limit: 3 });
63+
expect(history.buildInboundHistory({ historyKey: "room-1", limit: 3 })).toEqual([]);
64+
});
65+
});
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import {
2+
buildInboundHistoryFromMap,
3+
buildPendingHistoryContextFromMap,
4+
clearHistoryEntriesIfEnabled,
5+
recordPendingHistoryEntryIfEnabled,
6+
recordPendingHistoryEntryWithMedia,
7+
} from "../../auto-reply/reply/history.js";
8+
import type { HistoryEntry, HistoryMediaEntry } from "../../auto-reply/reply/history.types.js";
9+
10+
type MaybePromise<T> = T | Promise<T>;
11+
12+
export type ChannelHistoryWindow = {
13+
record: (params: {
14+
historyKey: string;
15+
entry?: HistoryEntry | null;
16+
limit: number;
17+
}) => HistoryEntry[];
18+
recordWithMedia: (params: {
19+
historyKey: string;
20+
entry?: HistoryEntry | null;
21+
limit: number;
22+
media?:
23+
| readonly HistoryMediaEntry[]
24+
| null
25+
| (() => MaybePromise<readonly HistoryMediaEntry[] | null | undefined>);
26+
mediaLimit?: number;
27+
messageId?: string;
28+
shouldRecord?: () => boolean;
29+
}) => Promise<HistoryEntry[]>;
30+
buildPendingContext: (params: {
31+
historyKey: string;
32+
limit: number;
33+
currentMessage: string;
34+
formatEntry: (entry: HistoryEntry) => string;
35+
lineBreak?: string;
36+
}) => string;
37+
buildInboundHistory: (params: {
38+
historyKey: string;
39+
limit: number;
40+
}) => HistoryEntry[] | undefined;
41+
clear: (params: { historyKey: string; limit: number }) => void;
42+
};
43+
44+
export function createChannelHistoryWindow(params: {
45+
historyMap: Map<string, HistoryEntry[]>;
46+
}): ChannelHistoryWindow {
47+
const { historyMap } = params;
48+
return {
49+
record: (recordParams) =>
50+
recordPendingHistoryEntryIfEnabled({
51+
historyMap,
52+
historyKey: recordParams.historyKey,
53+
limit: recordParams.limit,
54+
entry: recordParams.entry,
55+
}),
56+
recordWithMedia: (recordParams) =>
57+
recordPendingHistoryEntryWithMedia({
58+
historyMap,
59+
historyKey: recordParams.historyKey,
60+
limit: recordParams.limit,
61+
entry: recordParams.entry,
62+
media: recordParams.media,
63+
mediaLimit: recordParams.mediaLimit,
64+
messageId: recordParams.messageId,
65+
shouldRecord: recordParams.shouldRecord,
66+
}),
67+
buildPendingContext: (contextParams) =>
68+
buildPendingHistoryContextFromMap({
69+
historyMap,
70+
historyKey: contextParams.historyKey,
71+
limit: contextParams.limit,
72+
currentMessage: contextParams.currentMessage,
73+
formatEntry: contextParams.formatEntry,
74+
lineBreak: contextParams.lineBreak,
75+
}),
76+
buildInboundHistory: (historyParams) =>
77+
buildInboundHistoryFromMap({
78+
historyMap,
79+
historyKey: historyParams.historyKey,
80+
limit: historyParams.limit,
81+
}),
82+
clear: (clearParams) =>
83+
clearHistoryEntriesIfEnabled({
84+
historyMap,
85+
historyKey: clearParams.historyKey,
86+
limit: clearParams.limit,
87+
}),
88+
};
89+
}

src/channels/turn/kernel.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export {
2020
listTrackedChannelBotPairsForTests,
2121
recordChannelBotPairLoopAndCheckSuppression,
2222
} from "./bot-loop-protection.js";
23+
export { createChannelHistoryWindow } from "./history-window.js";
24+
export type { ChannelHistoryWindow } from "./history-window.js";
2325
export type { ChannelBotLoopProtectionFacts } from "./bot-loop-protection.js";
2426
export {
2527
deliverDurableInboundReplyPayload,

0 commit comments

Comments
 (0)