@openclaw/feishu 2026.2.25 → 2026.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/index.ts +2 -0
  2. package/package.json +2 -1
  3. package/skills/feishu-doc/SKILL.md +109 -3
  4. package/src/accounts.test.ts +161 -0
  5. package/src/accounts.ts +76 -8
  6. package/src/async.ts +62 -0
  7. package/src/bitable.ts +189 -215
  8. package/src/bot.card-action.test.ts +63 -0
  9. package/src/bot.checkBotMentioned.test.ts +56 -1
  10. package/src/bot.test.ts +1271 -56
  11. package/src/bot.ts +499 -215
  12. package/src/card-action.ts +79 -0
  13. package/src/channel.ts +26 -4
  14. package/src/chat-schema.ts +24 -0
  15. package/src/chat.test.ts +89 -0
  16. package/src/chat.ts +130 -0
  17. package/src/client.test.ts +121 -0
  18. package/src/client.ts +13 -0
  19. package/src/config-schema.test.ts +101 -1
  20. package/src/config-schema.ts +66 -11
  21. package/src/dedup.ts +47 -1
  22. package/src/doc-schema.ts +135 -0
  23. package/src/docx-batch-insert.ts +190 -0
  24. package/src/docx-color-text.ts +149 -0
  25. package/src/docx-table-ops.ts +298 -0
  26. package/src/docx.account-selection.test.ts +70 -0
  27. package/src/docx.test.ts +331 -9
  28. package/src/docx.ts +996 -72
  29. package/src/drive.ts +38 -33
  30. package/src/media.test.ts +227 -7
  31. package/src/media.ts +52 -11
  32. package/src/mention.ts +1 -1
  33. package/src/monitor.account.ts +534 -0
  34. package/src/monitor.reaction.test.ts +578 -0
  35. package/src/monitor.startup.test.ts +203 -0
  36. package/src/monitor.startup.ts +51 -0
  37. package/src/monitor.state.defaults.test.ts +46 -0
  38. package/src/monitor.state.ts +152 -0
  39. package/src/monitor.test-mocks.ts +12 -0
  40. package/src/monitor.transport.ts +163 -0
  41. package/src/monitor.ts +44 -346
  42. package/src/monitor.webhook-security.test.ts +53 -10
  43. package/src/onboarding.status.test.ts +25 -0
  44. package/src/onboarding.ts +144 -52
  45. package/src/outbound.test.ts +181 -0
  46. package/src/outbound.ts +94 -7
  47. package/src/perm.ts +37 -30
  48. package/src/policy.test.ts +56 -1
  49. package/src/policy.ts +5 -1
  50. package/src/post.test.ts +105 -0
  51. package/src/post.ts +274 -0
  52. package/src/probe.test.ts +271 -0
  53. package/src/probe.ts +131 -19
  54. package/src/reply-dispatcher.test.ts +300 -0
  55. package/src/reply-dispatcher.ts +159 -46
  56. package/src/secret-input.ts +19 -0
  57. package/src/send-target.test.ts +74 -0
  58. package/src/send-target.ts +6 -2
  59. package/src/send.reply-fallback.test.ts +105 -0
  60. package/src/send.test.ts +168 -0
  61. package/src/send.ts +143 -18
  62. package/src/streaming-card.ts +131 -43
  63. package/src/targets.test.ts +55 -1
  64. package/src/targets.ts +32 -7
  65. package/src/tool-account-routing.test.ts +129 -0
  66. package/src/tool-account.ts +70 -0
  67. package/src/tool-factory-test-harness.ts +76 -0
  68. package/src/tools-config.test.ts +21 -0
  69. package/src/tools-config.ts +2 -1
  70. package/src/types.ts +10 -1
  71. package/src/typing.test.ts +144 -0
  72. package/src/typing.ts +140 -10
  73. package/src/wiki.ts +55 -50
