cclawd 1.0.2 → 1.0.4

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 (68) hide show
  1. package/dist/build-info.json +3 -3
  2. package/dist/plugin-sdk/active-listener-CN-tMEvN.js +35 -0
  3. package/dist/plugin-sdk/api-key-rotation-CimGYMBc.js +176 -0
  4. package/dist/plugin-sdk/audio-preflight-C-xXBoE2.js +51 -0
  5. package/dist/plugin-sdk/audio-transcription-runner-CTIHpebA.js +2173 -0
  6. package/dist/plugin-sdk/audit-membership-runtime-BFatB2LJ.js +58 -0
  7. package/dist/plugin-sdk/channel-activity-DO0FEzyj.js +95 -0
  8. package/dist/plugin-sdk/channel-web-Da-__nUF.js +2238 -0
  9. package/dist/plugin-sdk/commands-registry-6no2NNrY.js +1118 -0
  10. package/dist/plugin-sdk/compact.runtime-CCoclu5e.js +35 -0
  11. package/dist/plugin-sdk/config-B9ODwgpz.js +37426 -0
  12. package/dist/plugin-sdk/deliver-B1fFpKjV.js +1757 -0
  13. package/dist/plugin-sdk/deliver-runtime-DB-VRMe1.js +15 -0
  14. package/dist/plugin-sdk/deps-send-discord.runtime-DklqycYG.js +15 -0
  15. package/dist/plugin-sdk/deps-send-imessage.runtime-Chs8zeon.js +14 -0
  16. package/dist/plugin-sdk/deps-send-signal.runtime-clW9aSJP.js +13 -0
  17. package/dist/plugin-sdk/deps-send-slack.runtime-BUx0LYY1.js +13 -0
  18. package/dist/plugin-sdk/deps-send-telegram.runtime-LECSHgMG.js +16 -0
  19. package/dist/plugin-sdk/deps-send-whatsapp.runtime-D2d65fw0.js +40 -0
  20. package/dist/plugin-sdk/diagnostic-CxIvS-C2.js +315 -0
  21. package/dist/plugin-sdk/dispatch-BqlR4dPx.js +105863 -0
  22. package/dist/plugin-sdk/env-b9k1PHMI.js +34 -0
  23. package/dist/plugin-sdk/fetch-PoxzAANT.js +326 -0
  24. package/dist/plugin-sdk/fetch-guard-4UVSZ0uS.js +164 -0
  25. package/dist/plugin-sdk/image-Ch6M4tnJ.js +2420 -0
  26. package/dist/plugin-sdk/image-runtime-CSh2o5wY.js +8 -0
  27. package/dist/plugin-sdk/index.js +35 -35
  28. package/dist/plugin-sdk/ir-CugsqGH8.js +1312 -0
  29. package/dist/plugin-sdk/local-roots-adnEg9zb.js +217 -0
  30. package/dist/plugin-sdk/logger-D6zRubj0.js +1164 -0
  31. package/dist/plugin-sdk/login-CYvkQ0At.js +54 -0
  32. package/dist/plugin-sdk/login-qr-ll4NtaT5.js +316 -0
  33. package/dist/plugin-sdk/manager-CHy8IclH.js +3959 -0
  34. package/dist/plugin-sdk/manager-runtime-C70EkEr7.js +11 -0
  35. package/dist/plugin-sdk/outbound-Wzs2iN7X.js +216 -0
  36. package/dist/plugin-sdk/outbound-attachment-khXJwucX.js +17 -0
  37. package/dist/plugin-sdk/paths-BtVqCdw4.js +3063 -0
  38. package/dist/plugin-sdk/pi-model-discovery-Dh4ziodY.js +131 -0
  39. package/dist/plugin-sdk/pi-model-discovery-runtime-b83Xe-HT.js +8 -0
  40. package/dist/plugin-sdk/pi-tools.before-tool-call.runtime-C1z5CDBF.js +349 -0
  41. package/dist/plugin-sdk/proxy-fetch-CJEmoBxi.js +54 -0
  42. package/dist/plugin-sdk/pw-ai-Dj3Cvlzl.js +1990 -0
  43. package/dist/plugin-sdk/qmd-manager-egHUAseQ.js +1581 -0
  44. package/dist/plugin-sdk/resolve-outbound-target-BiICvIKs.js +38 -0
  45. package/dist/plugin-sdk/runtime-whatsapp-login.runtime-DNApufzW.js +9 -0
  46. package/dist/plugin-sdk/runtime-whatsapp-outbound.runtime-CBmtfIQ8.js +13 -0
  47. package/dist/plugin-sdk/send-CScblaI4.js +532 -0
  48. package/dist/plugin-sdk/send-CeHhnld6.js +407 -0
  49. package/dist/plugin-sdk/send-DP_c8JfR.js +3277 -0
  50. package/dist/plugin-sdk/send-Dc5fI6e8.js +495 -0
  51. package/dist/plugin-sdk/send-l-77_s1_.js +2507 -0
  52. package/dist/plugin-sdk/session-CkOKZaqa.js +166 -0
  53. package/dist/plugin-sdk/signal.js +2 -2
  54. package/dist/plugin-sdk/skill-commands-BohYCgkq.js +336 -0
  55. package/dist/plugin-sdk/slash-commands.runtime-DpLfVTM6.js +8 -0
  56. package/dist/plugin-sdk/slash-dispatch.runtime-CASMHwpm.js +35 -0
  57. package/dist/plugin-sdk/slash-skill-commands.runtime-D7rrJEci.js +9 -0
  58. package/dist/plugin-sdk/sqlite-CJE3X7Mv.js +1005 -0
  59. package/dist/plugin-sdk/subagent-registry-runtime-B1oo5bih.js +35 -0
  60. package/dist/plugin-sdk/tables-D5VgpTmm.js +53 -0
  61. package/dist/plugin-sdk/target-errors-C6zZ_OpA.js +191 -0
  62. package/dist/plugin-sdk/tokens-DUnJnpMS.js +50 -0
  63. package/dist/plugin-sdk/web-TfUM1nSi.js +39 -0
  64. package/dist/plugin-sdk/whatsapp-actions-DuWJ0j1r.js +71 -0
  65. package/extensions/mfa-auth/index.ts +36 -17
  66. package/extensions/mfa-auth/src/auth-manager.ts +4 -0
  67. package/extensions/mfa-auth/src/notification-service.ts +5 -1
  68. package/package.json +1 -1
