@hanzo/bot 2026.3.8 → 2026.3.10

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