@@ -0,0 +1,534 @@
1
+ import * as crypto from "crypto";
2
+ import * as Lark from "@larksuiteoapi/node-sdk";
3
+ import type { ClawdbotConfig, RuntimeEnv, HistoryEntry } from "openclaw/plugin-sdk";
4
+ import { resolveFeishuAccount } from "./accounts.js";
5
+ import { raceWithTimeoutAndAbort } from "./async.js";
6
+ import {
7
+ handleFeishuMessage,
8
+ parseFeishuMessageEvent,
9
+ type FeishuMessageEvent,
10
+ type FeishuBotAddedEvent,
11
+ } from "./bot.js";
12
+ import { handleFeishuCardAction, type FeishuCardActionEvent } from "./card-action.js";
13
+ import { createEventDispatcher } from "./client.js";
14
+ import {
15
+ hasRecordedMessage,
16
+ hasRecordedMessagePersistent,
17
+ tryRecordMessage,
18
+ tryRecordMessagePersistent,
19
+ warmupDedupFromDisk,
20
+ } from "./dedup.js";
21
+ import { isMentionForwardRequest } from "./mention.js";
22
+ import { fetchBotOpenIdForMonitor } from "./monitor.startup.js";
23
+ import { botOpenIds } from "./monitor.state.js";
24
+ import { monitorWebhook, monitorWebSocket } from "./monitor.transport.js";
25
+ import { getFeishuRuntime } from "./runtime.js";
26
+ import { getMessageFeishu } from "./send.js";
27
+ import type { ResolvedFeishuAccount } from "./types.js";
28
+
29
+ const FEISHU_REACTION_VERIFY_TIMEOUT_MS = 1_500;
30
+
31
+ export type FeishuReactionCreatedEvent = {
32
+ message_id: string;
33
+ chat_id?: string;
34
+ chat_type?: "p2p" | "group" | "private";
35
+ reaction_type?: { emoji_type?: string };
36
+ operator_type?: string;
37
+ user_id?: { open_id?: string };
38
+ action_time?: string;
39
+ };
40
+
41
+ type ResolveReactionSyntheticEventParams = {
42
+ cfg: ClawdbotConfig;
43
+ accountId: string;
44
+ event: FeishuReactionCreatedEvent;
45
+ botOpenId?: string;
46
+ fetchMessage?: typeof getMessageFeishu;
47
+ verificationTimeoutMs?: number;
48
+ logger?: (message: string) => void;
49
+ uuid?: () => string;
50
+ };
51
+
52
+ export async function resolveReactionSyntheticEvent(
53
+ params: ResolveReactionSyntheticEventParams,
54
+ ): Promise<FeishuMessageEvent | null> {
55
+ const {
56
+ cfg,
57
+ accountId,
58
+ event,
59
+ botOpenId,
60
+ fetchMessage = getMessageFeishu,
61
+ verificationTimeoutMs = FEISHU_REACTION_VERIFY_TIMEOUT_MS,
62
+ logger,
63
+ uuid = () => crypto.randomUUID(),
64
+ } = params;
65
+
66
+ const emoji = event.reaction_type?.emoji_type;
67
+ const messageId = event.message_id;
68
+ const senderId = event.user_id?.open_id;
69
+ if (!emoji || !messageId || !senderId) {
70
+ return null;
71
+ }
72
+
73
+ const account = resolveFeishuAccount({ cfg, accountId });
74
+ const reactionNotifications = account.config.reactionNotifications ?? "own";
75
+ if (reactionNotifications === "off") {
76
+ return null;
77
+ }
78
+
79
+ if (event.operator_type === "app" || senderId === botOpenId) {
80
+ return null;
81
+ }
82
+
83
+ if (emoji === "Typing") {
84
+ return null;
85
+ }
86
+
87
+ if (reactionNotifications === "own" && !botOpenId) {
88
+ logger?.(
89
+ `feishu[${accountId}]: bot open_id unavailable, skipping reaction ${emoji} on ${messageId}`,
90
+ );
91
+ return null;
92
+ }
93
+
94
+ const reactedMsg = await raceWithTimeoutAndAbort(fetchMessage({ cfg, messageId, accountId }), {
95
+ timeoutMs: verificationTimeoutMs,
96
+ })
97
+ .then((result) => (result.status === "resolved" ? result.value : null))
98
+ .catch(() => null);
99
+ const isBotMessage = reactedMsg?.senderType === "app" || reactedMsg?.senderOpenId === botOpenId;
100
+ if (!reactedMsg || (reactionNotifications === "own" && !isBotMessage)) {
101
+ logger?.(
102
+ `feishu[${accountId}]: ignoring reaction on non-bot/unverified message ${messageId} ` +
103
+ `(sender: ${reactedMsg?.senderOpenId ?? "unknown"})`,
104
+ );
105
+ return null;
106
+ }
107
+
108
+ const syntheticChatIdRaw = event.chat_id ?? reactedMsg.chatId;
109
+ const syntheticChatId = syntheticChatIdRaw?.trim() ? syntheticChatIdRaw : `p2p:${senderId}`;
110
+ const syntheticChatType: "p2p" | "group" | "private" =
111
+ event.chat_type === "group" ? "group" : "p2p";
112
+ return {
113
+ sender: {
114
+ sender_id: { open_id: senderId },
115
+ sender_type: "user",
116
+ },
117
+ message: {
118
+ message_id: `${messageId}:reaction:${emoji}:${uuid()}`,
119
+ chat_id: syntheticChatId,
120
+ chat_type: syntheticChatType,
121
+ message_type: "text",
122
+ content: JSON.stringify({
123
+ text: `[reacted with ${emoji} to message ${messageId}]`,
124
+ }),
125
+ },
126
+ };
127
+ }
128
+
129
+ type RegisterEventHandlersContext = {
130
+ cfg: ClawdbotConfig;
131
+ accountId: string;
132
+ runtime?: RuntimeEnv;
133
+ chatHistories: Map<string, HistoryEntry[]>;
134
+ fireAndForget?: boolean;
135
+ };
136
+
137
+ /**
138
+ * Per-chat serial queue that ensures messages from the same chat are processed
139
+ * in arrival order while allowing different chats to run concurrently.
140
+ */
141
+ function createChatQueue() {
142
+ const queues = new Map<string, Promise<void>>();
143
+ return (chatId: string, task: () => Promise<void>): Promise<void> => {
144
+ const prev = queues.get(chatId) ?? Promise.resolve();
145
+ const next = prev.then(task, task);
146
+ queues.set(chatId, next);
147
+ void next.finally(() => {
148
+ if (queues.get(chatId) === next) {
149
+ queues.delete(chatId);
150
+ }
151
+ });
152
+ return next;
153
+ };
154
+ }
155
+
156
+ function mergeFeishuDebounceMentions(
157
+ entries: FeishuMessageEvent[],
158
+ ): FeishuMessageEvent["message"]["mentions"] | undefined {
159
+ const merged = new Map<string, NonNullable<FeishuMessageEvent["message"]["mentions"]>[number]>();
160
+ for (const entry of entries) {
161
+ for (const mention of entry.message.mentions ?? []) {
162
+ const stableId =
163
+ mention.id.open_id?.trim() || mention.id.user_id?.trim() || mention.id.union_id?.trim();
164
+ const mentionName = mention.name?.trim();
165
+ const mentionKey = mention.key?.trim();
166
+ const fallback =
167
+ mentionName && mentionKey ? `${mentionName}|${mentionKey}` : mentionName || mentionKey;
168
+ const key = stableId || fallback;
169
+ if (!key || merged.has(key)) {
170
+ continue;
171
+ }
172
+ merged.set(key, mention);
173
+ }
174
+ }
175
+ if (merged.size === 0) {
176
+ return undefined;
177
+ }
178
+ return Array.from(merged.values());
179
+ }
180
+
181
+ function dedupeFeishuDebounceEntriesByMessageId(
182
+ entries: FeishuMessageEvent[],
183
+ ): FeishuMessageEvent[] {
184
+ const seen = new Set<string>();
185
+ const deduped: FeishuMessageEvent[] = [];
186
+ for (const entry of entries) {
187
+ const messageId = entry.message.message_id?.trim();
188
+ if (!messageId) {
189
+ deduped.push(entry);
190
+ continue;
191
+ }
192
+ if (seen.has(messageId)) {
193
+ continue;
194
+ }
195
+ seen.add(messageId);
196
+ deduped.push(entry);
197
+ }
198
+ return deduped;
199
+ }
200
+
201
+ function resolveFeishuDebounceMentions(params: {
202
+ entries: FeishuMessageEvent[];
203
+ botOpenId?: string;
204
+ }): FeishuMessageEvent["message"]["mentions"] | undefined {
205
+ const { entries, botOpenId } = params;
206
+ if (entries.length === 0) {
207
+ return undefined;
208
+ }
209
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
210
+ const entry = entries[index];
211
+ if (isMentionForwardRequest(entry, botOpenId)) {
212
+ // Keep mention-forward semantics scoped to a single source message.
213
+ return mergeFeishuDebounceMentions([entry]);
214
+ }
215
+ }
216
+ const merged = mergeFeishuDebounceMentions(entries);
217
+ if (!merged) {
218
+ return undefined;
219
+ }
220
+ const normalizedBotOpenId = botOpenId?.trim();
221
+ if (!normalizedBotOpenId) {
222
+ return undefined;
223
+ }
224
+ const botMentions = merged.filter(
225
+ (mention) => mention.id.open_id?.trim() === normalizedBotOpenId,
226
+ );
227
+ return botMentions.length > 0 ? botMentions : undefined;
228
+ }
229
+
230
+ function registerEventHandlers(
231
+ eventDispatcher: Lark.EventDispatcher,
232
+ context: RegisterEventHandlersContext,
233
+ ): void {
234
+ const { cfg, accountId, runtime, chatHistories, fireAndForget } = context;
235
+ const core = getFeishuRuntime();
236
+ const inboundDebounceMs = core.channel.debounce.resolveInboundDebounceMs({
237
+ cfg,
238
+ channel: "feishu",
239
+ });
240
+ const log = runtime?.log ?? console.log;
241
+ const error = runtime?.error ?? console.error;
242
+ const enqueue = createChatQueue();
243
+ const dispatchFeishuMessage = async (event: FeishuMessageEvent) => {
244
+ const chatId = event.message.chat_id?.trim() || "unknown";
245
+ const task = () =>
246
+ handleFeishuMessage({
247
+ cfg,
248
+ event,
249
+ botOpenId: botOpenIds.get(accountId),
250
+ runtime,
251
+ chatHistories,
252
+ accountId,
253
+ });
254
+ await enqueue(chatId, task);
255
+ };
256
+ const resolveSenderDebounceId = (event: FeishuMessageEvent): string | undefined => {
257
+ const senderId =
258
+ event.sender.sender_id.open_id?.trim() || event.sender.sender_id.user_id?.trim();
259
+ return senderId || undefined;
260
+ };
261
+ const resolveDebounceText = (event: FeishuMessageEvent): string => {
262
+ const botOpenId = botOpenIds.get(accountId);
263
+ const parsed = parseFeishuMessageEvent(event, botOpenId);
264
+ return parsed.content.trim();
265
+ };
266
+ const recordSuppressedMessageIds = async (
267
+ entries: FeishuMessageEvent[],
268
+ dispatchMessageId?: string,
269
+ ) => {
270
+ const keepMessageId = dispatchMessageId?.trim();
271
+ const suppressedIds = new Set(
272
+ entries
273
+ .map((entry) => entry.message.message_id?.trim())
274
+ .filter((id): id is string => Boolean(id) && (!keepMessageId || id !== keepMessageId)),
275
+ );
276
+ if (suppressedIds.size === 0) {
277
+ return;
278
+ }
279
+ for (const messageId of suppressedIds) {
280
+ // Keep in-memory dedupe in sync with handleFeishuMessage's keying.
281
+ tryRecordMessage(`${accountId}:${messageId}`);
282
+ try {
283
+ await tryRecordMessagePersistent(messageId, accountId, log);
284
+ } catch (err) {
285
+ error(
286
+ `feishu[${accountId}]: failed to record merged dedupe id ${messageId}: ${String(err)}`,
287
+ );
288
+ }
289
+ }
290
+ };
291
+ const isMessageAlreadyProcessed = async (entry: FeishuMessageEvent): Promise<boolean> => {
292
+ const messageId = entry.message.message_id?.trim();
293
+ if (!messageId) {
294
+ return false;
295
+ }
296
+ const memoryKey = `${accountId}:${messageId}`;
297
+ if (hasRecordedMessage(memoryKey)) {
298
+ return true;
299
+ }
300
+ return hasRecordedMessagePersistent(messageId, accountId, log);
301
+ };
302
+ const inboundDebouncer = core.channel.debounce.createInboundDebouncer<FeishuMessageEvent>({
303
+ debounceMs: inboundDebounceMs,
304
+ buildKey: (event) => {
305
+ const chatId = event.message.chat_id?.trim();
306
+ const senderId = resolveSenderDebounceId(event);
307
+ if (!chatId || !senderId) {
308
+ return null;
309
+ }
310
+ const rootId = event.message.root_id?.trim();
311
+ const threadKey = rootId ? `thread:${rootId}` : "chat";
312
+ return `feishu:${accountId}:${chatId}:${threadKey}:${senderId}`;
313
+ },
314
+ shouldDebounce: (event) => {
315
+ if (event.message.message_type !== "text") {
316
+ return false;
317
+ }
318
+ const text = resolveDebounceText(event);
319
+ if (!text) {
320
+ return false;
321
+ }
322
+ return !core.channel.text.hasControlCommand(text, cfg);
323
+ },
324
+ onFlush: async (entries) => {
325
+ const last = entries.at(-1);
326
+ if (!last) {
327
+ return;
328
+ }
329
+ if (entries.length === 1) {
330
+ await dispatchFeishuMessage(last);
331
+ return;
332
+ }
333
+ const dedupedEntries = dedupeFeishuDebounceEntriesByMessageId(entries);
334
+ const freshEntries: FeishuMessageEvent[] = [];
335
+ for (const entry of dedupedEntries) {
336
+ if (!(await isMessageAlreadyProcessed(entry))) {
337
+ freshEntries.push(entry);
338
+ }
339
+ }
340
+ const dispatchEntry = freshEntries.at(-1);
341
+ if (!dispatchEntry) {
342
+ return;
343
+ }
344
+ await recordSuppressedMessageIds(dedupedEntries, dispatchEntry.message.message_id);
345
+ const combinedText = freshEntries
346
+ .map((entry) => resolveDebounceText(entry))
347
+ .filter(Boolean)
348
+ .join("\n");
349
+ const mergedMentions = resolveFeishuDebounceMentions({
350
+ entries: freshEntries,
351
+ botOpenId: botOpenIds.get(accountId),
352
+ });
353
+ if (!combinedText.trim()) {
354
+ await dispatchFeishuMessage({
355
+ ...dispatchEntry,
356
+ message: {
357
+ ...dispatchEntry.message,
358
+ mentions: mergedMentions ?? dispatchEntry.message.mentions,
359
+ },
360
+ });
361
+ return;
362
+ }
363
+ await dispatchFeishuMessage({
364
+ ...dispatchEntry,
365
+ message: {
366
+ ...dispatchEntry.message,
367
+ message_type: "text",
368
+ content: JSON.stringify({ text: combinedText }),
369
+ mentions: mergedMentions ?? dispatchEntry.message.mentions,
370
+ },
371
+ });
372
+ },
373
+ onError: (err) => {
374
+ error(`feishu[${accountId}]: inbound debounce flush failed: ${String(err)}`);
375
+ },
376
+ });
377
+
378
+ eventDispatcher.register({
379
+ "im.message.receive_v1": async (data) => {
380
+ const processMessage = async () => {
381
+ const event = data as unknown as FeishuMessageEvent;
382
+ await inboundDebouncer.enqueue(event);
383
+ };
384
+ if (fireAndForget) {
385
+ void processMessage().catch((err) => {
386
+ error(`feishu[${accountId}]: error handling message: ${String(err)}`);
387
+ });
388
+ return;
389
+ }
390
+ try {
391
+ await processMessage();
392
+ } catch (err) {
393
+ error(`feishu[${accountId}]: error handling message: ${String(err)}`);
394
+ }
395
+ },
396
+ "im.message.message_read_v1": async () => {
397
+ // Ignore read receipts
398
+ },
399
+ "im.chat.member.bot.added_v1": async (data) => {
400
+ try {
401
+ const event = data as unknown as FeishuBotAddedEvent;
402
+ log(`feishu[${accountId}]: bot added to chat ${event.chat_id}`);
403
+ } catch (err) {
404
+ error(`feishu[${accountId}]: error handling bot added event: ${String(err)}`);
405
+ }
406
+ },
407
+ "im.chat.member.bot.deleted_v1": async (data) => {
408
+ try {
409
+ const event = data as unknown as { chat_id: string };
410
+ log(`feishu[${accountId}]: bot removed from chat ${event.chat_id}`);
411
+ } catch (err) {
412
+ error(`feishu[${accountId}]: error handling bot removed event: ${String(err)}`);
413
+ }
414
+ },
415
+ "im.message.reaction.created_v1": async (data) => {
416
+ const processReaction = async () => {
417
+ const event = data as FeishuReactionCreatedEvent;
418
+ const myBotId = botOpenIds.get(accountId);
419
+ const syntheticEvent = await resolveReactionSyntheticEvent({
420
+ cfg,
421
+ accountId,
422
+ event,
423
+ botOpenId: myBotId,
424
+ logger: log,
425
+ });
426
+ if (!syntheticEvent) {
427
+ return;
428
+ }
429
+ const promise = handleFeishuMessage({
430
+ cfg,
431
+ event: syntheticEvent,
432
+ botOpenId: myBotId,
433
+ runtime,
434
+ chatHistories,
435
+ accountId,
436
+ });
437
+ if (fireAndForget) {
438
+ promise.catch((err) => {
439
+ error(`feishu[${accountId}]: error handling reaction: ${String(err)}`);
440
+ });
441
+ return;
442
+ }
443
+ await promise;
444
+ };
445
+
446
+ if (fireAndForget) {
447
+ void processReaction().catch((err) => {
448
+ error(`feishu[${accountId}]: error handling reaction event: ${String(err)}`);
449
+ });
450
+ return;
451
+ }
452
+
453
+ try {
454
+ await processReaction();
455
+ } catch (err) {
456
+ error(`feishu[${accountId}]: error handling reaction event: ${String(err)}`);
457
+ }
458
+ },
459
+ "im.message.reaction.deleted_v1": async () => {
460
+ // Ignore reaction removals
461
+ },
462
+ "card.action.trigger": async (data: unknown) => {
463
+ try {
464
+ const event = data as unknown as FeishuCardActionEvent;
465
+ const promise = handleFeishuCardAction({
466
+ cfg,
467
+ event,
468
+ botOpenId: botOpenIds.get(accountId),
469
+ runtime,
470
+ accountId,
471
+ });
472
+ if (fireAndForget) {
473
+ promise.catch((err) => {
474
+ error(`feishu[${accountId}]: error handling card action: ${String(err)}`);
475
+ });
476
+ } else {
477
+ await promise;
478
+ }
479
+ } catch (err) {
480
+ error(`feishu[${accountId}]: error handling card action: ${String(err)}`);
481
+ }
482
+ },
483
+ });
484
+ }
485
+
486
+ export type BotOpenIdSource = { kind: "prefetched"; botOpenId?: string } | { kind: "fetch" };
487
+
488
+ export type MonitorSingleAccountParams = {
489
+ cfg: ClawdbotConfig;
490
+ account: ResolvedFeishuAccount;
491
+ runtime?: RuntimeEnv;
492
+ abortSignal?: AbortSignal;
493
+ botOpenIdSource?: BotOpenIdSource;
494
+ };
495
+
496
+ export async function monitorSingleAccount(params: MonitorSingleAccountParams): Promise<void> {
497
+ const { cfg, account, runtime, abortSignal } = params;
498
+ const { accountId } = account;
499
+ const log = runtime?.log ?? console.log;
500
+
501
+ const botOpenIdSource = params.botOpenIdSource ?? { kind: "fetch" };
502
+ const botOpenId =
503
+ botOpenIdSource.kind === "prefetched"
504
+ ? botOpenIdSource.botOpenId
505
+ : await fetchBotOpenIdForMonitor(account, { runtime, abortSignal });
506
+ botOpenIds.set(accountId, botOpenId ?? "");
507
+ log(`feishu[${accountId}]: bot open_id resolved: ${botOpenId ?? "unknown"}`);
508
+
509
+ const connectionMode = account.config.connectionMode ?? "websocket";
510
+ if (connectionMode === "webhook" && !account.verificationToken?.trim()) {
511
+ throw new Error(`Feishu account "${accountId}" webhook mode requires verificationToken`);
512
+ }
513
+
514
+ const warmupCount = await warmupDedupFromDisk(accountId, log);
515
+ if (warmupCount > 0) {
516
+ log(`feishu[${accountId}]: dedup warmup loaded ${warmupCount} entries from disk`);
517
+ }
518
+
519
+ const eventDispatcher = createEventDispatcher(account);
520
+ const chatHistories = new Map<string, HistoryEntry[]>();
521
+
522
+ registerEventHandlers(eventDispatcher, {
523
+ cfg,
524
+ accountId,
525
+ runtime,
526
+ chatHistories,
527
+ fireAndForget: true,
528
+ });
529
+
530
+ if (connectionMode === "webhook") {
531
+ return monitorWebhook({ account, accountId, runtime, abortSignal, eventDispatcher });
532
+ }
533
+ return monitorWebSocket({ account, accountId, runtime, abortSignal, eventDispatcher });
534
+ }