@@ -0,0 +1,1757 @@
1
+ import { It as normalizeAccountId, Lt as normalizeOptionalAccountId } from "./paths-BtVqCdw4.js";
2
+ import { $c as getActivePluginRegistry, Cr as resolveMirroredTranscriptText, In as isMessagingToolDuplicate, Pi as getChannelDock, Sr as appendAssistantMessageToSessionTranscript, Xr as parseInlineDirectives, fl as createInternalHookEvent, gi as getChannelPlugin, pl as triggerInternalHook, tl as getActivePluginRegistryVersion, ts as generateSecureUuid, vi as normalizeChannelId } from "./config-B9ODwgpz.js";
3
+ import { c as resolveStateDir } from "./paths-eFexkPEh.js";
4
+ import { R as logVerbose, a as createSubsystemLogger } from "./logger-D6zRubj0.js";
5
+ import { t as getAgentScopedMediaLocalRoots } from "./local-roots-adnEg9zb.js";
6
+ import { c as chunkMarkdownTextWithMode, d as resolveChunkMode, f as resolveTextChunkLimit, g as parseFenceSpans, i as resolveMarkdownTableMode, o as chunkByParagraph } from "./ir-CugsqGH8.js";
7
+ import { i as isSilentReplyText } from "./tokens-DUnJnpMS.js";
8
+ import { r as parseTelegramTarget } from "./targets-s9KeyATC.js";
9
+ import { c as markdownToSignalTextChunks, t as sendMessageSignal } from "./send-CScblaI4.js";
10
+ import fs from "node:fs";
11
+ import path from "node:path";
12
+ //#region src/hooks/fire-and-forget.ts
13
+ function fireAndForgetHook(task, label, logger = logVerbose) {
14
+ task.catch((err) => {
15
+ logger(`${label}: ${String(err)}`);
16
+ });
17
+ }
18
+ //#endregion
19
+ //#region src/hooks/message-hook-mappers.ts
20
+ function deriveInboundMessageHookContext(ctx, overrides) {
21
+ const content = overrides?.content ?? (typeof ctx.BodyForCommands === "string" ? ctx.BodyForCommands : typeof ctx.RawBody === "string" ? ctx.RawBody : typeof ctx.Body === "string" ? ctx.Body : "");
22
+ const channelId = (ctx.OriginatingChannel ?? ctx.Surface ?? ctx.Provider ?? "").toLowerCase();
23
+ const conversationId = ctx.OriginatingTo ?? ctx.To ?? ctx.From ?? void 0;
24
+ const isGroup = Boolean(ctx.GroupSubject || ctx.GroupChannel);
25
+ return {
26
+ from: ctx.From ?? "",
27
+ to: ctx.To,
28
+ content,
29
+ body: ctx.Body,
30
+ bodyForAgent: ctx.BodyForAgent,
31
+ transcript: ctx.Transcript,
32
+ timestamp: typeof ctx.Timestamp === "number" && Number.isFinite(ctx.Timestamp) ? ctx.Timestamp : void 0,
33
+ channelId,
34
+ accountId: ctx.AccountId,
35
+ conversationId,
36
+ messageId: overrides?.messageId ?? ctx.MessageSidFull ?? ctx.MessageSid ?? ctx.MessageSidFirst ?? ctx.MessageSidLast,
37
+ senderId: ctx.SenderId,
38
+ senderName: ctx.SenderName,
39
+ senderUsername: ctx.SenderUsername,
40
+ senderE164: ctx.SenderE164,
41
+ provider: ctx.Provider,
42
+ surface: ctx.Surface,
43
+ threadId: ctx.MessageThreadId,
44
+ mediaPath: ctx.MediaPath,
45
+ mediaType: ctx.MediaType,
46
+ originatingChannel: ctx.OriginatingChannel,
47
+ originatingTo: ctx.OriginatingTo,
48
+ guildId: ctx.GroupSpace,
49
+ channelName: ctx.GroupChannel,
50
+ isGroup,
51
+ groupId: isGroup ? conversationId : void 0
52
+ };
53
+ }
54
+ function buildCanonicalSentMessageHookContext(params) {
55
+ return {
56
+ to: params.to,
57
+ content: params.content,
58
+ success: params.success,
59
+ error: params.error,
60
+ channelId: params.channelId,
61
+ accountId: params.accountId,
62
+ conversationId: params.conversationId ?? params.to,
63
+ messageId: params.messageId,
64
+ isGroup: params.isGroup,
65
+ groupId: params.groupId
66
+ };
67
+ }
68
+ function toPluginMessageContext(canonical) {
69
+ return {
70
+ channelId: canonical.channelId,
71
+ accountId: canonical.accountId,
72
+ conversationId: canonical.conversationId
73
+ };
74
+ }
75
+ function toPluginMessageReceivedEvent(canonical) {
76
+ return {
77
+ from: canonical.from,
78
+ content: canonical.content,
79
+ timestamp: canonical.timestamp,
80
+ metadata: {
81
+ to: canonical.to,
82
+ provider: canonical.provider,
83
+ surface: canonical.surface,
84
+ threadId: canonical.threadId,
85
+ originatingChannel: canonical.originatingChannel,
86
+ originatingTo: canonical.originatingTo,
87
+ messageId: canonical.messageId,
88
+ senderId: canonical.senderId,
89
+ senderName: canonical.senderName,
90
+ senderUsername: canonical.senderUsername,
91
+ senderE164: canonical.senderE164,
92
+ guildId: canonical.guildId,
93
+ channelName: canonical.channelName
94
+ }
95
+ };
96
+ }
97
+ function toPluginMessageSentEvent(canonical) {
98
+ return {
99
+ to: canonical.to,
100
+ content: canonical.content,
101
+ success: canonical.success,
102
+ ...canonical.error ? { error: canonical.error } : {}
103
+ };
104
+ }
105
+ function toInternalMessageReceivedContext(canonical) {
106
+ return {
107
+ from: canonical.from,
108
+ content: canonical.content,
109
+ timestamp: canonical.timestamp,
110
+ channelId: canonical.channelId,
111
+ accountId: canonical.accountId,
112
+ conversationId: canonical.conversationId,
113
+ messageId: canonical.messageId,
114
+ metadata: {
115
+ to: canonical.to,
116
+ provider: canonical.provider,
117
+ surface: canonical.surface,
118
+ threadId: canonical.threadId,
119
+ senderId: canonical.senderId,
120
+ senderName: canonical.senderName,
121
+ senderUsername: canonical.senderUsername,
122
+ senderE164: canonical.senderE164,
123
+ guildId: canonical.guildId,
124
+ channelName: canonical.channelName
125
+ }
126
+ };
127
+ }
128
+ function toInternalMessageTranscribedContext(canonical, cfg) {
129
+ return {
130
+ ...toInternalInboundMessageHookContextBase(canonical),
131
+ transcript: canonical.transcript ?? "",
132
+ cfg
133
+ };
134
+ }
135
+ function toInternalMessagePreprocessedContext(canonical, cfg) {
136
+ return {
137
+ ...toInternalInboundMessageHookContextBase(canonical),
138
+ transcript: canonical.transcript,
139
+ isGroup: canonical.isGroup,
140
+ groupId: canonical.groupId,
141
+ cfg
142
+ };
143
+ }
144
+ function toInternalInboundMessageHookContextBase(canonical) {
145
+ return {
146
+ from: canonical.from,
147
+ to: canonical.to,
148
+ body: canonical.body,
149
+ bodyForAgent: canonical.bodyForAgent,
150
+ timestamp: canonical.timestamp,
151
+ channelId: canonical.channelId,
152
+ conversationId: canonical.conversationId,
153
+ messageId: canonical.messageId,
154
+ senderId: canonical.senderId,
155
+ senderName: canonical.senderName,
156
+ senderUsername: canonical.senderUsername,
157
+ provider: canonical.provider,
158
+ surface: canonical.surface,
159
+ mediaPath: canonical.mediaPath,
160
+ mediaType: canonical.mediaType
161
+ };
162
+ }
163
+ function toInternalMessageSentContext(canonical) {
164
+ return {
165
+ to: canonical.to,
166
+ content: canonical.content,
167
+ success: canonical.success,
168
+ ...canonical.error ? { error: canonical.error } : {},
169
+ channelId: canonical.channelId,
170
+ accountId: canonical.accountId,
171
+ conversationId: canonical.conversationId,
172
+ messageId: canonical.messageId,
173
+ ...canonical.isGroup != null ? { isGroup: canonical.isGroup } : {},
174
+ ...canonical.groupId ? { groupId: canonical.groupId } : {}
175
+ };
176
+ }
177
+ //#endregion
178
+ //#region src/shared/text/join-segments.ts
179
+ function concatOptionalTextSegments(params) {
180
+ const separator = params.separator ?? "\n\n";
181
+ if (params.left && params.right) return `${params.left}${separator}${params.right}`;
182
+ return params.right ?? params.left;
183
+ }
184
+ function joinPresentTextSegments(segments, options) {
185
+ const separator = options?.separator ?? "\n\n";
186
+ const trim = options?.trim ?? false;
187
+ const values = [];
188
+ for (const segment of segments) {
189
+ if (typeof segment !== "string") continue;
190
+ const normalized = trim ? segment.trim() : segment;
191
+ if (!normalized) continue;
192
+ values.push(normalized);
193
+ }
194
+ return values.length > 0 ? values.join(separator) : void 0;
195
+ }
196
+ //#endregion
197
+ //#region src/plugins/hooks.ts
198
+ /**
199
+ * Plugin Hook Runner
200
+ *
201
+ * Provides utilities for executing plugin lifecycle hooks with proper
202
+ * error handling, priority ordering, and async support.
203
+ */
204
+ /**
205
+ * Get hooks for a specific hook name, sorted by priority (higher first).
206
+ */
207
+ function getHooksForName(registry, hookName) {
208
+ return registry.typedHooks.filter((h) => h.hookName === hookName).toSorted((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
209
+ }
210
+ /**
211
+ * Create a hook runner for a specific registry.
212
+ */
213
+ function createHookRunner(registry, options = {}) {
214
+ const logger = options.logger;
215
+ const catchErrors = options.catchErrors ?? true;
216
+ const mergeBeforeModelResolve = (acc, next) => ({
217
+ modelOverride: acc?.modelOverride ?? next.modelOverride,
218
+ providerOverride: acc?.providerOverride ?? next.providerOverride
219
+ });
220
+ const mergeBeforePromptBuild = (acc, next) => ({
221
+ systemPrompt: next.systemPrompt ?? acc?.systemPrompt,
222
+ prependContext: concatOptionalTextSegments({
223
+ left: acc?.prependContext,
224
+ right: next.prependContext
225
+ }),
226
+ prependSystemContext: concatOptionalTextSegments({
227
+ left: acc?.prependSystemContext,
228
+ right: next.prependSystemContext
229
+ }),
230
+ appendSystemContext: concatOptionalTextSegments({
231
+ left: acc?.appendSystemContext,
232
+ right: next.appendSystemContext
233
+ })
234
+ });
235
+ const mergeSubagentSpawningResult = (acc, next) => {
236
+ if (acc?.status === "error") return acc;
237
+ if (next.status === "error") return next;
238
+ return {
239
+ status: "ok",
240
+ threadBindingReady: Boolean(acc?.threadBindingReady || next.threadBindingReady)
241
+ };
242
+ };
243
+ const mergeSubagentDeliveryTargetResult = (acc, next) => {
244
+ if (acc?.origin) return acc;
245
+ return next;
246
+ };
247
+ const handleHookError = (params) => {
248
+ const msg = `[hooks] ${params.hookName} handler from ${params.pluginId} failed: ${String(params.error)}`;
249
+ if (catchErrors) {
250
+ logger?.error(msg);
251
+ return;
252
+ }
253
+ throw new Error(msg, { cause: params.error });
254
+ };
255
+ /**
256
+ * Run a hook that doesn't return a value (fire-and-forget style).
257
+ * All handlers are executed in parallel for performance.
258
+ */
259
+ async function runVoidHook(hookName, event, ctx) {
260
+ const hooks = getHooksForName(registry, hookName);
261
+ if (hooks.length === 0) return;
262
+ logger?.debug?.(`[hooks] running ${hookName} (${hooks.length} handlers)`);
263
+ const promises = hooks.map(async (hook) => {
264
+ try {
265
+ await hook.handler(event, ctx);
266
+ } catch (err) {
267
+ handleHookError({
268
+ hookName,
269
+ pluginId: hook.pluginId,
270
+ error: err
271
+ });
272
+ }
273
+ });
274
+ await Promise.all(promises);
275
+ }
276
+ /**
277
+ * Run a hook that can return a modifying result.
278
+ * Handlers are executed sequentially in priority order, and results are merged.
279
+ */
280
+ async function runModifyingHook(hookName, event, ctx, mergeResults) {
281
+ const hooks = getHooksForName(registry, hookName);
282
+ if (hooks.length === 0) return;
283
+ logger?.debug?.(`[hooks] running ${hookName} (${hooks.length} handlers, sequential)`);
284
+ let result;
285
+ for (const hook of hooks) try {
286
+ const handlerResult = await hook.handler(event, ctx);
287
+ if (handlerResult !== void 0 && handlerResult !== null) if (mergeResults && result !== void 0) result = mergeResults(result, handlerResult);
288
+ else result = handlerResult;
289
+ } catch (err) {
290
+ handleHookError({
291
+ hookName,
292
+ pluginId: hook.pluginId,
293
+ error: err
294
+ });
295
+ }
296
+ return result;
297
+ }
298
+ /**
299
+ * Run before_model_resolve hook.
300
+ * Allows plugins to override provider/model before model resolution.
301
+ */
302
+ async function runBeforeModelResolve(event, ctx) {
303
+ return runModifyingHook("before_model_resolve", event, ctx, mergeBeforeModelResolve);
304
+ }
305
+ /**
306
+ * Run before_prompt_build hook.
307
+ * Allows plugins to inject context and system prompt before prompt submission.
308
+ */
309
+ async function runBeforePromptBuild(event, ctx) {
310
+ return runModifyingHook("before_prompt_build", event, ctx, mergeBeforePromptBuild);
311
+ }
312
+ /**
313
+ * Run before_agent_start hook.
314
+ * Legacy compatibility hook that combines model resolve + prompt build phases.
315
+ */
316
+ async function runBeforeAgentStart(event, ctx) {
317
+ return runModifyingHook("before_agent_start", event, ctx, (acc, next) => ({
318
+ ...mergeBeforePromptBuild(acc, next),
319
+ ...mergeBeforeModelResolve(acc, next)
320
+ }));
321
+ }
322
+ /**
323
+ * Run agent_end hook.
324
+ * Allows plugins to analyze completed conversations.
325
+ * Runs in parallel (fire-and-forget).
326
+ */
327
+ async function runAgentEnd(event, ctx) {
328
+ return runVoidHook("agent_end", event, ctx);
329
+ }
330
+ /**
331
+ * Run llm_input hook.
332
+ * Allows plugins to observe the exact input payload sent to the LLM.
333
+ * Runs in parallel (fire-and-forget).
334
+ */
335
+ async function runLlmInput(event, ctx) {
336
+ return runVoidHook("llm_input", event, ctx);
337
+ }
338
+ /**
339
+ * Run llm_output hook.
340
+ * Allows plugins to observe the exact output payload returned by the LLM.
341
+ * Runs in parallel (fire-and-forget).
342
+ */
343
+ async function runLlmOutput(event, ctx) {
344
+ return runVoidHook("llm_output", event, ctx);
345
+ }
346
+ /**
347
+ * Run before_compaction hook.
348
+ */
349
+ async function runBeforeCompaction(event, ctx) {
350
+ return runVoidHook("before_compaction", event, ctx);
351
+ }
352
+ /**
353
+ * Run after_compaction hook.
354
+ */
355
+ async function runAfterCompaction(event, ctx) {
356
+ return runVoidHook("after_compaction", event, ctx);
357
+ }
358
+ /**
359
+ * Run before_reset hook.
360
+ * Fired when /new or /reset clears a session, before messages are lost.
361
+ * Runs in parallel (fire-and-forget).
362
+ */
363
+ async function runBeforeReset(event, ctx) {
364
+ return runVoidHook("before_reset", event, ctx);
365
+ }
366
+ /**
367
+ * Run message_received hook.
368
+ * Runs in parallel (fire-and-forget).
369
+ */
370
+ async function runMessageReceived(event, ctx) {
371
+ return runVoidHook("message_received", event, ctx);
372
+ }
373
+ /**
374
+ * Run message_sending hook.
375
+ * Allows plugins to modify or cancel outgoing messages.
376
+ * Runs sequentially.
377
+ */
378
+ async function runMessageSending(event, ctx) {
379
+ return runModifyingHook("message_sending", event, ctx, (acc, next) => ({
380
+ content: next.content ?? acc?.content,
381
+ cancel: next.cancel ?? acc?.cancel
382
+ }));
383
+ }
384
+ /**
385
+ * Run message_sent hook.
386
+ * Runs in parallel (fire-and-forget).
387
+ */
388
+ async function runMessageSent(event, ctx) {
389
+ return runVoidHook("message_sent", event, ctx);
390
+ }
391
+ /**
392
+ * Run before_tool_call hook.
393
+ * Allows plugins to modify or block tool calls.
394
+ * Runs sequentially.
395
+ */
396
+ async function runBeforeToolCall(event, ctx) {
397
+ return runModifyingHook("before_tool_call", event, ctx, (acc, next) => ({
398
+ params: next.params ?? acc?.params,
399
+ block: next.block ?? acc?.block,
400
+ blockReason: next.blockReason ?? acc?.blockReason
401
+ }));
402
+ }
403
+ /**
404
+ * Run after_tool_call hook.
405
+ * Runs in parallel (fire-and-forget).
406
+ */
407
+ async function runAfterToolCall(event, ctx) {
408
+ return runVoidHook("after_tool_call", event, ctx);
409
+ }
410
+ /**
411
+ * Run tool_result_persist hook.
412
+ *
413
+ * This hook is intentionally synchronous: it runs in hot paths where session
414
+ * transcripts are appended synchronously.
415
+ *
416
+ * Handlers are executed sequentially in priority order (higher first). Each
417
+ * handler may return `{ message }` to replace the message passed to the next
418
+ * handler.
419
+ */
420
+ function runToolResultPersist(event, ctx) {
421
+ const hooks = getHooksForName(registry, "tool_result_persist");
422
+ if (hooks.length === 0) return;
423
+ let current = event.message;
424
+ for (const hook of hooks) try {
425
+ const out = hook.handler({
426
+ ...event,
427
+ message: current
428
+ }, ctx);
429
+ if (out && typeof out.then === "function") {
430
+ const msg = `[hooks] tool_result_persist handler from ${hook.pluginId} returned a Promise; this hook is synchronous and the result was ignored.`;
431
+ if (catchErrors) {
432
+ logger?.warn?.(msg);
433
+ continue;
434
+ }
435
+ throw new Error(msg);
436
+ }
437
+ const next = out?.message;
438
+ if (next) current = next;
439
+ } catch (err) {
440
+ const msg = `[hooks] tool_result_persist handler from ${hook.pluginId} failed: ${String(err)}`;
441
+ if (catchErrors) logger?.error(msg);
442
+ else throw new Error(msg, { cause: err });
443
+ }
444
+ return { message: current };
445
+ }
446
+ /**
447
+ * Run before_message_write hook.
448
+ *
449
+ * This hook is intentionally synchronous: it runs on the hot path where
450
+ * session transcripts are appended synchronously.
451
+ *
452
+ * Handlers are executed sequentially in priority order (higher first).
453
+ * If any handler returns { block: true }, the message is NOT written
454
+ * to the session JSONL and we return immediately.
455
+ * If a handler returns { message }, the modified message replaces the
456
+ * original for subsequent handlers and the final write.
457
+ */
458
+ function runBeforeMessageWrite(event, ctx) {
459
+ const hooks = getHooksForName(registry, "before_message_write");
460
+ if (hooks.length === 0) return;
461
+ let current = event.message;
462
+ for (const hook of hooks) try {
463
+ const out = hook.handler({
464
+ ...event,
465
+ message: current
466
+ }, ctx);
467
+ if (out && typeof out.then === "function") {
468
+ const msg = `[hooks] before_message_write handler from ${hook.pluginId} returned a Promise; this hook is synchronous and the result was ignored.`;
469
+ if (catchErrors) {
470
+ logger?.warn?.(msg);
471
+ continue;
472
+ }
473
+ throw new Error(msg);
474
+ }
475
+ const result = out;
476
+ if (result?.block) return { block: true };
477
+ if (result?.message) current = result.message;
478
+ } catch (err) {
479
+ const msg = `[hooks] before_message_write handler from ${hook.pluginId} failed: ${String(err)}`;
480
+ if (catchErrors) logger?.error(msg);
481
+ else throw new Error(msg, { cause: err });
482
+ }
483
+ if (current !== event.message) return { message: current };
484
+ }
485
+ /**
486
+ * Run session_start hook.
487
+ * Runs in parallel (fire-and-forget).
488
+ */
489
+ async function runSessionStart(event, ctx) {
490
+ return runVoidHook("session_start", event, ctx);
491
+ }
492
+ /**
493
+ * Run session_end hook.
494
+ * Runs in parallel (fire-and-forget).
495
+ */
496
+ async function runSessionEnd(event, ctx) {
497
+ return runVoidHook("session_end", event, ctx);
498
+ }
499
+ /**
500
+ * Run subagent_spawning hook.
501
+ * Runs sequentially so channel plugins can deterministically provision session bindings.
502
+ */
503
+ async function runSubagentSpawning(event, ctx) {
504
+ return runModifyingHook("subagent_spawning", event, ctx, mergeSubagentSpawningResult);
505
+ }
506
+ /**
507
+ * Run subagent_delivery_target hook.
508
+ * Runs sequentially so channel plugins can deterministically resolve routing.
509
+ */
510
+ async function runSubagentDeliveryTarget(event, ctx) {
511
+ return runModifyingHook("subagent_delivery_target", event, ctx, mergeSubagentDeliveryTargetResult);
512
+ }
513
+ /**
514
+ * Run subagent_spawned hook.
515
+ * Runs in parallel (fire-and-forget).
516
+ */
517
+ async function runSubagentSpawned(event, ctx) {
518
+ return runVoidHook("subagent_spawned", event, ctx);
519
+ }
520
+ /**
521
+ * Run subagent_ended hook.
522
+ * Runs in parallel (fire-and-forget).
523
+ */
524
+ async function runSubagentEnded(event, ctx) {
525
+ return runVoidHook("subagent_ended", event, ctx);
526
+ }
527
+ /**
528
+ * Run gateway_start hook.
529
+ * Runs in parallel (fire-and-forget).
530
+ */
531
+ async function runGatewayStart(event, ctx) {
532
+ return runVoidHook("gateway_start", event, ctx);
533
+ }
534
+ /**
535
+ * Run gateway_stop hook.
536
+ * Runs in parallel (fire-and-forget).
537
+ */
538
+ async function runGatewayStop(event, ctx) {
539
+ return runVoidHook("gateway_stop", event, ctx);
540
+ }
541
+ /**
542
+ * Check if any hooks are registered for a given hook name.
543
+ */
544
+ function hasHooks(hookName) {
545
+ return registry.typedHooks.some((h) => h.hookName === hookName);
546
+ }
547
+ /**
548
+ * Get count of registered hooks for a given hook name.
549
+ */
550
+ function getHookCount(hookName) {
551
+ return registry.typedHooks.filter((h) => h.hookName === hookName).length;
552
+ }
553
+ return {
554
+ runBeforeModelResolve,
555
+ runBeforePromptBuild,
556
+ runBeforeAgentStart,
557
+ runLlmInput,
558
+ runLlmOutput,
559
+ runAgentEnd,
560
+ runBeforeCompaction,
561
+ runAfterCompaction,
562
+ runBeforeReset,
563
+ runMessageReceived,
564
+ runMessageSending,
565
+ runMessageSent,
566
+ runBeforeToolCall,
567
+ runAfterToolCall,
568
+ runToolResultPersist,
569
+ runBeforeMessageWrite,
570
+ runSessionStart,
571
+ runSessionEnd,
572
+ runSubagentSpawning,
573
+ runSubagentDeliveryTarget,
574
+ runSubagentSpawned,
575
+ runSubagentEnded,
576
+ runGatewayStart,
577
+ runGatewayStop,
578
+ hasHooks,
579
+ getHookCount
580
+ };
581
+ }
582
+ //#endregion
583
+ //#region src/plugins/hook-runner-global.ts
584
+ /**
585
+ * Global Plugin Hook Runner
586
+ *
587
+ * Singleton hook runner that's initialized when plugins are loaded
588
+ * and can be called from anywhere in the codebase.
589
+ */
590
+ const log$1 = createSubsystemLogger("plugins");
591
+ const hookRunnerGlobalStateKey = Symbol.for("openclaw.plugins.hook-runner-global-state");
592
+ function getHookRunnerGlobalState() {
593
+ const globalStore = globalThis;
594
+ return globalStore[hookRunnerGlobalStateKey] ??= {
595
+ hookRunner: null,
596
+ registry: null
597
+ };
598
+ }
599
+ /**
600
+ * Initialize the global hook runner with a plugin registry.
601
+ * Called once when plugins are loaded during gateway startup.
602
+ */
603
+ function initializeGlobalHookRunner(registry) {
604
+ const state = getHookRunnerGlobalState();
605
+ state.registry = registry;
606
+ state.hookRunner = createHookRunner(registry, {
607
+ logger: {
608
+ debug: (msg) => log$1.debug(msg),
609
+ warn: (msg) => log$1.warn(msg),
610
+ error: (msg) => log$1.error(msg)
611
+ },
612
+ catchErrors: true
613
+ });
614
+ const hookCount = registry.hooks.length;
615
+ if (hookCount > 0) log$1.info(`hook runner initialized with ${hookCount} registered hooks`);
616
+ }
617
+ /**
618
+ * Get the global hook runner.
619
+ * Returns null if plugins haven't been loaded yet.
620
+ */
621
+ function getGlobalHookRunner() {
622
+ return getHookRunnerGlobalState().hookRunner;
623
+ }
624
+ //#endregion
625
+ //#region src/media/audio-tags.ts
626
+ /**
627
+ * Extract audio mode tag from text.
628
+ * Supports [[audio_as_voice]] to send audio as voice bubble instead of file.
629
+ * Default is file (preserves backward compatibility).
630
+ */
631
+ function parseAudioTag(text) {
632
+ const result = parseInlineDirectives(text, { stripReplyTags: false });
633
+ return {
634
+ text: result.text,
635
+ audioAsVoice: result.audioAsVoice,
636
+ hadTag: result.hasAudioTag
637
+ };
638
+ }
639
+ //#endregion
640
+ //#region src/media/parse.ts
641
+ const MEDIA_TOKEN_RE = /\bMEDIA:\s*`?([^\n]+)`?/gi;
642
+ function normalizeMediaSource(src) {
643
+ return src.startsWith("file://") ? src.replace("file://", "") : src;
644
+ }
645
+ function cleanCandidate(raw) {
646
+ return raw.replace(/^[`"'[{(]+/, "").replace(/[`"'\\})\],]+$/, "");
647
+ }
648
+ const WINDOWS_DRIVE_RE = /^[a-zA-Z]:[\\/]/;
649
+ const SCHEME_RE = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
650
+ const HAS_FILE_EXT = /\.\w{1,10}$/;
651
+ function isLikelyLocalPath(candidate) {
652
+ return candidate.startsWith("/") || candidate.startsWith("./") || candidate.startsWith("../") || candidate.startsWith("~") || WINDOWS_DRIVE_RE.test(candidate) || candidate.startsWith("\\\\") || !SCHEME_RE.test(candidate) && (candidate.includes("/") || candidate.includes("\\"));
653
+ }
654
+ function isValidMedia(candidate, opts) {
655
+ if (!candidate) return false;
656
+ if (candidate.length > 4096) return false;
657
+ if (!opts?.allowSpaces && /\s/.test(candidate)) return false;
658
+ if (/^https?:\/\//i.test(candidate)) return true;
659
+ if (isLikelyLocalPath(candidate)) return true;
660
+ if (opts?.allowBareFilename && !SCHEME_RE.test(candidate) && HAS_FILE_EXT.test(candidate)) return true;
661
+ return false;
662
+ }
663
+ function unwrapQuoted(value) {
664
+ const trimmed = value.trim();
665
+ if (trimmed.length < 2) return;
666
+ const first = trimmed[0];
667
+ if (first !== trimmed[trimmed.length - 1]) return;
668
+ if (first !== `"` && first !== "'" && first !== "`") return;
669
+ return trimmed.slice(1, -1).trim();
670
+ }
671
+ function mayContainFenceMarkers(input) {
672
+ return input.includes("```") || input.includes("~~~");
673
+ }
674
+ function isInsideFence(fenceSpans, offset) {
675
+ return fenceSpans.some((span) => offset >= span.start && offset < span.end);
676
+ }
677
+ function splitMediaFromOutput(raw) {
678
+ const trimmedRaw = raw.trimEnd();
679
+ if (!trimmedRaw.trim()) return { text: "" };
680
+ const mayContainMediaToken = /media:/i.test(trimmedRaw);
681
+ const mayContainAudioTag = trimmedRaw.includes("[[");
682
+ if (!mayContainMediaToken && !mayContainAudioTag) return { text: trimmedRaw };
683
+ const media = [];
684
+ let foundMediaToken = false;
685
+ const hasFenceMarkers = mayContainFenceMarkers(trimmedRaw);
686
+ const fenceSpans = hasFenceMarkers ? parseFenceSpans(trimmedRaw) : [];
687
+ const lines = trimmedRaw.split("\n");
688
+ const keptLines = [];
689
+ let lineOffset = 0;
690
+ for (const line of lines) {
691
+ if (hasFenceMarkers && isInsideFence(fenceSpans, lineOffset)) {
692
+ keptLines.push(line);
693
+ lineOffset += line.length + 1;
694
+ continue;
695
+ }
696
+ if (!line.trimStart().startsWith("MEDIA:")) {
697
+ keptLines.push(line);
698
+ lineOffset += line.length + 1;
699
+ continue;
700
+ }
701
+ const matches = Array.from(line.matchAll(MEDIA_TOKEN_RE));
702
+ if (matches.length === 0) {
703
+ keptLines.push(line);
704
+ lineOffset += line.length + 1;
705
+ continue;
706
+ }
707
+ const pieces = [];
708
+ let cursor = 0;
709
+ for (const match of matches) {
710
+ const start = match.index ?? 0;
711
+ pieces.push(line.slice(cursor, start));
712
+ const payload = match[1];
713
+ const unwrapped = unwrapQuoted(payload);
714
+ const payloadValue = unwrapped ?? payload;
715
+ const parts = unwrapped ? [unwrapped] : payload.split(/\s+/).filter(Boolean);
716
+ const mediaStartIndex = media.length;
717
+ let validCount = 0;
718
+ const invalidParts = [];
719
+ let hasValidMedia = false;
720
+ for (const part of parts) {
721
+ const candidate = normalizeMediaSource(cleanCandidate(part));
722
+ if (isValidMedia(candidate, unwrapped ? { allowSpaces: true } : void 0)) {
723
+ media.push(candidate);
724
+ hasValidMedia = true;
725
+ foundMediaToken = true;
726
+ validCount += 1;
727
+ } else invalidParts.push(part);
728
+ }
729
+ const trimmedPayload = payloadValue.trim();
730
+ const looksLikeLocalPath = isLikelyLocalPath(trimmedPayload) || trimmedPayload.startsWith("file://");
731
+ if (!unwrapped && validCount === 1 && invalidParts.length > 0 && /\s/.test(payloadValue) && looksLikeLocalPath) {
732
+ const fallback = normalizeMediaSource(cleanCandidate(payloadValue));
733
+ if (isValidMedia(fallback, { allowSpaces: true })) {
734
+ media.splice(mediaStartIndex, media.length - mediaStartIndex, fallback);
735
+ hasValidMedia = true;
736
+ foundMediaToken = true;
737
+ validCount = 1;
738
+ invalidParts.length = 0;
739
+ }
740
+ }
741
+ if (!hasValidMedia) {
742
+ const fallback = normalizeMediaSource(cleanCandidate(payloadValue));
743
+ if (isValidMedia(fallback, {
744
+ allowSpaces: true,
745
+ allowBareFilename: true
746
+ })) {
747
+ media.push(fallback);
748
+ hasValidMedia = true;
749
+ foundMediaToken = true;
750
+ invalidParts.length = 0;
751
+ }
752
+ }
753
+ if (hasValidMedia) {
754
+ if (invalidParts.length > 0) pieces.push(invalidParts.join(" "));
755
+ } else if (looksLikeLocalPath) foundMediaToken = true;
756
+ else pieces.push(match[0]);
757
+ cursor = start + match[0].length;
758
+ }
759
+ pieces.push(line.slice(cursor));
760
+ const cleanedLine = pieces.join("").replace(/[ \t]{2,}/g, " ").trim();
761
+ if (cleanedLine) keptLines.push(cleanedLine);
762
+ lineOffset += line.length + 1;
763
+ }
764
+ let cleanedText = keptLines.join("\n").replace(/[ \t]+\n/g, "\n").replace(/[ \t]{2,}/g, " ").replace(/\n{2,}/g, "\n").trim();
765
+ const audioTagResult = parseAudioTag(cleanedText);
766
+ const hasAudioAsVoice = audioTagResult.audioAsVoice;
767
+ if (audioTagResult.hadTag) cleanedText = audioTagResult.text.replace(/\n{2,}/g, "\n").trim();
768
+ if (media.length === 0) {
769
+ const result = { text: foundMediaToken || hasAudioAsVoice ? cleanedText : trimmedRaw };
770
+ if (hasAudioAsVoice) result.audioAsVoice = true;
771
+ return result;
772
+ }
773
+ return {
774
+ text: cleanedText,
775
+ mediaUrls: media,
776
+ mediaUrl: media[0],
777
+ ...hasAudioAsVoice ? { audioAsVoice: true } : {}
778
+ };
779
+ }
780
+ //#endregion
781
+ //#region src/auto-reply/reply/reply-directives.ts
782
+ function parseReplyDirectives(raw, options = {}) {
783
+ const split = splitMediaFromOutput(raw);
784
+ let text = split.text ?? "";
785
+ const replyParsed = parseInlineDirectives(text, {
786
+ currentMessageId: options.currentMessageId,
787
+ stripAudioTag: false,
788
+ stripReplyTags: true
789
+ });
790
+ if (replyParsed.hasReplyTag) text = replyParsed.text;
791
+ const silentToken = options.silentToken ?? "NO_REPLY";
792
+ const isSilent = isSilentReplyText(text, silentToken);
793
+ if (isSilent) text = "";
794
+ return {
795
+ text,
796
+ mediaUrls: split.mediaUrls,
797
+ mediaUrl: split.mediaUrl,
798
+ replyToId: replyParsed.replyToId,
799
+ replyToCurrent: replyParsed.replyToCurrent,
800
+ replyToTag: replyParsed.hasReplyTag,
801
+ audioAsVoice: split.audioAsVoice,
802
+ isSilent
803
+ };
804
+ }
805
+ //#endregion
806
+ //#region src/infra/outbound/abort.ts
807
+ /**
808
+ * Utility for checking AbortSignal state and throwing a standard AbortError.
809
+ */
810
+ /**
811
+ * Throws an AbortError if the given signal has been aborted.
812
+ * Use at async checkpoints to support cancellation.
813
+ */
814
+ function throwIfAborted(abortSignal) {
815
+ if (abortSignal?.aborted) {
816
+ const err = /* @__PURE__ */ new Error("Operation aborted");
817
+ err.name = "AbortError";
818
+ throw err;
819
+ }
820
+ }
821
+ //#endregion
822
+ //#region src/infra/outbound/target-normalization.ts
823
+ function normalizeChannelTargetInput(raw) {
824
+ return raw.trim();
825
+ }
826
+ const targetNormalizerCacheByChannelId = /* @__PURE__ */ new Map();
827
+ function resolveTargetNormalizer(channelId) {
828
+ const version = getActivePluginRegistryVersion();
829
+ const cached = targetNormalizerCacheByChannelId.get(channelId);
830
+ if (cached?.version === version) return cached.normalizer;
831
+ const normalizer = getChannelPlugin(channelId)?.messaging?.normalizeTarget;
832
+ targetNormalizerCacheByChannelId.set(channelId, {
833
+ version,
834
+ normalizer
835
+ });
836
+ return normalizer;
837
+ }
838
+ function normalizeTargetForProvider(provider, raw) {
839
+ if (!raw) return;
840
+ const fallback = raw.trim() || void 0;
841
+ if (!fallback) return;
842
+ const providerId = normalizeChannelId(provider);
843
+ return ((providerId ? resolveTargetNormalizer(providerId) : void 0)?.(raw) ?? fallback) || void 0;
844
+ }
845
+ function buildTargetResolverSignature(channel) {
846
+ const resolver = getChannelPlugin(channel)?.messaging?.targetResolver;
847
+ const hint = resolver?.hint ?? "";
848
+ const looksLike = resolver?.looksLikeId;
849
+ return hashSignature(`${hint}|${looksLike ? looksLike.toString() : ""}`);
850
+ }
851
+ function hashSignature(value) {
852
+ let hash = 5381;
853
+ for (let i = 0; i < value.length; i += 1) hash = (hash << 5) + hash ^ value.charCodeAt(i);
854
+ return (hash >>> 0).toString(36);
855
+ }
856
+ //#endregion
857
+ //#region src/channels/plugins/media-limits.ts
858
+ const MB = 1024 * 1024;
859
+ function resolveChannelMediaMaxBytes(params) {
860
+ const accountId = normalizeAccountId(params.accountId);
861
+ const channelLimit = params.resolveChannelLimitMb({
862
+ cfg: params.cfg,
863
+ accountId
864
+ });
865
+ if (channelLimit) return channelLimit * MB;
866
+ if (params.cfg.agents?.defaults?.mediaMaxMb) return params.cfg.agents.defaults.mediaMaxMb * MB;
867
+ }
868
+ //#endregion
869
+ //#region src/channels/plugins/registry-loader.ts
870
+ function createChannelRegistryLoader(resolveValue) {
871
+ const cache = /* @__PURE__ */ new Map();
872
+ let lastRegistry = null;
873
+ return async (id) => {
874
+ const registry = getActivePluginRegistry();
875
+ if (registry !== lastRegistry) {
876
+ cache.clear();
877
+ lastRegistry = registry;
878
+ }
879
+ const cached = cache.get(id);
880
+ if (cached) return cached;
881
+ const pluginEntry = registry?.channels.find((entry) => entry.plugin.id === id);
882
+ if (!pluginEntry) return;
883
+ const resolved = resolveValue(pluginEntry);
884
+ if (resolved) cache.set(id, resolved);
885
+ return resolved;
886
+ };
887
+ }
888
+ //#endregion
889
+ //#region src/channels/plugins/outbound/load.ts
890
+ const loadOutboundAdapterFromRegistry = createChannelRegistryLoader((entry) => entry.plugin.outbound);
891
+ async function loadChannelOutboundAdapter(id) {
892
+ return loadOutboundAdapterFromRegistry(id);
893
+ }
894
+ //#endregion
895
+ //#region src/infra/outbound/delivery-queue.ts
896
+ const QUEUE_DIRNAME = "delivery-queue";
897
+ const FAILED_DIRNAME = "failed";
898
+ function resolveQueueDir(stateDir) {
899
+ const base = stateDir ?? resolveStateDir();
900
+ return path.join(base, QUEUE_DIRNAME);
901
+ }
902
+ function resolveFailedDir(stateDir) {
903
+ return path.join(resolveQueueDir(stateDir), FAILED_DIRNAME);
904
+ }
905
+ function resolveQueueEntryPaths(id, stateDir) {
906
+ const queueDir = resolveQueueDir(stateDir);
907
+ return {
908
+ jsonPath: path.join(queueDir, `${id}.json`),
909
+ deliveredPath: path.join(queueDir, `${id}.delivered`)
910
+ };
911
+ }
912
+ function getErrnoCode(err) {
913
+ return err && typeof err === "object" && "code" in err ? String(err.code) : null;
914
+ }
915
+ async function unlinkBestEffort(filePath) {
916
+ try {
917
+ await fs.promises.unlink(filePath);
918
+ } catch {}
919
+ }
920
+ /** Ensure the queue directory (and failed/ subdirectory) exist. */
921
+ async function ensureQueueDir(stateDir) {
922
+ const queueDir = resolveQueueDir(stateDir);
923
+ await fs.promises.mkdir(queueDir, {
924
+ recursive: true,
925
+ mode: 448
926
+ });
927
+ await fs.promises.mkdir(resolveFailedDir(stateDir), {
928
+ recursive: true,
929
+ mode: 448
930
+ });
931
+ return queueDir;
932
+ }
933
+ async function enqueueDelivery(params, stateDir) {
934
+ const queueDir = await ensureQueueDir(stateDir);
935
+ const id = generateSecureUuid();
936
+ const entry = {
937
+ id,
938
+ enqueuedAt: Date.now(),
939
+ channel: params.channel,
940
+ to: params.to,
941
+ accountId: params.accountId,
942
+ payloads: params.payloads,
943
+ threadId: params.threadId,
944
+ replyToId: params.replyToId,
945
+ bestEffort: params.bestEffort,
946
+ gifPlayback: params.gifPlayback,
947
+ silent: params.silent,
948
+ mirror: params.mirror,
949
+ retryCount: 0
950
+ };
951
+ const filePath = path.join(queueDir, `${id}.json`);
952
+ const tmp = `${filePath}.${process.pid}.tmp`;
953
+ const json = JSON.stringify(entry, null, 2);
954
+ await fs.promises.writeFile(tmp, json, {
955
+ encoding: "utf-8",
956
+ mode: 384
957
+ });
958
+ await fs.promises.rename(tmp, filePath);
959
+ return id;
960
+ }
961
+ /** Remove a successfully delivered entry from the queue.
962
+ *
963
+ * Uses a two-phase approach so that a crash between delivery and cleanup
964
+ * does not cause the message to be replayed on the next recovery scan:
965
+ * Phase 1: atomic rename {id}.json → {id}.delivered
966
+ * Phase 2: unlink the .delivered marker
967
+ * If the process dies between phase 1 and phase 2 the marker is cleaned up
968
+ * by {@link loadPendingDeliveries} on the next startup without re-sending.
969
+ */
970
+ async function ackDelivery(id, stateDir) {
971
+ const { jsonPath, deliveredPath } = resolveQueueEntryPaths(id, stateDir);
972
+ try {
973
+ await fs.promises.rename(jsonPath, deliveredPath);
974
+ } catch (err) {
975
+ if (getErrnoCode(err) === "ENOENT") {
976
+ await unlinkBestEffort(deliveredPath);
977
+ return;
978
+ }
979
+ throw err;
980
+ }
981
+ await unlinkBestEffort(deliveredPath);
982
+ }
983
+ /** Update a queue entry after a failed delivery attempt. */
984
+ async function failDelivery(id, error, stateDir) {
985
+ const filePath = path.join(resolveQueueDir(stateDir), `${id}.json`);
986
+ const raw = await fs.promises.readFile(filePath, "utf-8");
987
+ const entry = JSON.parse(raw);
988
+ entry.retryCount += 1;
989
+ entry.lastAttemptAt = Date.now();
990
+ entry.lastError = error;
991
+ const tmp = `${filePath}.${process.pid}.tmp`;
992
+ await fs.promises.writeFile(tmp, JSON.stringify(entry, null, 2), {
993
+ encoding: "utf-8",
994
+ mode: 384
995
+ });
996
+ await fs.promises.rename(tmp, filePath);
997
+ }
998
+ //#endregion
999
+ //#region src/auto-reply/reply/reply-tags.ts
1000
+ function extractReplyToTag(text, currentMessageId) {
1001
+ const result = parseInlineDirectives(text, {
1002
+ currentMessageId,
1003
+ stripAudioTag: false
1004
+ });
1005
+ return {
1006
+ cleaned: result.text,
1007
+ replyToId: result.replyToId,
1008
+ replyToCurrent: result.replyToCurrent,
1009
+ hasTag: result.hasReplyTag
1010
+ };
1011
+ }
1012
+ //#endregion
1013
+ //#region src/auto-reply/reply/reply-threading.ts
1014
+ function resolveReplyToMode(cfg, channel, accountId, chatType) {
1015
+ const provider = normalizeChannelId(channel);
1016
+ if (!provider) return "all";
1017
+ return getChannelDock(provider)?.threading?.resolveReplyToMode?.({
1018
+ cfg,
1019
+ accountId,
1020
+ chatType
1021
+ }) ?? "all";
1022
+ }
1023
+ function createReplyToModeFilter(mode, opts = {}) {
1024
+ let hasThreaded = false;
1025
+ return (payload) => {
1026
+ if (!payload.replyToId) return payload;
1027
+ if (mode === "off") {
1028
+ const isExplicit = Boolean(payload.replyToTag) || Boolean(payload.replyToCurrent);
1029
+ if (opts.allowExplicitReplyTagsWhenOff && isExplicit) return payload;
1030
+ return {
1031
+ ...payload,
1032
+ replyToId: void 0
1033
+ };
1034
+ }
1035
+ if (mode === "all") return payload;
1036
+ if (hasThreaded) return {
1037
+ ...payload,
1038
+ replyToId: void 0
1039
+ };
1040
+ hasThreaded = true;
1041
+ return payload;
1042
+ };
1043
+ }
1044
+ function createReplyToModeFilterForChannel(mode, channel) {
1045
+ const provider = normalizeChannelId(channel);
1046
+ const isWebchat = (typeof channel === "string" ? channel.trim().toLowerCase() : void 0) === "webchat";
1047
+ const dock = provider ? getChannelDock(provider) : void 0;
1048
+ return createReplyToModeFilter(mode, { allowExplicitReplyTagsWhenOff: provider ? dock?.threading?.allowExplicitReplyTagsWhenOff ?? dock?.threading?.allowTagsWhenOff ?? true : isWebchat });
1049
+ }
1050
+ //#endregion
1051
+ //#region src/auto-reply/reply/reply-payloads.ts
1052
+ function resolveReplyThreadingForPayload(params) {
1053
+ const implicitReplyToId = params.implicitReplyToId?.trim() || void 0;
1054
+ const currentMessageId = params.currentMessageId?.trim() || void 0;
1055
+ let resolved = params.payload.replyToId || params.payload.replyToCurrent === false || !implicitReplyToId ? params.payload : {
1056
+ ...params.payload,
1057
+ replyToId: implicitReplyToId
1058
+ };
1059
+ if (typeof resolved.text === "string" && resolved.text.includes("[[")) {
1060
+ const { cleaned, replyToId, replyToCurrent, hasTag } = extractReplyToTag(resolved.text, currentMessageId);
1061
+ resolved = {
1062
+ ...resolved,
1063
+ text: cleaned ? cleaned : void 0,
1064
+ replyToId: replyToId ?? resolved.replyToId,
1065
+ replyToTag: hasTag || resolved.replyToTag,
1066
+ replyToCurrent: replyToCurrent || resolved.replyToCurrent
1067
+ };
1068
+ }
1069
+ if (resolved.replyToCurrent && !resolved.replyToId && currentMessageId) resolved = {
1070
+ ...resolved,
1071
+ replyToId: currentMessageId
1072
+ };
1073
+ return resolved;
1074
+ }
1075
+ function applyReplyTagsToPayload(payload, currentMessageId) {
1076
+ return resolveReplyThreadingForPayload({
1077
+ payload,
1078
+ currentMessageId
1079
+ });
1080
+ }
1081
+ function isRenderablePayload(payload) {
1082
+ return Boolean(payload.text || payload.mediaUrl || payload.mediaUrls && payload.mediaUrls.length > 0 || payload.audioAsVoice || payload.channelData);
1083
+ }
1084
+ function shouldSuppressReasoningPayload(payload) {
1085
+ return payload.isReasoning === true;
1086
+ }
1087
+ function applyReplyThreading(params) {
1088
+ const { payloads, replyToMode, replyToChannel, currentMessageId } = params;
1089
+ const applyReplyToMode = createReplyToModeFilterForChannel(replyToMode, replyToChannel);
1090
+ const implicitReplyToId = currentMessageId?.trim() || void 0;
1091
+ return payloads.map((payload) => resolveReplyThreadingForPayload({
1092
+ payload,
1093
+ implicitReplyToId,
1094
+ currentMessageId
1095
+ })).filter(isRenderablePayload).map(applyReplyToMode);
1096
+ }
1097
+ function filterMessagingToolDuplicates(params) {
1098
+ const { payloads, sentTexts } = params;
1099
+ if (sentTexts.length === 0) return payloads;
1100
+ return payloads.filter((payload) => !isMessagingToolDuplicate(payload.text ?? "", sentTexts));
1101
+ }
1102
+ function filterMessagingToolMediaDuplicates(params) {
1103
+ const normalizeMediaForDedupe = (value) => {
1104
+ const trimmed = value.trim();
1105
+ if (!trimmed) return "";
1106
+ if (!trimmed.toLowerCase().startsWith("file://")) return trimmed;
1107
+ try {
1108
+ const parsed = new URL(trimmed);
1109
+ if (parsed.protocol === "file:") return decodeURIComponent(parsed.pathname || "");
1110
+ } catch {}
1111
+ return trimmed.replace(/^file:\/\//i, "");
1112
+ };
1113
+ const { payloads, sentMediaUrls } = params;
1114
+ if (sentMediaUrls.length === 0) return payloads;
1115
+ const sentSet = new Set(sentMediaUrls.map(normalizeMediaForDedupe).filter(Boolean));
1116
+ return payloads.map((payload) => {
1117
+ const mediaUrl = payload.mediaUrl;
1118
+ const mediaUrls = payload.mediaUrls;
1119
+ const stripSingle = mediaUrl && sentSet.has(normalizeMediaForDedupe(mediaUrl));
1120
+ const filteredUrls = mediaUrls?.filter((u) => !sentSet.has(normalizeMediaForDedupe(u)));
1121
+ if (!stripSingle && (!mediaUrls || filteredUrls?.length === mediaUrls.length)) return payload;
1122
+ return {
1123
+ ...payload,
1124
+ mediaUrl: stripSingle ? void 0 : mediaUrl,
1125
+ mediaUrls: filteredUrls?.length ? filteredUrls : void 0
1126
+ };
1127
+ });
1128
+ }
1129
+ const PROVIDER_ALIAS_MAP = { lark: "feishu" };
1130
+ function normalizeProviderForComparison(value) {
1131
+ const trimmed = value?.trim();
1132
+ if (!trimmed) return;
1133
+ const lowered = trimmed.toLowerCase();
1134
+ const normalizedChannel = normalizeChannelId(trimmed);
1135
+ if (normalizedChannel) return normalizedChannel;
1136
+ return PROVIDER_ALIAS_MAP[lowered] ?? lowered;
1137
+ }
1138
+ function normalizeThreadIdForComparison(value) {
1139
+ const trimmed = value?.trim();
1140
+ if (!trimmed) return;
1141
+ if (/^-?\d+$/.test(trimmed)) return String(Number.parseInt(trimmed, 10));
1142
+ return trimmed.toLowerCase();
1143
+ }
1144
+ function resolveTargetProviderForComparison(params) {
1145
+ const targetProvider = normalizeProviderForComparison(params.targetProvider);
1146
+ if (!targetProvider || targetProvider === "message") return params.currentProvider;
1147
+ return targetProvider;
1148
+ }
1149
+ function targetsMatchForSuppression(params) {
1150
+ if (params.provider !== "telegram") return params.targetKey === params.originTarget;
1151
+ const origin = parseTelegramTarget(params.originTarget);
1152
+ const target = parseTelegramTarget(params.targetKey);
1153
+ const targetThreadId = normalizeThreadIdForComparison(params.targetThreadId) ?? (target.messageThreadId != null ? String(target.messageThreadId) : void 0);
1154
+ const originThreadId = origin.messageThreadId != null ? String(origin.messageThreadId) : void 0;
1155
+ if (origin.chatId.trim().toLowerCase() !== target.chatId.trim().toLowerCase()) return false;
1156
+ if (originThreadId && targetThreadId != null) return originThreadId === targetThreadId;
1157
+ if (originThreadId && targetThreadId == null) return false;
1158
+ if (!originThreadId && targetThreadId != null) return false;
1159
+ return true;
1160
+ }
1161
+ function shouldSuppressMessagingToolReplies(params) {
1162
+ const provider = normalizeProviderForComparison(params.messageProvider);
1163
+ if (!provider) return false;
1164
+ const originTarget = normalizeTargetForProvider(provider, params.originatingTo);
1165
+ if (!originTarget) return false;
1166
+ const originAccount = normalizeOptionalAccountId(params.accountId);
1167
+ const sentTargets = params.messagingToolSentTargets ?? [];
1168
+ if (sentTargets.length === 0) return false;
1169
+ return sentTargets.some((target) => {
1170
+ const targetProvider = resolveTargetProviderForComparison({
1171
+ currentProvider: provider,
1172
+ targetProvider: target?.provider
1173
+ });
1174
+ if (targetProvider !== provider) return false;
1175
+ const targetKey = normalizeTargetForProvider(targetProvider, target.to);
1176
+ if (!targetKey) return false;
1177
+ const targetAccount = normalizeOptionalAccountId(target.accountId);
1178
+ if (originAccount && targetAccount && originAccount !== targetAccount) return false;
1179
+ return targetsMatchForSuppression({
1180
+ provider,
1181
+ originTarget,
1182
+ targetKey,
1183
+ targetThreadId: target.threadId
1184
+ });
1185
+ });
1186
+ }
1187
+ //#endregion
1188
+ //#region src/infra/outbound/payloads.ts
1189
+ function mergeMediaUrls(...lists) {
1190
+ const seen = /* @__PURE__ */ new Set();
1191
+ const merged = [];
1192
+ for (const list of lists) {
1193
+ if (!list) continue;
1194
+ for (const entry of list) {
1195
+ const trimmed = entry?.trim();
1196
+ if (!trimmed) continue;
1197
+ if (seen.has(trimmed)) continue;
1198
+ seen.add(trimmed);
1199
+ merged.push(trimmed);
1200
+ }
1201
+ }
1202
+ return merged;
1203
+ }
1204
+ function normalizeReplyPayloadsForDelivery(payloads) {
1205
+ const normalized = [];
1206
+ for (const payload of payloads) {
1207
+ if (shouldSuppressReasoningPayload(payload)) continue;
1208
+ const parsed = parseReplyDirectives(payload.text ?? "");
1209
+ const explicitMediaUrls = payload.mediaUrls ?? parsed.mediaUrls;
1210
+ const explicitMediaUrl = payload.mediaUrl ?? parsed.mediaUrl;
1211
+ const mergedMedia = mergeMediaUrls(explicitMediaUrls, explicitMediaUrl ? [explicitMediaUrl] : void 0);
1212
+ const resolvedMediaUrl = (explicitMediaUrls?.length ?? 0) > 1 ? void 0 : explicitMediaUrl;
1213
+ const next = {
1214
+ ...payload,
1215
+ text: parsed.text ?? "",
1216
+ mediaUrls: mergedMedia.length ? mergedMedia : void 0,
1217
+ mediaUrl: resolvedMediaUrl,
1218
+ replyToId: payload.replyToId ?? parsed.replyToId,
1219
+ replyToTag: payload.replyToTag || parsed.replyToTag,
1220
+ replyToCurrent: payload.replyToCurrent || parsed.replyToCurrent,
1221
+ audioAsVoice: Boolean(payload.audioAsVoice || parsed.audioAsVoice)
1222
+ };
1223
+ if (parsed.isSilent && mergedMedia.length === 0) continue;
1224
+ if (!isRenderablePayload(next)) continue;
1225
+ normalized.push(next);
1226
+ }
1227
+ return normalized;
1228
+ }
1229
+ function normalizeOutboundPayloads(payloads) {
1230
+ const normalizedPayloads = [];
1231
+ for (const payload of normalizeReplyPayloadsForDelivery(payloads)) {
1232
+ const mediaUrls = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
1233
+ const channelData = payload.channelData;
1234
+ const hasChannelData = Boolean(channelData && Object.keys(channelData).length > 0);
1235
+ const text = payload.text ?? "";
1236
+ if (!text && mediaUrls.length === 0 && !hasChannelData) continue;
1237
+ normalizedPayloads.push({
1238
+ text,
1239
+ mediaUrls,
1240
+ ...hasChannelData ? { channelData } : {}
1241
+ });
1242
+ }
1243
+ return normalizedPayloads;
1244
+ }
1245
+ function normalizeOutboundPayloadsForJson(payloads) {
1246
+ const normalized = [];
1247
+ for (const payload of normalizeReplyPayloadsForDelivery(payloads)) normalized.push({
1248
+ text: payload.text ?? "",
1249
+ mediaUrl: payload.mediaUrl ?? null,
1250
+ mediaUrls: payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : void 0),
1251
+ channelData: payload.channelData
1252
+ });
1253
+ return normalized;
1254
+ }
1255
+ function formatOutboundPayloadLog(payload) {
1256
+ const lines = [];
1257
+ if (payload.text) lines.push(payload.text.trimEnd());
1258
+ for (const url of payload.mediaUrls) lines.push(`MEDIA:${url}`);
1259
+ return lines.join("\n");
1260
+ }
1261
+ //#endregion
1262
+ //#region src/infra/outbound/sanitize-text.ts
1263
+ /**
1264
+ * Sanitize model output for plain-text messaging surfaces.
1265
+ *
1266
+ * LLMs occasionally produce HTML tags (`<br>`, `<b>`, `<i>`, etc.) that render
1267
+ * correctly on web but appear as literal text on WhatsApp, Signal, SMS, and IRC.
1268
+ *
1269
+ * Converts common inline HTML to lightweight-markup equivalents used by
1270
+ * WhatsApp/Signal/Telegram and strips any remaining tags.
1271
+ *
1272
+ * @see https://github.com/openclaw/openclaw/issues/31884
1273
+ * @see https://github.com/openclaw/openclaw/issues/18558
1274
+ */
1275
+ /** Channels where HTML tags should be converted/stripped. */
1276
+ const PLAIN_TEXT_SURFACES = new Set([
1277
+ "whatsapp",
1278
+ "signal",
1279
+ "sms",
1280
+ "irc",
1281
+ "telegram",
1282
+ "imessage",
1283
+ "googlechat"
1284
+ ]);
1285
+ /** Returns `true` when the channel cannot render raw HTML. */
1286
+ function isPlainTextSurface(channelId) {
1287
+ return PLAIN_TEXT_SURFACES.has(channelId.toLowerCase());
1288
+ }
1289
+ /**
1290
+ * Convert common HTML tags to their plain-text/lightweight-markup equivalents
1291
+ * and strip anything that remains.
1292
+ *
1293
+ * The function is intentionally conservative — it only targets tags that models
1294
+ * are known to produce and avoids false positives on angle brackets in normal
1295
+ * prose (e.g. `a < b`).
1296
+ */
1297
+ function sanitizeForPlainText(text) {
1298
+ return text.replace(/<((?:https?:\/\/|mailto:)[^<>\s]+)>/gi, "$1").replace(/<br\s*\/?>/gi, "\n").replace(/<\/?(p|div)>/gi, "\n").replace(/<(b|strong)>(.*?)<\/\1>/gi, "*$2*").replace(/<(i|em)>(.*?)<\/\1>/gi, "_$2_").replace(/<(s|strike|del)>(.*?)<\/\1>/gi, "~$2~").replace(/<code>(.*?)<\/code>/gi, "`$1`").replace(/<h[1-6][^>]*>(.*?)<\/h[1-6]>/gi, "\n*$1*\n").replace(/<li[^>]*>(.*?)<\/li>/gi, "• $1\n").replace(/<\/?[a-z][a-z0-9]*\b[^>]*>/gi, "").replace(/\n{3,}/g, "\n\n");
1299
+ }
1300
+ //#endregion
1301
+ //#region src/infra/outbound/deliver.ts
1302
+ const log = createSubsystemLogger("outbound/deliver");
1303
+ const TELEGRAM_TEXT_LIMIT = 4096;
1304
+ async function createChannelHandler(params) {
1305
+ const outbound = await loadChannelOutboundAdapter(params.channel);
1306
+ const handler = createPluginHandler({
1307
+ ...params,
1308
+ outbound
1309
+ });
1310
+ if (!handler) throw new Error(`Outbound not configured for channel: ${params.channel}`);
1311
+ return handler;
1312
+ }
1313
+ function createPluginHandler(params) {
1314
+ const outbound = params.outbound;
1315
+ if (!outbound?.sendText) return null;
1316
+ const baseCtx = createChannelOutboundContextBase(params);
1317
+ const sendText = outbound.sendText;
1318
+ const sendMedia = outbound.sendMedia;
1319
+ const chunker = outbound.chunker ?? null;
1320
+ const chunkerMode = outbound.chunkerMode;
1321
+ const resolveCtx = (overrides) => ({
1322
+ ...baseCtx,
1323
+ replyToId: overrides?.replyToId ?? baseCtx.replyToId,
1324
+ threadId: overrides?.threadId ?? baseCtx.threadId
1325
+ });
1326
+ return {
1327
+ chunker,
1328
+ chunkerMode,
1329
+ textChunkLimit: outbound.textChunkLimit,
1330
+ supportsMedia: Boolean(sendMedia),
1331
+ sendPayload: outbound.sendPayload ? async (payload, overrides) => outbound.sendPayload({
1332
+ ...resolveCtx(overrides),
1333
+ text: payload.text ?? "",
1334
+ mediaUrl: payload.mediaUrl,
1335
+ payload
1336
+ }) : void 0,
1337
+ sendText: async (text, overrides) => sendText({
1338
+ ...resolveCtx(overrides),
1339
+ text
1340
+ }),
1341
+ sendMedia: async (caption, mediaUrl, overrides) => {
1342
+ if (sendMedia) return sendMedia({
1343
+ ...resolveCtx(overrides),
1344
+ text: caption,
1345
+ mediaUrl
1346
+ });
1347
+ return sendText({
1348
+ ...resolveCtx(overrides),
1349
+ text: caption
1350
+ });
1351
+ }
1352
+ };
1353
+ }
1354
+ function createChannelOutboundContextBase(params) {
1355
+ return {
1356
+ cfg: params.cfg,
1357
+ to: params.to,
1358
+ accountId: params.accountId,
1359
+ replyToId: params.replyToId,
1360
+ threadId: params.threadId,
1361
+ identity: params.identity,
1362
+ gifPlayback: params.gifPlayback,
1363
+ deps: params.deps,
1364
+ silent: params.silent,
1365
+ mediaLocalRoots: params.mediaLocalRoots
1366
+ };
1367
+ }
1368
+ const isAbortError = (err) => err instanceof Error && err.name === "AbortError";
1369
+ function hasMediaPayload(payload) {
1370
+ return Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0;
1371
+ }
1372
+ function hasChannelDataPayload(payload) {
1373
+ return Boolean(payload.channelData && Object.keys(payload.channelData).length > 0);
1374
+ }
1375
+ function normalizePayloadForChannelDelivery(payload, channelId) {
1376
+ const hasMedia = hasMediaPayload(payload);
1377
+ const hasChannelData = hasChannelDataPayload(payload);
1378
+ const rawText = typeof payload.text === "string" ? payload.text : "";
1379
+ const normalizedText = channelId === "whatsapp" ? rawText.replace(/^(?:[ \t]*\r?\n)+/, "") : rawText;
1380
+ if (!normalizedText.trim()) {
1381
+ if (!hasMedia && !hasChannelData) return null;
1382
+ return {
1383
+ ...payload,
1384
+ text: ""
1385
+ };
1386
+ }
1387
+ if (normalizedText === rawText) return payload;
1388
+ return {
1389
+ ...payload,
1390
+ text: normalizedText
1391
+ };
1392
+ }
1393
+ function normalizePayloadsForChannelDelivery(payloads, channel, _cfg, _to, _accountId) {
1394
+ const normalizedPayloads = [];
1395
+ for (const payload of normalizeReplyPayloadsForDelivery(payloads)) {
1396
+ let sanitizedPayload = payload;
1397
+ if (isPlainTextSurface(channel) && sanitizedPayload.text) {
1398
+ if (!(channel === "telegram" && sanitizedPayload.channelData)) sanitizedPayload = {
1399
+ ...sanitizedPayload,
1400
+ text: sanitizeForPlainText(sanitizedPayload.text)
1401
+ };
1402
+ }
1403
+ const normalized = normalizePayloadForChannelDelivery(sanitizedPayload, channel);
1404
+ if (normalized) normalizedPayloads.push(normalized);
1405
+ }
1406
+ return normalizedPayloads;
1407
+ }
1408
+ function buildPayloadSummary(payload) {
1409
+ return {
1410
+ text: payload.text ?? "",
1411
+ mediaUrls: payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []),
1412
+ channelData: payload.channelData
1413
+ };
1414
+ }
1415
+ function createMessageSentEmitter(params) {
1416
+ const hasMessageSentHooks = params.hookRunner?.hasHooks("message_sent") ?? false;
1417
+ const canEmitInternalHook = Boolean(params.sessionKeyForInternalHooks);
1418
+ const emitMessageSent = (event) => {
1419
+ if (!hasMessageSentHooks && !canEmitInternalHook) return;
1420
+ const canonical = buildCanonicalSentMessageHookContext({
1421
+ to: params.to,
1422
+ content: event.content,
1423
+ success: event.success,
1424
+ error: event.error,
1425
+ channelId: params.channel,
1426
+ accountId: params.accountId ?? void 0,
1427
+ conversationId: params.to,
1428
+ messageId: event.messageId,
1429
+ isGroup: params.mirrorIsGroup,
1430
+ groupId: params.mirrorGroupId
1431
+ });
1432
+ if (hasMessageSentHooks) fireAndForgetHook(params.hookRunner.runMessageSent(toPluginMessageSentEvent(canonical), toPluginMessageContext(canonical)), "deliverOutboundPayloads: message_sent plugin hook failed", (message) => {
1433
+ log.warn(message);
1434
+ });
1435
+ if (!canEmitInternalHook) return;
1436
+ fireAndForgetHook(triggerInternalHook(createInternalHookEvent("message", "sent", params.sessionKeyForInternalHooks, toInternalMessageSentContext(canonical))), "deliverOutboundPayloads: message:sent internal hook failed", (message) => {
1437
+ log.warn(message);
1438
+ });
1439
+ };
1440
+ return {
1441
+ emitMessageSent,
1442
+ hasMessageSentHooks
1443
+ };
1444
+ }
1445
+ async function applyMessageSendingHook(params) {
1446
+ if (!params.enabled) return {
1447
+ cancelled: false,
1448
+ payload: params.payload,
1449
+ payloadSummary: params.payloadSummary
1450
+ };
1451
+ try {
1452
+ const sendingResult = await params.hookRunner.runMessageSending({
1453
+ to: params.to,
1454
+ content: params.payloadSummary.text,
1455
+ metadata: {
1456
+ channel: params.channel,
1457
+ accountId: params.accountId,
1458
+ mediaUrls: params.payloadSummary.mediaUrls
1459
+ }
1460
+ }, {
1461
+ channelId: params.channel,
1462
+ accountId: params.accountId ?? void 0
1463
+ });
1464
+ if (sendingResult?.cancel) return {
1465
+ cancelled: true,
1466
+ payload: params.payload,
1467
+ payloadSummary: params.payloadSummary
1468
+ };
1469
+ if (sendingResult?.content == null) return {
1470
+ cancelled: false,
1471
+ payload: params.payload,
1472
+ payloadSummary: params.payloadSummary
1473
+ };
1474
+ return {
1475
+ cancelled: false,
1476
+ payload: {
1477
+ ...params.payload,
1478
+ text: sendingResult.content
1479
+ },
1480
+ payloadSummary: {
1481
+ ...params.payloadSummary,
1482
+ text: sendingResult.content
1483
+ }
1484
+ };
1485
+ } catch {
1486
+ return {
1487
+ cancelled: false,
1488
+ payload: params.payload,
1489
+ payloadSummary: params.payloadSummary
1490
+ };
1491
+ }
1492
+ }
1493
+ async function deliverOutboundPayloads(params) {
1494
+ const { channel, to, payloads } = params;
1495
+ const queueId = params.skipQueue ? null : await enqueueDelivery({
1496
+ channel,
1497
+ to,
1498
+ accountId: params.accountId,
1499
+ payloads,
1500
+ threadId: params.threadId,
1501
+ replyToId: params.replyToId,
1502
+ bestEffort: params.bestEffort,
1503
+ gifPlayback: params.gifPlayback,
1504
+ silent: params.silent,
1505
+ mirror: params.mirror
1506
+ }).catch(() => null);
1507
+ let hadPartialFailure = false;
1508
+ const wrappedParams = params.onError ? {
1509
+ ...params,
1510
+ onError: (err, payload) => {
1511
+ hadPartialFailure = true;
1512
+ params.onError(err, payload);
1513
+ }
1514
+ } : params;
1515
+ try {
1516
+ const results = await deliverOutboundPayloadsCore(wrappedParams);
1517
+ if (queueId) if (hadPartialFailure) await failDelivery(queueId, "partial delivery failure (bestEffort)").catch(() => {});
1518
+ else await ackDelivery(queueId).catch(() => {});
1519
+ return results;
1520
+ } catch (err) {
1521
+ if (queueId) if (isAbortError(err)) await ackDelivery(queueId).catch(() => {});
1522
+ else await failDelivery(queueId, err instanceof Error ? err.message : String(err)).catch(() => {});
1523
+ throw err;
1524
+ }
1525
+ }
1526
+ /** Core delivery logic (extracted for queue wrapper). */
1527
+ async function deliverOutboundPayloadsCore(params) {
1528
+ const { cfg, channel, to, payloads } = params;
1529
+ const accountId = params.accountId;
1530
+ const deps = params.deps;
1531
+ const abortSignal = params.abortSignal;
1532
+ const sendSignal = params.deps?.sendSignal ?? sendMessageSignal;
1533
+ const mediaLocalRoots = getAgentScopedMediaLocalRoots(cfg, params.session?.agentId ?? params.mirror?.agentId);
1534
+ const results = [];
1535
+ const handler = await createChannelHandler({
1536
+ cfg,
1537
+ channel,
1538
+ to,
1539
+ deps,
1540
+ accountId,
1541
+ replyToId: params.replyToId,
1542
+ threadId: params.threadId,
1543
+ identity: params.identity,
1544
+ gifPlayback: params.gifPlayback,
1545
+ silent: params.silent,
1546
+ mediaLocalRoots
1547
+ });
1548
+ const configuredTextLimit = handler.chunker ? resolveTextChunkLimit(cfg, channel, accountId, { fallbackLimit: handler.textChunkLimit }) : void 0;
1549
+ const textLimit = channel === "telegram" && typeof configuredTextLimit === "number" ? Math.min(configuredTextLimit, TELEGRAM_TEXT_LIMIT) : configuredTextLimit;
1550
+ const chunkMode = handler.chunker ? resolveChunkMode(cfg, channel, accountId) : "length";
1551
+ const isSignalChannel = channel === "signal";
1552
+ const signalTableMode = isSignalChannel ? resolveMarkdownTableMode({
1553
+ cfg,
1554
+ channel: "signal",
1555
+ accountId
1556
+ }) : "code";
1557
+ const signalMaxBytes = isSignalChannel ? resolveChannelMediaMaxBytes({
1558
+ cfg,
1559
+ resolveChannelLimitMb: ({ cfg, accountId }) => cfg.channels?.signal?.accounts?.[accountId]?.mediaMaxMb ?? cfg.channels?.signal?.mediaMaxMb,
1560
+ accountId
1561
+ }) : void 0;
1562
+ const sendTextChunks = async (text, overrides) => {
1563
+ throwIfAborted(abortSignal);
1564
+ if (!handler.chunker || textLimit === void 0) {
1565
+ results.push(await handler.sendText(text, overrides));
1566
+ return;
1567
+ }
1568
+ if (chunkMode === "newline") {
1569
+ const blockChunks = (handler.chunkerMode ?? "text") === "markdown" ? chunkMarkdownTextWithMode(text, textLimit, "newline") : chunkByParagraph(text, textLimit);
1570
+ if (!blockChunks.length && text) blockChunks.push(text);
1571
+ for (const blockChunk of blockChunks) {
1572
+ const chunks = handler.chunker(blockChunk, textLimit);
1573
+ if (!chunks.length && blockChunk) chunks.push(blockChunk);
1574
+ for (const chunk of chunks) {
1575
+ throwIfAborted(abortSignal);
1576
+ results.push(await handler.sendText(chunk, overrides));
1577
+ }
1578
+ }
1579
+ return;
1580
+ }
1581
+ const chunks = handler.chunker(text, textLimit);
1582
+ for (const chunk of chunks) {
1583
+ throwIfAborted(abortSignal);
1584
+ results.push(await handler.sendText(chunk, overrides));
1585
+ }
1586
+ };
1587
+ const sendSignalText = async (text, styles) => {
1588
+ throwIfAborted(abortSignal);
1589
+ return {
1590
+ channel: "signal",
1591
+ ...await sendSignal(to, text, {
1592
+ cfg,
1593
+ maxBytes: signalMaxBytes,
1594
+ accountId: accountId ?? void 0,
1595
+ textMode: "plain",
1596
+ textStyles: styles
1597
+ })
1598
+ };
1599
+ };
1600
+ const sendSignalTextChunks = async (text) => {
1601
+ throwIfAborted(abortSignal);
1602
+ let signalChunks = textLimit === void 0 ? markdownToSignalTextChunks(text, Number.POSITIVE_INFINITY, { tableMode: signalTableMode }) : markdownToSignalTextChunks(text, textLimit, { tableMode: signalTableMode });
1603
+ if (signalChunks.length === 0 && text) signalChunks = [{
1604
+ text,
1605
+ styles: []
1606
+ }];
1607
+ for (const chunk of signalChunks) {
1608
+ throwIfAborted(abortSignal);
1609
+ results.push(await sendSignalText(chunk.text, chunk.styles));
1610
+ }
1611
+ };
1612
+ const sendSignalMedia = async (caption, mediaUrl) => {
1613
+ throwIfAborted(abortSignal);
1614
+ const formatted = markdownToSignalTextChunks(caption, Number.POSITIVE_INFINITY, { tableMode: signalTableMode })[0] ?? {
1615
+ text: caption,
1616
+ styles: []
1617
+ };
1618
+ return {
1619
+ channel: "signal",
1620
+ ...await sendSignal(to, formatted.text, {
1621
+ cfg,
1622
+ mediaUrl,
1623
+ maxBytes: signalMaxBytes,
1624
+ accountId: accountId ?? void 0,
1625
+ textMode: "plain",
1626
+ textStyles: formatted.styles,
1627
+ mediaLocalRoots
1628
+ })
1629
+ };
1630
+ };
1631
+ const normalizedPayloads = normalizePayloadsForChannelDelivery(payloads, channel, cfg, to, accountId);
1632
+ const hookRunner = getGlobalHookRunner();
1633
+ const sessionKeyForInternalHooks = params.mirror?.sessionKey ?? params.session?.key;
1634
+ const mirrorIsGroup = params.mirror?.isGroup;
1635
+ const mirrorGroupId = params.mirror?.groupId;
1636
+ const { emitMessageSent, hasMessageSentHooks } = createMessageSentEmitter({
1637
+ hookRunner,
1638
+ channel,
1639
+ to,
1640
+ accountId,
1641
+ sessionKeyForInternalHooks,
1642
+ mirrorIsGroup,
1643
+ mirrorGroupId
1644
+ });
1645
+ const hasMessageSendingHooks = hookRunner?.hasHooks("message_sending") ?? false;
1646
+ if (hasMessageSentHooks && params.session?.agentId && !sessionKeyForInternalHooks) log.warn("deliverOutboundPayloads: session.agentId present without session key; internal message:sent hook will be skipped", {
1647
+ channel,
1648
+ to,
1649
+ agentId: params.session.agentId
1650
+ });
1651
+ for (const payload of normalizedPayloads) {
1652
+ let payloadSummary = buildPayloadSummary(payload);
1653
+ try {
1654
+ throwIfAborted(abortSignal);
1655
+ const hookResult = await applyMessageSendingHook({
1656
+ hookRunner,
1657
+ enabled: hasMessageSendingHooks,
1658
+ payload,
1659
+ payloadSummary,
1660
+ to,
1661
+ channel,
1662
+ accountId
1663
+ });
1664
+ if (hookResult.cancelled) continue;
1665
+ const effectivePayload = hookResult.payload;
1666
+ payloadSummary = hookResult.payloadSummary;
1667
+ params.onPayload?.(payloadSummary);
1668
+ const sendOverrides = {
1669
+ replyToId: effectivePayload.replyToId ?? params.replyToId ?? void 0,
1670
+ threadId: params.threadId ?? void 0
1671
+ };
1672
+ if (handler.sendPayload && effectivePayload.channelData) {
1673
+ const delivery = await handler.sendPayload(effectivePayload, sendOverrides);
1674
+ results.push(delivery);
1675
+ emitMessageSent({
1676
+ success: true,
1677
+ content: payloadSummary.text,
1678
+ messageId: delivery.messageId
1679
+ });
1680
+ continue;
1681
+ }
1682
+ if (payloadSummary.mediaUrls.length === 0) {
1683
+ const beforeCount = results.length;
1684
+ if (isSignalChannel) await sendSignalTextChunks(payloadSummary.text);
1685
+ else await sendTextChunks(payloadSummary.text, sendOverrides);
1686
+ const messageId = results.at(-1)?.messageId;
1687
+ emitMessageSent({
1688
+ success: results.length > beforeCount,
1689
+ content: payloadSummary.text,
1690
+ messageId
1691
+ });
1692
+ continue;
1693
+ }
1694
+ if (!handler.supportsMedia) {
1695
+ log.warn("Plugin outbound adapter does not implement sendMedia; media URLs will be dropped and text fallback will be used", {
1696
+ channel,
1697
+ to,
1698
+ mediaCount: payloadSummary.mediaUrls.length
1699
+ });
1700
+ const fallbackText = payloadSummary.text.trim();
1701
+ if (!fallbackText) throw new Error("Plugin outbound adapter does not implement sendMedia and no text fallback is available for media payload");
1702
+ const beforeCount = results.length;
1703
+ await sendTextChunks(fallbackText, sendOverrides);
1704
+ const messageId = results.at(-1)?.messageId;
1705
+ emitMessageSent({
1706
+ success: results.length > beforeCount,
1707
+ content: payloadSummary.text,
1708
+ messageId
1709
+ });
1710
+ continue;
1711
+ }
1712
+ let first = true;
1713
+ let lastMessageId;
1714
+ for (const url of payloadSummary.mediaUrls) {
1715
+ throwIfAborted(abortSignal);
1716
+ const caption = first ? payloadSummary.text : "";
1717
+ first = false;
1718
+ if (isSignalChannel) {
1719
+ const delivery = await sendSignalMedia(caption, url);
1720
+ results.push(delivery);
1721
+ lastMessageId = delivery.messageId;
1722
+ } else {
1723
+ const delivery = await handler.sendMedia(caption, url, sendOverrides);
1724
+ results.push(delivery);
1725
+ lastMessageId = delivery.messageId;
1726
+ }
1727
+ }
1728
+ emitMessageSent({
1729
+ success: true,
1730
+ content: payloadSummary.text,
1731
+ messageId: lastMessageId
1732
+ });
1733
+ } catch (err) {
1734
+ emitMessageSent({
1735
+ success: false,
1736
+ content: payloadSummary.text,
1737
+ error: err instanceof Error ? err.message : String(err)
1738
+ });
1739
+ if (!params.bestEffort) throw err;
1740
+ params.onError?.(err, payloadSummary);
1741
+ }
1742
+ }
1743
+ if (params.mirror && results.length > 0) {
1744
+ const mirrorText = resolveMirroredTranscriptText({
1745
+ text: params.mirror.text,
1746
+ mediaUrls: params.mirror.mediaUrls
1747
+ });
1748
+ if (mirrorText) await appendAssistantMessageToSessionTranscript({
1749
+ agentId: params.mirror.agentId,
1750
+ sessionKey: params.mirror.sessionKey,
1751
+ text: mirrorText
1752
+ });
1753
+ }
1754
+ return results;
1755
+ }
1756
+ //#endregion
1757
+ export { toInternalMessageTranscribedContext as A, initializeGlobalHookRunner as C, toInternalMessagePreprocessedContext as D, deriveInboundMessageHookContext as E, toPluginMessageReceivedEvent as M, toPluginMessageSentEvent as N, toInternalMessageReceivedContext as O, fireAndForgetHook as P, getGlobalHookRunner as S, buildCanonicalSentMessageHookContext as T, normalizeChannelTargetInput as _, normalizeReplyPayloadsForDelivery as a, parseReplyDirectives as b, filterMessagingToolDuplicates as c, shouldSuppressMessagingToolReplies as d, shouldSuppressReasoningPayload as f, buildTargetResolverSignature as g, resolveChannelMediaMaxBytes as h, normalizeOutboundPayloadsForJson as i, toPluginMessageContext as j, toInternalMessageSentContext as k, filterMessagingToolMediaDuplicates as l, resolveReplyToMode as m, formatOutboundPayloadLog as n, applyReplyTagsToPayload as o, createReplyToModeFilterForChannel as p, normalizeOutboundPayloads as r, applyReplyThreading as s, deliverOutboundPayloads as t, isRenderablePayload as u, normalizeTargetForProvider as v, joinPresentTextSegments as w, splitMediaFromOutput as x, throwIfAborted as y };