openclaw-mochat 2026.2.3

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.
@@ -0,0 +1,46 @@
1
+ import { z } from "zod";
2
+
3
+ export const MochatGroupSchema = z
4
+ .object({
5
+ requireMention: z.boolean().optional(),
6
+ })
7
+ .strict();
8
+
9
+ export const MochatMentionSchema = z
10
+ .object({
11
+ requireInGroups: z.boolean().optional(),
12
+ })
13
+ .strict();
14
+
15
+ const MochatConfigSchemaBase = z
16
+ .object({
17
+ name: z.string().optional(),
18
+ enabled: z.boolean().optional(),
19
+ baseUrl: z.string().optional(),
20
+ clawToken: z.string().optional(),
21
+ agentUserId: z.string().optional(),
22
+ sessions: z.array(z.string()).optional(),
23
+ panels: z.array(z.string()).optional(),
24
+ mention: MochatMentionSchema.optional(),
25
+ groups: z.record(z.string(), MochatGroupSchema.optional()).optional(),
26
+ socketUrl: z.string().optional(),
27
+ socketPath: z.string().optional(),
28
+ socketDisableMsgpack: z.boolean().optional(),
29
+ socketReconnectDelayMs: z.number().int().min(0).optional(),
30
+ socketMaxReconnectDelayMs: z.number().int().min(0).optional(),
31
+ socketConnectTimeoutMs: z.number().int().min(0).optional(),
32
+ refreshIntervalMs: z.number().int().min(1000).optional(),
33
+ watchTimeoutMs: z.number().int().positive().optional(),
34
+ watchLimit: z.number().int().positive().optional(),
35
+ retryDelayMs: z.number().int().min(0).optional(),
36
+ maxRetryAttempts: z.number().int().min(0).optional(),
37
+ replyDelayMode: z.enum(["off", "non-mention"]).optional(),
38
+ replyDelayMs: z.number().int().min(0).optional(),
39
+ })
40
+ .strict();
41
+
42
+ export const MochatConfigSchema = MochatConfigSchemaBase.extend({
43
+ accounts: z.record(z.string(), MochatConfigSchemaBase.optional()).optional(),
44
+ }).strict();
45
+
46
+ export type MochatConfig = z.infer<typeof MochatConfigSchema>;
@@ -0,0 +1,81 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import {
3
+ clearDelayedEntries,
4
+ enqueueDelayedEntry,
5
+ flushDelayedEntries,
6
+ type MochatBufferedEntry,
7
+ } from "./delay-buffer.js";
8
+
9
+ const makeEntry = (rawBody: string, author = "user"): MochatBufferedEntry => ({
10
+ rawBody,
11
+ author,
12
+ });
13
+
14
+ describe("mochat delay buffer", () => {
15
+ beforeEach(() => {
16
+ vi.useFakeTimers();
17
+ });
18
+
19
+ afterEach(() => {
20
+ vi.useRealTimers();
21
+ });
22
+
23
+ it("flushes after delay and merges entries", async () => {
24
+ const key = "session:one";
25
+ const onFlush = vi.fn(async () => {});
26
+
27
+ await enqueueDelayedEntry({
28
+ key,
29
+ entry: makeEntry("first"),
30
+ delayMs: 1000,
31
+ onFlush,
32
+ });
33
+
34
+ await vi.advanceTimersByTimeAsync(500);
35
+
36
+ await enqueueDelayedEntry({
37
+ key,
38
+ entry: makeEntry("second", "user2"),
39
+ delayMs: 1000,
40
+ onFlush,
41
+ });
42
+
43
+ await vi.advanceTimersByTimeAsync(999);
44
+ expect(onFlush).not.toHaveBeenCalled();
45
+
46
+ await vi.advanceTimersByTimeAsync(1);
47
+ expect(onFlush).toHaveBeenCalledTimes(1);
48
+ const entries = onFlush.mock.calls[0]?.[0] as MochatBufferedEntry[];
49
+ expect(entries.map((entry) => entry.rawBody)).toEqual(["first", "second"]);
50
+
51
+ clearDelayedEntries(key);
52
+ });
53
+
54
+ it("flushes immediately on mention and includes buffered entries", async () => {
55
+ const key = "session:two";
56
+ const onFlush = vi.fn(async () => {});
57
+
58
+ await enqueueDelayedEntry({
59
+ key,
60
+ entry: makeEntry("buffered"),
61
+ delayMs: 1000,
62
+ onFlush,
63
+ });
64
+
65
+ await flushDelayedEntries({
66
+ key,
67
+ entry: makeEntry("@bot", "user3"),
68
+ reason: "mention",
69
+ onFlush,
70
+ });
71
+
72
+ expect(onFlush).toHaveBeenCalledTimes(1);
73
+ const entries = onFlush.mock.calls[0]?.[0] as MochatBufferedEntry[];
74
+ expect(entries.map((entry) => entry.rawBody)).toEqual(["buffered", "@bot"]);
75
+
76
+ await vi.advanceTimersByTimeAsync(1000);
77
+ expect(onFlush).toHaveBeenCalledTimes(1);
78
+
79
+ clearDelayedEntries(key);
80
+ });
81
+ });
@@ -0,0 +1,123 @@
1
+ export type MochatBufferedEntry = {
2
+ rawBody: string;
3
+ author: string;
4
+ senderName?: string;
5
+ senderUsername?: string;
6
+ timestamp?: number;
7
+ messageId?: string;
8
+ groupId?: string;
9
+ };
10
+
11
+ export type MochatDelayFlushReason = "mention" | "timer";
12
+
13
+ export type MochatDelayFlushHandler = (
14
+ entries: MochatBufferedEntry[],
15
+ reason: MochatDelayFlushReason,
16
+ ) => Promise<void>;
17
+
18
+ type BufferState = {
19
+ entries: MochatBufferedEntry[];
20
+ timer: NodeJS.Timeout | null;
21
+ queue: Promise<void>;
22
+ onFlush?: MochatDelayFlushHandler;
23
+ };
24
+
25
+ const buffers = new Map<string, BufferState>();
26
+
27
+ function getState(key: string): BufferState {
28
+ const existing = buffers.get(key);
29
+ if (existing) {
30
+ return existing;
31
+ }
32
+ const created: BufferState = {
33
+ entries: [],
34
+ timer: null,
35
+ queue: Promise.resolve(),
36
+ };
37
+ buffers.set(key, created);
38
+ return created;
39
+ }
40
+
41
+ function enqueueTask(key: string, task: () => Promise<void>) {
42
+ const state = getState(key);
43
+ const previous = state.queue;
44
+ const next = previous.then(task, task);
45
+ state.queue = next.catch(() => undefined);
46
+ return next;
47
+ }
48
+
49
+ function clearTimer(state: BufferState) {
50
+ if (state.timer) {
51
+ clearTimeout(state.timer);
52
+ state.timer = null;
53
+ }
54
+ }
55
+
56
+ async function flushInternal(
57
+ key: string,
58
+ reason: MochatDelayFlushReason,
59
+ onFlush?: MochatDelayFlushHandler,
60
+ ) {
61
+ const state = buffers.get(key);
62
+ if (!state) {
63
+ return;
64
+ }
65
+ clearTimer(state);
66
+ if (onFlush) {
67
+ state.onFlush = onFlush;
68
+ }
69
+ const entries = state.entries.slice();
70
+ state.entries.length = 0;
71
+ if (entries.length === 0) {
72
+ return;
73
+ }
74
+ const handler = state.onFlush;
75
+ if (!handler) {
76
+ return;
77
+ }
78
+ await handler(entries, reason);
79
+ }
80
+
81
+ export async function enqueueDelayedEntry(params: {
82
+ key: string;
83
+ entry: MochatBufferedEntry;
84
+ delayMs: number;
85
+ onFlush: MochatDelayFlushHandler;
86
+ }) {
87
+ const { key, entry, delayMs, onFlush } = params;
88
+ await enqueueTask(key, async () => {
89
+ const state = getState(key);
90
+ state.onFlush = onFlush;
91
+ state.entries.push(entry);
92
+ clearTimer(state);
93
+ state.timer = setTimeout(() => {
94
+ void enqueueTask(key, () => flushInternal(key, "timer"));
95
+ }, Math.max(0, delayMs));
96
+ });
97
+ }
98
+
99
+ export async function flushDelayedEntries(params: {
100
+ key: string;
101
+ entry?: MochatBufferedEntry;
102
+ reason: MochatDelayFlushReason;
103
+ onFlush: MochatDelayFlushHandler;
104
+ }) {
105
+ const { key, entry, reason, onFlush } = params;
106
+ await enqueueTask(key, async () => {
107
+ const state = getState(key);
108
+ state.onFlush = onFlush;
109
+ if (entry) {
110
+ state.entries.push(entry);
111
+ }
112
+ await flushInternal(key, reason, onFlush);
113
+ });
114
+ }
115
+
116
+ export function clearDelayedEntries(key: string) {
117
+ const state = buffers.get(key);
118
+ if (!state) {
119
+ return;
120
+ }
121
+ clearTimer(state);
122
+ state.entries.length = 0;
123
+ }
@@ -0,0 +1,56 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { getMochatRuntime } from "./runtime.js";
4
+
5
+ type PanelEventRecord = {
6
+ ts: string;
7
+ accountId: string;
8
+ eventName: string;
9
+ payload: unknown;
10
+ };
11
+
12
+ const queues = new Map<string, Promise<void>>();
13
+
14
+ function resolveEventFilePath(timestamp: Date): string {
15
+ const runtime = getMochatRuntime();
16
+ const stateDir = runtime.state.resolveStateDir();
17
+ const dateKey = timestamp.toISOString().slice(0, 10);
18
+ return path.join(stateDir, "mochat", "events", `${dateKey}.jsonl`);
19
+ }
20
+
21
+ function safeStringify(value: unknown): string {
22
+ try {
23
+ return JSON.stringify(value, (_key, val) => (typeof val === "bigint" ? val.toString() : val));
24
+ } catch {
25
+ return JSON.stringify({ value: String(value), note: "non-serializable" });
26
+ }
27
+ }
28
+
29
+ async function appendLine(filePath: string, line: string) {
30
+ const previous = queues.get(filePath) ?? Promise.resolve();
31
+ const next = previous
32
+ .then(async () => {
33
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
34
+ await fs.appendFile(filePath, line, "utf8");
35
+ })
36
+ .catch(() => undefined);
37
+ queues.set(filePath, next);
38
+ await next;
39
+ }
40
+
41
+ export async function recordPanelEvent(params: {
42
+ accountId: string;
43
+ eventName: string;
44
+ payload: unknown;
45
+ }) {
46
+ const timestamp = new Date();
47
+ const filePath = resolveEventFilePath(timestamp);
48
+ const record: PanelEventRecord = {
49
+ ts: timestamp.toISOString(),
50
+ accountId: params.accountId,
51
+ eventName: params.eventName,
52
+ payload: params.payload,
53
+ };
54
+ const line = `${safeStringify(record)}\n`;
55
+ await appendLine(filePath, line);
56
+ }
package/src/inbound.ts ADDED
@@ -0,0 +1,402 @@
1
+ import type { ChannelLogSink, OpenClawConfig } from "openclaw/plugin-sdk";
2
+ import { sendPanelMessage, sendSessionMessage, type MochatEvent } from "./api.js";
3
+ import type { ResolvedMochatAccount } from "./accounts.js";
4
+ import {
5
+ enqueueDelayedEntry,
6
+ flushDelayedEntries,
7
+ type MochatBufferedEntry,
8
+ } from "./delay-buffer.js";
9
+ import { getMochatRuntime } from "./runtime.js";
10
+
11
+ export type MochatStatusSink = (patch: {
12
+ lastInboundAt?: number;
13
+ lastOutboundAt?: number;
14
+ lastError?: string | null;
15
+ }) => void;
16
+
17
+ function normalizeContent(content: unknown): string {
18
+ if (typeof content === "string") {
19
+ return content;
20
+ }
21
+ if (content === null || content === undefined) {
22
+ return "";
23
+ }
24
+ try {
25
+ return JSON.stringify(content);
26
+ } catch {
27
+ return String(content);
28
+ }
29
+ }
30
+
31
+ function parseTimestamp(value?: string): number | undefined {
32
+ if (!value) {
33
+ return undefined;
34
+ }
35
+ const parsed = Date.parse(value);
36
+ return Number.isFinite(parsed) ? parsed : undefined;
37
+ }
38
+
39
+ function extractMentionIds(value: unknown): string[] {
40
+ if (!Array.isArray(value)) {
41
+ return [];
42
+ }
43
+ const ids: string[] = [];
44
+ for (const entry of value) {
45
+ if (typeof entry === "string" && entry.trim()) {
46
+ ids.push(entry.trim());
47
+ continue;
48
+ }
49
+ if (entry && typeof entry === "object") {
50
+ const obj = entry as Record<string, unknown>;
51
+ const candidate =
52
+ (typeof obj.id === "string" ? obj.id : undefined) ??
53
+ (typeof obj.userId === "string" ? obj.userId : undefined) ??
54
+ (typeof obj._id === "string" ? obj._id : undefined);
55
+ if (candidate) {
56
+ ids.push(candidate);
57
+ }
58
+ }
59
+ }
60
+ return ids;
61
+ }
62
+
63
+ function resolveWasMentioned(payload: MochatEvent["payload"], agentUserId?: string): boolean {
64
+ const meta = payload?.meta as Record<string, unknown> | undefined;
65
+ if (meta) {
66
+ const directBool =
67
+ (typeof meta.mentioned === "boolean" && meta.mentioned) ||
68
+ (typeof meta.wasMentioned === "boolean" && meta.wasMentioned);
69
+ if (directBool) {
70
+ return true;
71
+ }
72
+
73
+ const mentionSources = [
74
+ meta.mentions,
75
+ meta.mentionIds,
76
+ meta.mentionedUserIds,
77
+ meta.mentionedUsers,
78
+ ];
79
+ for (const source of mentionSources) {
80
+ const ids = extractMentionIds(source);
81
+ if (agentUserId && ids.includes(agentUserId)) {
82
+ return true;
83
+ }
84
+ }
85
+ }
86
+
87
+ if (!agentUserId) {
88
+ return false;
89
+ }
90
+ const content = typeof payload?.content === "string" ? payload.content : "";
91
+ if (!content) {
92
+ return false;
93
+ }
94
+ return content.includes(`<@${agentUserId}>`) || content.includes(`@${agentUserId}`);
95
+ }
96
+
97
+ function resolveRequireMention(params: {
98
+ account: ResolvedMochatAccount;
99
+ sessionId: string;
100
+ groupId?: string;
101
+ }): boolean {
102
+ const { account, sessionId, groupId } = params;
103
+ const groups = account.config.groups;
104
+ if (groups) {
105
+ if (groupId && typeof groups[groupId]?.requireMention === "boolean") {
106
+ return groups[groupId]?.requireMention ?? false;
107
+ }
108
+ const direct = groups[sessionId]?.requireMention;
109
+ if (typeof direct === "boolean") {
110
+ return direct;
111
+ }
112
+ const wildcard = groups["*"]?.requireMention;
113
+ if (typeof wildcard === "boolean") {
114
+ return wildcard;
115
+ }
116
+ }
117
+ return Boolean(account.config.mention?.requireInGroups);
118
+ }
119
+
120
+ function resolveSenderLabel(entry: MochatBufferedEntry): string {
121
+ return (
122
+ (entry.senderName && entry.senderName.trim()) ||
123
+ (entry.senderUsername && entry.senderUsername.trim()) ||
124
+ entry.author
125
+ );
126
+ }
127
+
128
+ function buildBufferedBody(entries: MochatBufferedEntry[], isGroup: boolean): string {
129
+ if (entries.length === 1) {
130
+ return entries[0]?.rawBody ?? "";
131
+ }
132
+ const lines: string[] = [];
133
+ for (const entry of entries) {
134
+ const body = entry.rawBody;
135
+ if (!body) {
136
+ continue;
137
+ }
138
+ if (isGroup) {
139
+ const label = resolveSenderLabel(entry);
140
+ if (label) {
141
+ lines.push(`${label}: ${body}`);
142
+ continue;
143
+ }
144
+ }
145
+ lines.push(body);
146
+ }
147
+ return lines.join("\n").trim();
148
+ }
149
+
150
+ async function dispatchBufferedEntries(params: {
151
+ account: ResolvedMochatAccount;
152
+ sessionId: string;
153
+ targetKind: "session" | "panel";
154
+ entries: MochatBufferedEntry[];
155
+ isGroup: boolean;
156
+ wasMentioned: boolean;
157
+ log?: ChannelLogSink;
158
+ statusSink?: MochatStatusSink;
159
+ markInboundAt?: boolean;
160
+ }) {
161
+ const {
162
+ account,
163
+ sessionId,
164
+ targetKind,
165
+ entries,
166
+ isGroup,
167
+ wasMentioned,
168
+ log,
169
+ statusSink,
170
+ } = params;
171
+ if (entries.length === 0) {
172
+ return;
173
+ }
174
+ const rawBody = buildBufferedBody(entries, isGroup);
175
+ const lastEntry = entries[entries.length - 1];
176
+
177
+ const core = getMochatRuntime();
178
+ const config = core.config.loadConfig() as OpenClawConfig;
179
+
180
+ const route = core.channel.routing.resolveAgentRoute({
181
+ cfg: config,
182
+ channel: "mochat",
183
+ accountId: account.accountId,
184
+ peer: {
185
+ kind: isGroup ? "group" : "dm",
186
+ id: sessionId,
187
+ },
188
+ });
189
+
190
+ const fromLabel = isGroup
191
+ ? `group:${lastEntry.groupId ?? sessionId}`
192
+ : `user:${lastEntry.author}`;
193
+ const storePath = core.channel.session.resolveStorePath(config.session?.store, {
194
+ agentId: route.agentId,
195
+ });
196
+ const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(config);
197
+ const previousTimestamp = core.channel.session.readSessionUpdatedAt({
198
+ storePath,
199
+ sessionKey: route.sessionKey,
200
+ });
201
+ const body = core.channel.reply.formatAgentEnvelope({
202
+ channel: "Mochat",
203
+ from: fromLabel,
204
+ timestamp: lastEntry.timestamp,
205
+ previousTimestamp,
206
+ envelope: envelopeOptions,
207
+ body: rawBody,
208
+ });
209
+
210
+ const ctxPayload = core.channel.reply.finalizeInboundContext({
211
+ Body: body,
212
+ RawBody: rawBody,
213
+ CommandBody: rawBody,
214
+ From: `mochat:${lastEntry.author}`,
215
+ To: `mochat:${sessionId}`,
216
+ SessionKey: route.sessionKey,
217
+ AccountId: route.accountId,
218
+ ChatType: isGroup ? "group" : "direct",
219
+ ConversationLabel: fromLabel,
220
+ SenderName: lastEntry.senderName || undefined,
221
+ SenderUsername: lastEntry.senderUsername || undefined,
222
+ SenderId: lastEntry.author,
223
+ WasMentioned: isGroup ? wasMentioned : undefined,
224
+ MessageSid: lastEntry.messageId,
225
+ Timestamp: lastEntry.timestamp,
226
+ GroupSubject: isGroup ? String(lastEntry.groupId ?? sessionId) : undefined,
227
+ Provider: "mochat",
228
+ Surface: "mochat",
229
+ OriginatingChannel: "mochat",
230
+ OriginatingTo: `mochat:${sessionId}`,
231
+ });
232
+
233
+ await core.channel.session.recordInboundSession({
234
+ storePath,
235
+ sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
236
+ ctx: ctxPayload,
237
+ onRecordError: (err) => {
238
+ log?.error?.(`mochat: failed updating session meta: ${String(err)}`);
239
+ },
240
+ });
241
+
242
+ if (params.markInboundAt !== false) {
243
+ statusSink?.({ lastInboundAt: Date.now(), lastError: null });
244
+ }
245
+
246
+ await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
247
+ ctx: ctxPayload,
248
+ cfg: config,
249
+ dispatcherOptions: {
250
+ deliver: async (payload: {
251
+ text?: string;
252
+ mediaUrls?: string[];
253
+ mediaUrl?: string;
254
+ replyToId?: string | null;
255
+ }) => {
256
+ const contentParts: string[] = [];
257
+ if (payload.text) {
258
+ contentParts.push(payload.text);
259
+ }
260
+ const mediaUrls = [
261
+ ...(payload.mediaUrls ?? []),
262
+ ...(payload.mediaUrl ? [payload.mediaUrl] : []),
263
+ ].filter(Boolean);
264
+ if (mediaUrls.length > 0) {
265
+ contentParts.push(...mediaUrls);
266
+ }
267
+ const content = contentParts.join("\n").trim();
268
+ if (!content) {
269
+ return;
270
+ }
271
+ if (targetKind === "panel") {
272
+ await sendPanelMessage({
273
+ baseUrl: account.config.baseUrl,
274
+ clawToken: account.config.clawToken ?? "",
275
+ panelId: sessionId,
276
+ content,
277
+ replyTo: payload.replyToId ?? null,
278
+ });
279
+ } else {
280
+ await sendSessionMessage({
281
+ baseUrl: account.config.baseUrl,
282
+ clawToken: account.config.clawToken ?? "",
283
+ sessionId,
284
+ content,
285
+ replyTo: payload.replyToId ?? null,
286
+ });
287
+ }
288
+ statusSink?.({ lastOutboundAt: Date.now(), lastError: null });
289
+ },
290
+ onError: (err, info) => {
291
+ log?.error?.(`mochat ${info.kind} reply failed: ${String(err)}`);
292
+ },
293
+ },
294
+ });
295
+ }
296
+
297
+ export async function handleInboundMessage(params: {
298
+ account: ResolvedMochatAccount;
299
+ sessionId: string;
300
+ event: MochatEvent;
301
+ targetKind?: "session" | "panel";
302
+ log?: ChannelLogSink;
303
+ statusSink?: MochatStatusSink;
304
+ }) {
305
+ const { account, sessionId, event, log, statusSink } = params;
306
+ const targetKind = params.targetKind ?? "session";
307
+ const payload = event.payload;
308
+ if (!payload) {
309
+ return;
310
+ }
311
+ const author = payload.author ? String(payload.author) : "";
312
+ const authorInfo =
313
+ payload.authorInfo && typeof payload.authorInfo === "object"
314
+ ? (payload.authorInfo as {
315
+ nickname?: string | null;
316
+ email?: string | null;
317
+ agentId?: string | null;
318
+ })
319
+ : null;
320
+ const senderName =
321
+ (authorInfo?.nickname && authorInfo.nickname.trim()) ||
322
+ (authorInfo?.email && authorInfo.email.trim()) ||
323
+ "";
324
+ if (!author) {
325
+ return;
326
+ }
327
+ const agentUserId = account.config.agentUserId;
328
+ if (agentUserId && author === agentUserId) {
329
+ return;
330
+ }
331
+
332
+ const isGroup = Boolean(payload.groupId);
333
+ const wasMentioned = resolveWasMentioned(payload, agentUserId);
334
+ const requireMention =
335
+ targetKind === "panel" &&
336
+ isGroup &&
337
+ resolveRequireMention({ account, sessionId, groupId: String(payload.groupId ?? "") });
338
+ const replyDelayMode = account.config.replyDelayMode;
339
+ const useDelay = targetKind === "panel" && replyDelayMode === "non-mention";
340
+ if (requireMention && !wasMentioned && !useDelay) {
341
+ return;
342
+ }
343
+
344
+ const rawBody = normalizeContent(payload.content);
345
+ const timestamp = parseTimestamp(event.timestamp);
346
+ const entry: MochatBufferedEntry = {
347
+ rawBody,
348
+ author,
349
+ senderName: senderName || undefined,
350
+ senderUsername: authorInfo?.agentId || undefined,
351
+ timestamp,
352
+ messageId: payload.messageId ? String(payload.messageId) : undefined,
353
+ groupId: isGroup ? String(payload.groupId ?? sessionId) : undefined,
354
+ };
355
+
356
+ if (useDelay) {
357
+ statusSink?.({ lastInboundAt: Date.now(), lastError: null });
358
+ const delayKey = `${account.accountId}:${targetKind}:${sessionId}`;
359
+ const delayMs = account.config.replyDelayMs;
360
+ const onFlush = async (entries: MochatBufferedEntry[], reason: "mention" | "timer") => {
361
+ await dispatchBufferedEntries({
362
+ account,
363
+ sessionId,
364
+ targetKind,
365
+ entries,
366
+ isGroup,
367
+ wasMentioned: reason === "mention",
368
+ log,
369
+ statusSink,
370
+ markInboundAt: false,
371
+ });
372
+ };
373
+
374
+ if (wasMentioned) {
375
+ await flushDelayedEntries({
376
+ key: delayKey,
377
+ entry,
378
+ reason: "mention",
379
+ onFlush,
380
+ });
381
+ } else {
382
+ await enqueueDelayedEntry({
383
+ key: delayKey,
384
+ entry,
385
+ delayMs,
386
+ onFlush,
387
+ });
388
+ }
389
+ return;
390
+ }
391
+
392
+ await dispatchBufferedEntries({
393
+ account,
394
+ sessionId,
395
+ targetKind,
396
+ entries: [entry],
397
+ isGroup,
398
+ wasMentioned,
399
+ log,
400
+ statusSink,
401
+ });
402
+ }